diff options
author | Jon Bratseth <bratseth@yahoo-inc.com> | 2016-06-15 23:09:44 +0200 |
---|---|---|
committer | Jon Bratseth <bratseth@yahoo-inc.com> | 2016-06-15 23:09:44 +0200 |
commit | 72231250ed81e10d66bfe70701e64fa5fe50f712 (patch) | |
tree | 2728bba1131a6f6e5bdf95afec7d7ff9358dac50 /documentapi/src/main |
Publish
Diffstat (limited to 'documentapi/src/main')
130 files changed, 14325 insertions, 0 deletions
diff --git a/documentapi/src/main/docapi-with-dependencies.xml b/documentapi/src/main/docapi-with-dependencies.xml new file mode 100644 index 00000000000..f16f31e0398 --- /dev/null +++ b/documentapi/src/main/docapi-with-dependencies.xml @@ -0,0 +1,19 @@ +<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. --> +<assembly> + <id>jar-with-dependencies</id> + <formats> + <format>jar</format> + </formats> + <includeBaseDirectory>false</includeBaseDirectory> + <dependencySets> + <dependencySet> + <unpack>true</unpack> + <scope>runtime</scope> + </dependencySet> + </dependencySets> + <fileSets> + <fileSet> + <directory>${project.build.outputDirectory}</directory> + </fileSet> + </fileSets> +</assembly> diff --git a/documentapi/src/main/java/com/yahoo/documentapi/AckToken.java b/documentapi/src/main/java/com/yahoo/documentapi/AckToken.java new file mode 100644 index 00000000000..6bacad38786 --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/AckToken.java @@ -0,0 +1,21 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi; + +/** + * Token to use to acknowledge data for visiting. + * + * @author <a href="mailto:thomasg@yahoo-inc.com">Thomas Gundersen</a> + */ +public class AckToken { + + public Object ackObject; + + /** + * Creates ack token from the supplied parameter. + * + * @param ackObject the object to use to ack data + */ + public AckToken(Object ackObject) { + this.ackObject = ackObject; + } +} diff --git a/documentapi/src/main/java/com/yahoo/documentapi/AsyncParameters.java b/documentapi/src/main/java/com/yahoo/documentapi/AsyncParameters.java new file mode 100644 index 00000000000..3e06d5f8d94 --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/AsyncParameters.java @@ -0,0 +1,21 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi; + +/** + * Parameters for creating an async session + * + * @author <a href="mailto:bratseth@yahoo-inc.com">Jon Bratseth</a> + */ +public class AsyncParameters extends Parameters { + + private ResponseHandler responseHandler = null; + + public ResponseHandler getResponseHandler() { + return responseHandler; + } + + public AsyncParameters setResponseHandler(ResponseHandler responseHandler) { + this.responseHandler = responseHandler; + return this; + } +} diff --git a/documentapi/src/main/java/com/yahoo/documentapi/AsyncSession.java b/documentapi/src/main/java/com/yahoo/documentapi/AsyncSession.java new file mode 100644 index 00000000000..183e4ea63d3 --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/AsyncSession.java @@ -0,0 +1,142 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi; + +import com.yahoo.document.Document; +import com.yahoo.document.DocumentId; +import com.yahoo.document.DocumentUpdate; +import com.yahoo.documentapi.messagebus.protocol.DocumentProtocol; + +/** + * <p>A session for asynchronous access to a document repository. + * This class provides document repository writes and random access with high + * throughput.</p> + * + * <p>All operations which are <i>accepted</i> by an async session will cause one or more + * {@link Response responses} to be returned within the timeout limit. When an operation fails, + * the response will contain the argument which was submitted to the operation.</p> + * + * @author bratseth + */ +public interface AsyncSession extends Session { + + /** + * <p>Puts a document. This method returns immediately.</p> + * + * <p>If this result is a success, this + * call will cause one or more {@link DocumentResponse} objects to appear within the timeout time of this session. + * The response returned later will either be a success, or contain the document submitted here. + * If it was not a success, this method has no further effects.</p> + * + * @param document the Document to put + * @return the synchronous result of this operation + */ + Result put(Document document); + + /** + * <p>Puts a document. This method returns immediately.</p> + * + * <p>If this result is a success, this + * call will cause one or more {@link DocumentResponse} objects to appear within the timeout time of this session. + * The response returned later will either be a success, or contain the document submitted here. + * If it was not a success, this method has no further effects.</p> + * + * @param document the Document to put + * @return the synchronous result of this operation + */ + Result put(Document document, DocumentProtocol.Priority priority); + + /** + * <p>Gets a document. This method returns immediately.</p> + * + * <p>If this result is a success, this + * call will cause one or more {@link DocumentResponse} objects to appear within the timeout time of this session. + * The response returned later will contain the requested document if it is a success. + * If it was not a success, this method has no further effects.</p> + * + * @param id the id of the document to get + * @return the synchronous result of this operation + * @throws UnsupportedOperationException if this access implementation does not support retrieving + */ + Result get(DocumentId id); + + /** + * <p>Gets a document. This method returns immediately.</p> + * + * <p>If this result is a success, this + * call will cause one or more {@link DocumentResponse} objects to appear within the timeout time of this session. + * The response returned later will contain the requested document if it is a success. + * If it was not a success, this method has no further effects.</p> + * + * @param id the id of the document to get + * @param priority The priority with which to perform this operation. + * @return the synchronous result of this operation + * @throws UnsupportedOperationException if this access implementation does not support retrieving + */ + Result get(DocumentId id, boolean headersOnly, DocumentProtocol.Priority priority); + + /** + * <p>Removes a document if it is present. This method returns immediately.</p> + * + * <p>If this result is a success, this + * call will cause one or more {@link RemoveResponse} objects to appear within the timeout time of this session. + * The response returned later will either be a success, or contain the document id submitted here. + * If it was not a success, this method has no further effects.</p> + * + * @param id the id of the document to remove + * @return the synchronous result of this operation + * @throws UnsupportedOperationException if this access implementation does not support removal + */ + Result remove(DocumentId id); + + /** + * <p>Removes a document if it is present. This method returns immediately.</p> + * + * <p>If this result is a success, this + * call will cause one or more {@link DocumentIdResponse} objects to apprear within the timeout time of this session. + * The response returned later will either be a success, or contain the document id submitted here. + * If it was not a success, this method has no further effects.</p> + * + * @param id the id of the document to remove + * @param priority The priority with which to perform this operation. + * @return the synchronous result of this operation + * @throws UnsupportedOperationException if this access implementation does not support removal + */ + Result remove(DocumentId id, DocumentProtocol.Priority priority); + + /** + * <p>Updates a document. This method returns immediately.</p> + * + * <p>If this result is a success, this + * call will cause one or more {@link DocumentUpdateResponse} within the timeout time of this session. + * The returned response returned later will either be a success or contain the update submitted here. + * If it was not a success, this method has no further effects.</p> + * + * @param update the updates to perform + * @return the synchronous result of this operation + * @throws UnsupportedOperationException if this access implementation does not support update + */ + Result update(DocumentUpdate update); + + /** + * <p>Updates a document. This method returns immediately.</p> + * + * <p>If this result is a success, this + * call will cause one or more {@link DocumentUpdateResponse} within the timeout time of this session. + * The returned response returned later will either be a success or contain the update submitted here. + * If it was not a success, this method has no further effects.</p> + * + * @param update the updates to perform + * @param priority The priority with which to perform this operation. + * @return the synchronous result of this operation + * @throws UnsupportedOperationException if this access implementation does not support update + */ + Result update(DocumentUpdate update, DocumentProtocol.Priority priority); + + /** + * Returns the current send window size of the session. + * + * @return Returns the window size. + */ + double getCurrentWindowSize(); + +} diff --git a/documentapi/src/main/java/com/yahoo/documentapi/BucketListVisitorResponse.java b/documentapi/src/main/java/com/yahoo/documentapi/BucketListVisitorResponse.java new file mode 100644 index 00000000000..1ccb9c1c2fa --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/BucketListVisitorResponse.java @@ -0,0 +1,29 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi; + +import com.yahoo.document.BucketId; +import com.yahoo.documentapi.messagebus.protocol.DocumentListEntry; + +import java.util.List; + +/** + * @author <a href="mailto:einarmr@yahoo-inc.com">Einar M R Rosenvinge</a> + */ +public class BucketListVisitorResponse extends VisitorResponse { + private BucketId bucketId; + private List<DocumentListEntry> documents; + + public BucketListVisitorResponse(BucketId bucketId, List<DocumentListEntry> documents, AckToken token) { + super(token); + this.bucketId = bucketId; + this.documents = documents; + } + + public BucketId getBucketId() { + return bucketId; + } + + public List<DocumentListEntry> getDocuments() { + return documents; + } +} diff --git a/documentapi/src/main/java/com/yahoo/documentapi/DocumentAccess.java b/documentapi/src/main/java/com/yahoo/documentapi/DocumentAccess.java new file mode 100644 index 00000000000..0d781e4ca95 --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/DocumentAccess.java @@ -0,0 +1,172 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi; + +import com.yahoo.document.DocumentTypeManager; +import com.yahoo.document.DocumentTypeManagerConfigurer; +import com.yahoo.document.select.parser.ParseException; +import com.yahoo.config.subscription.ConfigSubscriber; + +/** + * <p>This is the starting point of the <b>document api</b>. This api provides + * access to documents in a document repository. The document api contains four + * separate access types: </p> + * + * <ul><li><b>Synchronous random access</b> - provided by {@link SyncSession}, + * allows simple access where throughput is not a concern.</li> + * <li><b>Asynchronous random access</b> - provided by {@link AsyncSession}, + * allows document repository writes and random access with high + * throughput.</li> + * <li><b>Visiting</b> - provided by {@link VisitorSession}, allows a set of + * documents to be accessed in an order decided by the document repository. This + * allows much higher read throughput than random access.</li> + * <li><b>Subscription</b> - provided by {@link SubscriptionSession}, allows + * changes to a defined set of documents in the repository to be + * visited.</li></ul> + * + * <p>This class is the factory for creating the four session types mentioned + * above.</p> + * + * <p>There may be multiple implementations of the document api classes. If + * default configuration is sufficient, use the {@link #createDefault} method to + * return a running document access. Note that there are running threads within + * an access object, so you must shut it down when done.</p> + * + * <p>An implementation of the Document Api may support just a subset of the + * access types defined in this interface. For example, some document + * repositories, like indexes, are <i>write only</i>. Others may support random + * access, but not visiting and subscription. Any method which is not supported + * by the underlying implementation will throw + * UnsupportedOperationException.</p> + * + * <p>Access to this class is thread-safe.</p> + * + * @author bratseth + * @author <a href="mailto:einarmr@yahoo-inc.com">Einar Rosenvinge</a> + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public abstract class DocumentAccess { + + protected DocumentTypeManager documentMgr; + protected ConfigSubscriber documentTypeManagerConfig; + + /** + * <p>This is a convenience method to return a document access object with + * all default parameter values. The client that calls this method is also + * responsible for shutting the object down when done. If an error occurred + * while attempting to create such an object, this method will throw an + * exception.</p> + * + * @return A running document access object with all default configuration. + */ + public static DocumentAccess createDefault() { + return new com.yahoo.documentapi.messagebus.MessageBusDocumentAccess(); + } + + /** + * <p>Constructs a new document access object.</p> + * + * @param params The parameters to use for setup. + */ + protected DocumentAccess(DocumentAccessParams params) { + super(); + documentMgr = new DocumentTypeManager(); + documentTypeManagerConfig = DocumentTypeManagerConfigurer.configure(documentMgr, params.getDocumentManagerConfigId()); + } + + /** + * <p>Returns a session for synchronous document access. Use this for simple + * access.</p> + * + * @param parameters The parameters of this sync session. + * @return A session to use for synchronous document access. + * @throws UnsupportedOperationException If this access implementation does + * not support synchronous access. + * @throws RuntimeException If an error prevented the session + * from being created. + */ + public abstract SyncSession createSyncSession(SyncParameters parameters); + + /** + * <p>Returns a session for asynchronous document access. Use this if high + * operation throughput is required.</p> + * + * @param parameters The parameters of this async session. + * @return A session to use for asynchronous document access. + * @throws UnsupportedOperationException If this access implementation does + * not support asynchronous access. + * @throws RuntimeException If an error prevented the session + * from being created. + */ + public abstract AsyncSession createAsyncSession(AsyncParameters parameters); + + /** + * <p>Run a visitor with the given visitor parameters, and get the result + * back here.</p> + * + * @param parameters The parameters of this visitor session. + * @return A session used to track progress of the visitor and get the + * actual data returned. + * @throws UnsupportedOperationException If this access implementation does + * not support visiting. + * @throws RuntimeException If an error prevented the session + * from being created. + * @throws ParseException If the document selection string + * could not be parsed. + */ + public abstract VisitorSession createVisitorSession(VisitorParameters parameters) throws ParseException; + + /** + * <p>Creates a destination session for receiving data from visiting. The + * visitor must be started and progress tracked through a visitor + * session.</p> + * + * @param parameters The parameters of this visitor destination session. + * @return A session used to get the actual data returned. + * @throws UnsupportedOperationException If this access implementation does + * not support visiting. + */ + public abstract VisitorDestinationSession createVisitorDestinationSession(VisitorDestinationParameters parameters); + + /** + * <p>Creates a subscription and returns a session for getting data from + * it. Use this to get document operations being done by other parties.</p> + * + * @param parameters The parameters of this subscription session. + * @return A session to use for document subscription. + * @throws UnsupportedOperationException If this access implementation does + * not support subscription. + * @throws RuntimeException If an error prevented the session + * from being created. + */ + public abstract SubscriptionSession createSubscription(SubscriptionParameters parameters); + + /** + * <p>Returns a session for document subscription. Use this to get document + * operations being done by other parties.</p> + * + * @param parameters The parameters of this subscription session. + * @return A session to use for document subscription. + * @throws UnsupportedOperationException If this access implementation does + * not support subscription. + * @throws RuntimeException If an error prevented the session + * from being created. + */ + public abstract SubscriptionSession openSubscription(SubscriptionParameters parameters); + + /** + * <p>Shuts down the underlying sessions used by this DocumentAccess; + * subsequent use of this DocumentAccess will throw unspecified exceptions, + * depending on implementation.</p> + */ + public abstract void shutdown(); + + /** + * <p>Returns the {@link DocumentTypeManager} used by this + * DocumentAccess.</p> + * + * @return The document type manager. + */ + public DocumentTypeManager getDocumentTypeManager() { + return documentMgr; + } +} diff --git a/documentapi/src/main/java/com/yahoo/documentapi/DocumentAccessException.java b/documentapi/src/main/java/com/yahoo/documentapi/DocumentAccessException.java new file mode 100644 index 00000000000..4c9c3b0e817 --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/DocumentAccessException.java @@ -0,0 +1,40 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi; + +import java.util.Set; +import java.util.HashSet; + +/** + * General exception thrown from various methods in the Vespa Document API. + * + * @author <a href="mailto:einarmr@yahoo-inc.com">Einar M R Rosenvinge</a> + */ +public class DocumentAccessException extends RuntimeException { + + private Set<Integer> errorCodes = new HashSet<>(); + + public Set<Integer> getErrorCodes() { + return errorCodes; + } + + public DocumentAccessException() { + super(); + } + + public DocumentAccessException(String message) { + super(message); + } + + public DocumentAccessException(String message, Set<Integer> errorCodes) { + super(message); + this.errorCodes = errorCodes; + } + + public DocumentAccessException(String message, Throwable cause) { + super(message, cause); + } + + public DocumentAccessException(Throwable cause) { + super(cause); + } +} diff --git a/documentapi/src/main/java/com/yahoo/documentapi/DocumentAccessParams.java b/documentapi/src/main/java/com/yahoo/documentapi/DocumentAccessParams.java new file mode 100755 index 00000000000..57cfdbd32e1 --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/DocumentAccessParams.java @@ -0,0 +1,33 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi;
+
+/**
+ * Superclass of the classes which contains the parameters for creating or opening a document access.
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class DocumentAccessParams {
+
+ // The id to resolve to document manager config.
+ private String documentManagerConfigId = "client";
+
+ /**
+ * Returns the config id that the document manager should subscribe to.
+ *
+ * @return The config id.
+ */
+ public String getDocumentManagerConfigId() {
+ return documentManagerConfigId;
+ }
+
+ /**
+ * Sets the config id that the document manager should subscribe to.
+ *
+ * @param configId The config id.
+ * @return This, to allow chaining.
+ */
+ public DocumentAccessParams setDocumentManagerConfigId(String configId) {
+ documentManagerConfigId = configId;
+ return this;
+ }
+}
\ No newline at end of file diff --git a/documentapi/src/main/java/com/yahoo/documentapi/DocumentIdResponse.java b/documentapi/src/main/java/com/yahoo/documentapi/DocumentIdResponse.java new file mode 100644 index 00000000000..4d79f0973cb --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/DocumentIdResponse.java @@ -0,0 +1,81 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi; + +import com.yahoo.document.DocumentId; + +/** + * The asynchronous response to a document remove operation. + * This is a <i>value object</i>. + * + * @author <a href="mailto:einarmr@yahoo-inc.com">Einar M R Rosenvinge</a> + */ +public class DocumentIdResponse extends Response { + + /** The document id of this response, if any */ + private DocumentId documentId = null; + + /** Creates a successful response */ + public DocumentIdResponse(long requestId) { + super(requestId); + } + + /** + * Creates a successful response containing a document id + * + * @param documentId the DocumentId to encapsulate in the Response + */ + public DocumentIdResponse(long requestId, DocumentId documentId) { + super(requestId); + this.documentId = documentId; + } + + /** + * Creates a response containing a textual message + * + * @param textMessage the message to encapsulate in the Response + * @param success true if the response represents a successful call + */ + public DocumentIdResponse(long requestId, String textMessage, boolean success) { + super(requestId, textMessage, success); + } + + /** + * Creates a response containing a textual message and/or a document id + * + * @param documentId the DocumentId to encapsulate in the Response + * @param textMessage the message to encapsulate in the Response + * @param success true if the response represents a successful call + */ + public DocumentIdResponse(long requestId, DocumentId documentId, String textMessage, boolean success) { + super(requestId, textMessage, success); + this.documentId = documentId; + } + + + /** + * Returns the document id of this response, or null if there is none + * + * @return the DocumentId, or null + */ + public DocumentId getDocumentId() { return documentId; } + + public int hashCode() { + return super.hashCode() + (documentId == null ? 0 : documentId.hashCode()); + } + + public boolean equals(Object o) { + if (!(o instanceof DocumentIdResponse)) { + return false; + } + + DocumentIdResponse docResp = (DocumentIdResponse) o; + + return super.equals(docResp) && ((documentId == null && docResp.documentId == null) || + (documentId != null && docResp.documentId != null && documentId.equals(docResp.documentId))); + } + + public String toString() { + return "DocumentId" + super.toString() + (documentId == null ? "" : " " + documentId); + } + +} diff --git a/documentapi/src/main/java/com/yahoo/documentapi/DocumentListVisitorResponse.java b/documentapi/src/main/java/com/yahoo/documentapi/DocumentListVisitorResponse.java new file mode 100644 index 00000000000..945538244c4 --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/DocumentListVisitorResponse.java @@ -0,0 +1,28 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi; + +import com.yahoo.vdslib.DocumentList; + +/** + * Visitor response containing a document list. All visitor responses have ack + * tokens that must be acked. + * + * @author <a href="mailto:humbe@yahoo-inc.com">Håkon Humberset</a> + */ +public class DocumentListVisitorResponse extends VisitorResponse { + private DocumentList documents; + + /** + * Creates visitor response containing a document list and an ack token. + * + * @param docs the document list + * @param ack the ack token + */ + public DocumentListVisitorResponse(DocumentList docs, AckToken ack) { + super(ack); + documents = docs; + } + + /** @return the document list */ + public DocumentList getDocumentList() { return documents; } +} diff --git a/documentapi/src/main/java/com/yahoo/documentapi/DocumentResponse.java b/documentapi/src/main/java/com/yahoo/documentapi/DocumentResponse.java new file mode 100644 index 00000000000..5a32b19c342 --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/DocumentResponse.java @@ -0,0 +1,82 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi; + +import com.yahoo.document.Document; +import com.yahoo.component.Version; + +/** + * The asynchronous response to a document put or get operation. + * This is a <i>value object</i>. + * + * @author <a href="mailto:einarmr@yahoo-inc.com">Einar M R Rosenvinge</a> + */ +public class DocumentResponse extends Response { + + /** The document of this response, if any */ + private Document document = null; + + /** Creates a successful response */ + public DocumentResponse(long requestId) { + super(requestId); + } + + /** + * Creates a successful response containing a document + * + * @param document the Document to encapsulate in the Response + */ + public DocumentResponse(long requestId, Document document) { + super(requestId); + this.document = document; + } + + /** + * Creates a response containing a textual message + * + * @param textMessage the message to encapsulate in the Response + * @param success true if the response represents a successful call + */ + public DocumentResponse(long requestId, String textMessage, boolean success) { + super(requestId, textMessage, success); + } + + /** + * Creates a response containing a textual message and/or a document + * + * @param document the Document to encapsulate in the Response + * @param textMessage the message to encapsulate in the Response + * @param success true if the response represents a successful call + */ + public DocumentResponse(long requestId, Document document, String textMessage, boolean success) { + super(requestId, textMessage, success); + this.document = document; + } + + + /** + * Returns the document of this response, or null if there is none + * + * @return the Document, or null + */ + public Document getDocument() { return document; } + + public int hashCode() { + return super.hashCode() + (document == null ? 0 : document.hashCode()); + } + + public boolean equals(Object o) { + if (!(o instanceof DocumentResponse)) { + return false; + } + + DocumentResponse docResp = (DocumentResponse) o; + + return super.equals(docResp) && ((document == null && docResp.document == null) || + (document != null && docResp.document != null && document.equals(docResp.document))); + } + + public String toString() { + return "Document" + super.toString() + (document == null ? "" : " " + document); + } + +} diff --git a/documentapi/src/main/java/com/yahoo/documentapi/DocumentUpdateResponse.java b/documentapi/src/main/java/com/yahoo/documentapi/DocumentUpdateResponse.java new file mode 100644 index 00000000000..44044a41bc3 --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/DocumentUpdateResponse.java @@ -0,0 +1,82 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi; + +import com.yahoo.document.DocumentUpdate; + +/** + * The asynchronous response to a document update operation. + * This is a <i>value object</i>. + * + * @author <a href="mailto:einarmr@yahoo-inc.com">Einar M R Rosenvinge</a> + */ +public class DocumentUpdateResponse extends Response { + + /** The document update of this response, if any */ + private DocumentUpdate documentUpdate = null; + + /** Creates a successful response */ + public DocumentUpdateResponse(long requestId) { + super(requestId); + } + + /** + * Creates a successful response containing a document update + * + * @param documentUpdate the DocumentUpdate to encapsulate in the Response + */ + public DocumentUpdateResponse(long requestId, DocumentUpdate documentUpdate) { + super(requestId); + this.documentUpdate = documentUpdate; + } + + /** + * Creates a response containing a textual message + * + * @param textMessage the message to encapsulate in the Response + * @param success true if the response represents a successful call + */ + public DocumentUpdateResponse(long requestId, String textMessage, boolean success) { + super(requestId, textMessage, success); + } + + /** + * Creates a response containing a textual message and/or a document update + * + * @param documentUpdate the DocumentUpdate to encapsulate in the Response + * @param textMessage the message to encapsulate in the Response + * @param success true if the response represents a successful call + */ + public DocumentUpdateResponse(long requestId, DocumentUpdate documentUpdate, String textMessage, boolean success) { + super(requestId, textMessage, success); + this.documentUpdate = documentUpdate; + } + + + /** + * Returns the document update of this response or null if there is none + * + * @return the DocumentUpdate, or null + */ + public DocumentUpdate getDocumentUpdate() { return documentUpdate; } + + public int hashCode() { + return super.hashCode() + (documentUpdate == null ? 0 : documentUpdate.hashCode()); + } + + public boolean equals(Object o) { + if (!(o instanceof DocumentUpdateResponse)) { + return false; + } + + DocumentUpdateResponse docResp = (DocumentUpdateResponse) o; + + return super.equals(docResp) && ((documentUpdate == null && docResp.documentUpdate == null) || ( + documentUpdate != null && docResp.documentUpdate != null && + documentUpdate.equals(docResp.documentUpdate))); + } + + public String toString() { + return "Update" + super.toString() + (documentUpdate == null ? "" : " " + documentUpdate); + } + +} diff --git a/documentapi/src/main/java/com/yahoo/documentapi/DocumentVisitor.java b/documentapi/src/main/java/com/yahoo/documentapi/DocumentVisitor.java new file mode 100644 index 00000000000..fd0e9725866 --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/DocumentVisitor.java @@ -0,0 +1,23 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi; + +/** + * Visitor that simply returns documents found in storage. + * + * @author <a href="mailto:humbe@yahoo-inc.com">Håkon Humberset</a> + */ +public class DocumentVisitor extends VisitorParameters { + + /** + * Create a document visitor. + * + * @param documentSelection The document selection criteria. + */ + public DocumentVisitor(String documentSelection) { + super(documentSelection); + } + + // Inherited docs from VisitorParameters + public String getVisitorLibrary() { return "DumpVisitor"; } + +} diff --git a/documentapi/src/main/java/com/yahoo/documentapi/DumpVisitorDataHandler.java b/documentapi/src/main/java/com/yahoo/documentapi/DumpVisitorDataHandler.java new file mode 100644 index 00000000000..8d09feec707 --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/DumpVisitorDataHandler.java @@ -0,0 +1,59 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi; + +import com.yahoo.document.Document; +import com.yahoo.document.DocumentId; +import com.yahoo.documentapi.messagebus.protocol.PutDocumentMessage; +import com.yahoo.documentapi.messagebus.protocol.RemoveDocumentMessage; +import com.yahoo.messagebus.Message; + +/** + * <p>Implementation of VisitorDataHandler which invokes onDocument() for each + * received document and onRemove() for each document id that was returned as + * part of a remove entry. The latter only applies if the visitor was run with + * visitRemoves enabled.</p> + * + * <p>NOTE: onDocument and onRemove may be called in a re-entrant manner, as + * these run on top of a thread pool. Any mutation of shared state must be + * appropriately synchronized.</p> + */ +public abstract class DumpVisitorDataHandler extends VisitorDataHandler { + + public DumpVisitorDataHandler() { + } + + @Override + public void onMessage(Message m, AckToken token) { + if (m instanceof PutDocumentMessage) { + PutDocumentMessage pm = (PutDocumentMessage)m; + + onDocument(pm.getDocumentPut().getDocument(), pm.getTimestamp()); + } else if (m instanceof RemoveDocumentMessage) { + RemoveDocumentMessage rm = (RemoveDocumentMessage)m; + onRemove(rm.getDocumentId()); + } else { + throw new UnsupportedOperationException("Received unsupported message " + m.toString() + " to dump visitor data handler. This handler only accepts Put and Remove"); + } + ack(token); + } + + /** + * Called when a document is received. + * + * May be called from multiple threads concurrently. + * + * @param doc The document found + * @param timeStamp The time when the document was stored. + */ + public abstract void onDocument(Document doc, long timeStamp); + + /** + * Called when a remove is received. + * + * May be called from multiple threads concurrently. + * + * @param id The document id that was removed. + */ + public abstract void onRemove(DocumentId id); + +} diff --git a/documentapi/src/main/java/com/yahoo/documentapi/EmptyBucketsVisitorResponse.java b/documentapi/src/main/java/com/yahoo/documentapi/EmptyBucketsVisitorResponse.java new file mode 100644 index 00000000000..68431f8ecaa --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/EmptyBucketsVisitorResponse.java @@ -0,0 +1,27 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi; + +import com.yahoo.document.BucketId; + +import java.util.List; + +/** + * Response containing list of empty buckets. + * + * @author <a href="mailto:einarmr@yahoo-inc.com">Einar M R Rosenvinge</a> + */ +public class EmptyBucketsVisitorResponse extends VisitorResponse { + private List<BucketId> bucketIds; + /** + * Creates visitor response containing an ack token. + * + * @param bucketIds the empty buckets + * @param token the ack token + */ + public EmptyBucketsVisitorResponse(List<BucketId> bucketIds, AckToken token) { + super(token); + this.bucketIds = bucketIds; + } + + public List<BucketId> getBucketIds() { return bucketIds; } +} diff --git a/documentapi/src/main/java/com/yahoo/documentapi/Parameters.java b/documentapi/src/main/java/com/yahoo/documentapi/Parameters.java new file mode 100644 index 00000000000..fdf57cecfc8 --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/Parameters.java @@ -0,0 +1,13 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi; + +/** + * Superclass of the classes which contains the parameters for creating or opening a session. This is currently empty, + * but keeping this parameter hierarchy in place means that we can later add parameters with default values that all + * clients will be able to use with no code changes. + * + * @author <a href="mailto:bratseth@yahoo-inc.com">Jon Bratseth</a> + */ +public class Parameters { + // empty +} diff --git a/documentapi/src/main/java/com/yahoo/documentapi/ProgressToken.java b/documentapi/src/main/java/com/yahoo/documentapi/ProgressToken.java new file mode 100644 index 00000000000..ce200c89751 --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/ProgressToken.java @@ -0,0 +1,816 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi; + +import java.util.Collections; +import java.util.Map; +import java.util.TreeMap; +import java.util.logging.Logger; + +import com.yahoo.document.*; +import com.yahoo.document.serialization.*; +import com.yahoo.io.GrowableByteBuffer; +import com.yahoo.log.LogLevel; +import com.yahoo.vespa.objects.Serializer; + +/** + * Token to use to keep track of progress for visiting. Can be used to resume + * visiting if visiting has been aborted for any reason. + * + * @author <a href="mailto:thomasg@yahoo-inc.com">Thomas Gundersen</a> + * @author <a href="mailto:vekterli@yahoo-inc.com">Tor Brede Vekterli</a> + */ +public class ProgressToken { + + private static final Logger log = Logger.getLogger(ProgressToken.class.getName()); + /** + * Any bucket kept track of by a <code>ProgressToken</code> instance may + * be in one of two states: pending or active. <em>Pending</em> means that + * a bucket may be returned by a VisitorIterator, i.e. it is ready for + * visiting, while <em>active</em> means that a bucket is currently being + * visited and may thus not be returned from an iterator. + * + * Getting a pending bucket via the iterator sets its state to active and + * updating an active bucket sets its state back to pending again. + */ + public enum BucketState { + BUCKET_PENDING, + BUCKET_ACTIVE + } + + public static final BucketId NULL_BUCKET = new BucketId(); + public static final BucketId FINISHED_BUCKET = new BucketId(Integer.MAX_VALUE); + + /** + * When a bucket has its state kept by the progress token, we need to + * discern between active buckets (i.e. those that have been returned by + * {@link com.yahoo.documentapi.VisitorIterator#getNext()} but have not + * yet been update()'d) and pending buckets (i.e. those that have been + * update()'d and may be returned by getNext() at some point) + */ + public static class BucketEntry { + private BucketId progress; + private BucketState state; + + private BucketEntry(BucketId progress, BucketState state) { + this.progress = progress; + this.state = state; + } + + public BucketId getProgress() { + return progress; + } + + public void setProgress(BucketId progress) { + this.progress = progress; + } + + public BucketState getState() { + return state; + } + + public void setState(BucketState state) { + this.state = state; + } + } + + /** + * For consistent bucket key ordering, we need to ensure that reverse bucket + * IDs that have their MSB set actually are compared as being greater than + * those that don't. This is yet another issue caused by Java's lack of + * unsigned integers. + */ + public static class BucketKeyWrapper implements Comparable<BucketKeyWrapper> + { + private long key; + + public BucketKeyWrapper(long key) { + this.key = key; + } + + public int compareTo(BucketKeyWrapper other) { + if ((key & 0x8000000000000000L) != (other.key & 0x8000000000000000L)) { + // MSBs differ + return ((key >>> 63) > (other.key >>> 63)) ? 1 : -1; + } + // Mask off MSBs since we've already checked them, and with MSB != 1 + // we know the ordering will be consistent + if ((key & 0x7FFFFFFFFFFFFFFFL) < (other.key & 0x7FFFFFFFFFFFFFFFL)) { + return -1; + } else if ((key & 0x7FFFFFFFFFFFFFFFL) > (other.key & 0x7FFFFFFFFFFFFFFFL)) { + return 1; + } + return 0; + } + + public long getKey() { + return key; + } + + public BucketId toBucketId() { + return new BucketId(keyToBucketId(key)); + } + + @Override + public String toString() { + return Long.toHexString(key); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || !(o instanceof BucketKeyWrapper)) return false; + return key == ((BucketKeyWrapper)o).key; + } + + @Override + public int hashCode() { + return (int) (key ^ (key >>> 32)); + } + } + + /** + * By default, a ProgressToken's distribution bit count is set to the VDS + * standard value of 16, but it may be changed via the iterator using it + * or by a bucket source when importing an existing progress + */ + private int distributionBits = 16; + + private TreeMap<BucketKeyWrapper, BucketEntry> buckets = new TreeMap<BucketKeyWrapper, BucketEntry>(); + private long activeBucketCount = 0; + private long pendingBucketCount = 0; + private long finishedBucketCount = 0; + private long totalBucketCount = 0; + private TreeMap<BucketId, BucketId> failedBuckets = new TreeMap<BucketId, BucketId>(); + private String firstErrorMsg; + + /** + * The bucket cursor (i.e. current position in the bucket space) is used + * by the range source + */ + private long bucketCursor = 0; + + /** + * Set by the VisitorIterator during a distribution bit change when + * the token contains active/pending buckets with different used-bits + */ + private boolean inconsistentState = false; + + /** + * Creates a progress token. + */ + public ProgressToken() { + } + + public ProgressToken(int distributionBits) { + this.distributionBits = distributionBits; + } + + public ProgressToken(String serialized) { + String[] lines = serialized.split("\\n"); + if (lines.length < 5) { + throw new IllegalArgumentException("Progress file is malformed or a deprecated version"); + } + + // 1st token is simple header text. Just check that it starts with + // a known value. To be 5.0 backwards compatible, we do not check + // the rest of the line. + final String header = lines[0]; + if (!header.startsWith("VDS bucket progress file")) { + throw new IllegalArgumentException("File does not appear to be a " + + "valid VDS progress file; expected first line to start with " + + "'VDS bucket progress file'"); + } + // 2nd token contains the distribution bit count the progress file was + // saved with + distributionBits = Integer.parseInt(lines[1]); + bucketCursor = Long.parseLong(lines[2]); + finishedBucketCount = Long.parseLong(lines[3]); + totalBucketCount = Long.parseLong(lines[4]); + + if (totalBucketCount == finishedBucketCount) { + return; // We're done here + } + + // The rest of the tokens are super:sub bucket progress pairs + for (int i = 5; i < lines.length; ++i) { + String[] buckets = lines[i].split(":"); + if (buckets.length != 2) { + throw new IllegalArgumentException("Bucket progress file contained malformed line"); + } + // Due to Java's fantastically broken handling of unsigned integer + // conversion, the following workaround (i.e. hack) is used for now + // (it was also used in the past for presumably the same reason). + BucketId superId = new BucketId("BucketId(0x" + buckets[0] + ")"); + BucketId subId; + if ("0".equals(buckets[1])) { + subId = new BucketId(); + } else { + subId = new BucketId("BucketId(0x" + buckets[1] + ")"); + } + addBucket(superId, subId, BucketState.BUCKET_PENDING); + } + } + + public ProgressToken(byte[] serialized) { + DocumentDeserializer in = DocumentDeserializerFactory.create42(null, GrowableByteBuffer.wrap(serialized)); + distributionBits = in.getInt(null); + bucketCursor = in.getLong(null); + finishedBucketCount = in.getLong(null); + totalBucketCount = in.getLong(null); + + int progressCount = in.getInt(null); + for (int i = 0; i < progressCount; ++i) { + long key = in.getLong(null); + long value = in.getLong(null); + addBucket(new BucketId(key), new BucketId(value), BucketState.BUCKET_PENDING); + } + } + + public byte[] serialize() { + DocumentSerializer out = DocumentSerializerFactory.create42(new GrowableByteBuffer()); + out.putInt(null, distributionBits); + out.putLong(null, bucketCursor); + out.putLong(null, finishedBucketCount); + out.putLong(null, totalBucketCount); + + out.putInt(null, buckets.size()); + + // Append individual bucket progress + for (Map.Entry<BucketKeyWrapper, ProgressToken.BucketEntry> entry : buckets.entrySet()) { + out.putLong(null, keyToBucketId(entry.getKey().getKey())); + out.putLong(null, entry.getValue().getProgress().getRawId()); + } + + byte[] ret = new byte[out.getBuf().position()]; + out.getBuf().rewind(); + out.getBuf().get(ret); + return ret; + } + + public void addFailedBucket(BucketId superbucket, BucketId progress, String errorMsg) { + BucketId existing = failedBuckets.put(superbucket, progress); + if (existing != null) { + throw new IllegalStateException( + "Attempting to add a superbucket to failed buckets that has already been added: " + + superbucket + ":" + progress); + } + if (firstErrorMsg == null) { + firstErrorMsg = errorMsg; + } + } + + /** + * Get all failed buckets and their progress. Not thread safe. + * @return Unmodifiable map of all failed buckets + */ + public Map<BucketId, BucketId> getFailedBuckets() { + return Collections.unmodifiableMap(failedBuckets); + } + + /** + * Updates internal progress state for <code>bucket</code>, indicating it's currently + * at <code>progress</code>. Assumes that given a completely finished bucket, this + * function will not be called again to further update its progress after + * the finished-update. + * + * @see VisitorIterator#update(com.yahoo.document.BucketId, com.yahoo.document.BucketId) + * + * @param superbucket A valid superbucket ID that exists in <code>buckets</code> + * @param progress The sub-bucket progress that has been reached in the + * superbucket + */ + protected void updateProgress(BucketId superbucket, BucketId progress) { + // There exists a valid case in which the progress bucket may actually contains + // its superbucket from the POV of the storage code, so it has to be handled + // appropriately. + if (!progress.equals(NULL_BUCKET) + && !progress.equals(FINISHED_BUCKET) + && !superbucket.contains(progress) + && !progress.contains(superbucket)) { + if (log.isLoggable(LogLevel.DEBUG)) { + log.log(LogLevel.DEBUG, "updateProgress called with non-contained bucket " + + "pair " + superbucket + ":" + progress + ", but allowing anyway"); + } + } + + BucketKeyWrapper superKey = bucketToKeyWrapper(superbucket); + BucketEntry entry = buckets.get(superKey); + if (entry == null) { + throw new IllegalArgumentException( + "updateProgress with unknown superbucket " + + superbucket + ":" + progress); + } + + // If progress == Integer.MAX_VALUE, we're done. Otherwise, we're not + if (!progress.equals(FINISHED_BUCKET)) { + if (entry.getState() != BucketState.BUCKET_ACTIVE) { + if (log.isLoggable(LogLevel.DEBUG)) { + log.log(LogLevel.DEBUG, "updateProgress called with sub-bucket that was " + + "not marked as active " + superbucket + ":" + progress); + } + } else { + assert(activeBucketCount > 0); + --activeBucketCount; + ++pendingBucketCount; + } + // Mark bucket as pending instead of active, allowing it to be + // reused by the iterator + entry.setState(BucketState.BUCKET_PENDING); + entry.setProgress(progress); + } + else { + // Superbucket is finished, alongside its sub-bucket tree + ++finishedBucketCount; + if (entry.getState() == BucketState.BUCKET_PENDING) { + assert(pendingBucketCount > 0); + --pendingBucketCount; + } else { + assert(activeBucketCount > 0); + --activeBucketCount; + } + buckets.remove(superKey); + } + } + + /** + * <em>For use internally by DocumentAPI code only</em>. Using this method by + * itself will invariably lead to undefined ProgressToken state unless + * care is taken. Leave it to the VisitorIterator. + * + * @param superbucket Superbucket that will be progress-tracked + * @param progress Bucket progress thus far + * @param state Initial bucket state. Only pending buckets may be returned + */ + protected void addBucket(BucketId superbucket, BucketId progress, BucketState state) { + if (progress.equals(FINISHED_BUCKET)) { + if (log.isLoggable(LogLevel.DEBUG)) { + log.log(LogLevel.DEBUG, "Trying to add already finished superbucket " + + superbucket + "; ignoring it"); + } + return; + } + if (log.isLoggable(LogLevel.SPAM)) { + log.log(LogLevel.SPAM, "Adding bucket pair " + superbucket + + ":" + progress + " with state " + state); + } + + BucketEntry entry = new BucketEntry(progress, state); + BucketEntry existing = buckets.put(bucketToKeyWrapper(superbucket), entry); + if (existing != null) { + throw new IllegalStateException( + "Attempting to add a superbucket that has already been added: " + + superbucket + ":" + progress); + } + if (state == BucketState.BUCKET_PENDING) { + ++pendingBucketCount; + } else { + ++activeBucketCount; + } + } + + /** + * Directly generate a bucket Id key for the <code>n</code>th bucket in + * reverse sorted order. + * + * @param n a number in the range [0, 2**<code>distributionBits</code>) + * @param distributionBits Distribution bit count for the generated key + * @return A value where, if you had generated 2**<code>distributionBits</code> + * {@link BucketId}s with incremental numerical IDs and then sorted + * them on their reverse bit-order keys, the returned key would be equal + * to the nth element in the resulting sorted sequence. + */ + public static long makeNthBucketKey(long n, int distributionBits) { + return (n << (64 - distributionBits)) | distributionBits; + } + + public int getDistributionBitCount() { + return distributionBits; + } + + /** + * Set the internal number of distribution bits, which wil be used for writing + * the progress file and calculating correct percent-wise sub-bucket completion. + * + * Note that simply invoking this method on the progress token does not actually + * change any of its bucket structures/counts! <i>This is the bucket source's + * responsibility</i>, since only it knows how such a change will affect the + * progress semantics. + * + * @param distributionBits new distribution bit value + */ + protected void setDistributionBitCount(int distributionBits) { + this.distributionBits = distributionBits; + } + + public long getActiveBucketCount() { + return activeBucketCount; + } + + public long getBucketCursor() { + return bucketCursor; + } + + protected void setBucketCursor(long bucketCursor) { + this.bucketCursor = bucketCursor; + } + + public long getFinishedBucketCount() { + return finishedBucketCount; + } + + /** + * <em>For use by bucket sources and unit tests only!</em> + * + * @param finishedBucketCount Number of buckets the token has finished + */ + protected void setFinishedBucketCount(long finishedBucketCount) { + this.finishedBucketCount = finishedBucketCount; + } + + public long getTotalBucketCount() { + return totalBucketCount; + } + + /** + * <em>For use by bucket sources and unit tests only!</em> + * + * @param totalBucketCount Total number of buckets that the progress token spans + */ + protected void setTotalBucketCount(long totalBucketCount) { + this.totalBucketCount = totalBucketCount; + } + + public long getPendingBucketCount() { + return pendingBucketCount; + } + + public boolean hasPending() { + return pendingBucketCount > 0; + } + + public boolean hasActive() { + return activeBucketCount > 0; + } + + public boolean isFinished() { + return finishedBucketCount == totalBucketCount; + } + + public boolean isEmpty() { + return buckets.isEmpty(); + } + + public String getFirstErrorMsg() { + return firstErrorMsg; + } + + public boolean containsFailedBuckets() { + return !failedBuckets.isEmpty(); + } + + public boolean isInconsistentState() { + return inconsistentState; + } + + public void setInconsistentState(boolean inconsistentState) { + this.inconsistentState = inconsistentState; + } + + /** + * Get internal progress token bucket state map. <em>For internal use only!</em> + * @return Map of superbuckets → sub buckets + */ + protected TreeMap<BucketKeyWrapper, BucketEntry> getBuckets() { + return buckets; + } + + protected void setActiveBucketCount(long activeBucketCount) { + this.activeBucketCount = activeBucketCount; + } + + protected void setPendingBucketCount(long pendingBucketCount) { + this.pendingBucketCount = pendingBucketCount; + } + + /** + * The format of the bucket progress output is as follows: + * <pre> + * VDS bucket progress file (n% completed)\n + * distribution bit count\n + * current bucket cursor\n + * number of finished buckets\n + * total number of buckets\n + * hex-of-superbucket:hex-of-progress\n + * ... repeat above line for each pending bucket ... + * </pre> + * + * Note that unlike earlier versions of ProgressToken, the bucket IDs are + * not prefixed with '0x'. + */ + public synchronized String toString() { + StringBuilder sb = new StringBuilder(); + // Append header + sb.append("VDS bucket progress file ("); + sb.append(percentFinished()); + sb.append("% completed)\n"); + sb.append(distributionBits); + sb.append('\n'); + sb.append(bucketCursor); + sb.append('\n'); + long doneBucketCount = Math.max(0l, finishedBucketCount - failedBuckets.size()); + sb.append(doneBucketCount); + sb.append('\n'); + sb.append(totalBucketCount); + sb.append('\n'); + // Append individual bucket progress + for (Map.Entry<BucketKeyWrapper, ProgressToken.BucketEntry> entry : buckets.entrySet()) { + sb.append(Long.toHexString(keyToBucketId(entry.getKey().getKey()))); + sb.append(':'); + sb.append(Long.toHexString(entry.getValue().getProgress().getRawId())); + sb.append('\n'); + } + for (Map.Entry<BucketId, BucketId> entry : failedBuckets.entrySet()) { + sb.append(Long.toHexString(entry.getKey().getRawId())); + sb.append(':'); + sb.append(Long.toHexString(entry.getValue().getRawId())); + sb.append('\n'); + } + return sb.toString(); + } + + /** + * Calculate an estimate on how far we've managed to iterate over both the + * superbuckets and the sub-buckets. + * + * Runs in <em>O(n+m)</em> time, where <em>n</em> is the number of active buckets + * and <em>m</em> is the number of pending buckets. Both these values should + * be fairly small in practice, however. + * + * Method is synchronized, as legacy code treats this as an atomic read. + * + * @return A value in the range [0, 100] estimating the progress. + */ + public synchronized double percentFinished() { + long superTotal = totalBucketCount; + long superFinished = finishedBucketCount; + + if (superTotal == 0 || superTotal == superFinished) return 100; + + double superDelta = 100.0 / superTotal; + double cumulativeSubProgress = 0; + + // Calculate cumulative for all non-finished buckets. 0 means the + // bucket has yet to see any progress + // There are numerical precision issues here, but this hardly requires + // aerospace engineering result-accuracy + for (Map.Entry<BucketKeyWrapper, ProgressToken.BucketEntry> entry : buckets.entrySet()) { + BucketId superbucket = new BucketId(keyToBucketId(entry.getKey().getKey())); + BucketId progress = entry.getValue().getProgress(); + // Prevent calculation of bucket progress on inconsistent buckets + if (progress.getId() != 0 && superbucket.contains(progress)) { + cumulativeSubProgress += superDelta * progressFraction(superbucket, progress); + } + } + + return (((double)superFinished / (double)superTotal) * 100.0) + + cumulativeSubProgress; + } + + /* + * Based on the following C++ code from document/bucket/bucketid.cpp: + * + * BucketId::Type + * BucketId::bucketIdToKey(Type id) + * { + * Type retVal = reverse(id); + * + * Type usedCountLSB = id >> maxNumBits(); + * retVal >>= CountBits; + * retVal <<= CountBits; + * retVal |= usedCountLSB; + * + * return retVal; + * } + * + * static uint32_t maxNumBits() { return (8 * sizeof(Type) - CountBits);} + */ + // TODO: this should probably be moved to BucketId at some point? + public static long bucketToKey(long id) { + long retVal = Long.reverse(id); + long usedCountLSB = id >>> (64 - BucketId.COUNT_BITS); + retVal >>>= BucketId.COUNT_BITS; + retVal <<= BucketId.COUNT_BITS; + retVal |= usedCountLSB; + + return retVal; + } + + private static BucketKeyWrapper bucketToKeyWrapper(BucketId bucket) { + return new BucketKeyWrapper(bucketToKey(bucket.getId())); + } + /* + * BucketId::Type + * BucketId::keyToBucketId(Type key) + * { + * Type retVal = reverse(key); + * + * Type usedCountMSB = key << maxNumBits(); + * retVal <<= CountBits; + * retVal >>= CountBits; + * retVal |= usedCountMSB; + * + * return retVal; + * } + */ + public static long keyToBucketId(long key) { + long retVal = Long.reverse(key); + long usedCountMSB = key << (64 - BucketId.COUNT_BITS); + retVal <<= BucketId.COUNT_BITS; + retVal >>>= BucketId.COUNT_BITS; + retVal |= usedCountMSB; + + return retVal; + } + + /** + * @param superbucket The superbucket of which <code>progress</code> is + * a sub-bucket + * @param progress The sub-bucket for which a fractional progress should + * be calculated + * @return a value in [0, 1] specifying how far the (sub-bucket) has + * reached in its superbucket. This is calculated by looking at the + * bucket's split factor. + */ + public synchronized double progressFraction(BucketId superbucket, BucketId progress) { + long revBits = bucketToKey(progress.getId()); + int superUsed = superbucket.getUsedBits(); + int progressUsed = progress.getUsedBits(); + + if (progressUsed == 0 || progressUsed < superUsed) { + return 0; + } + + int splitCount = progressUsed - superUsed; + + if (splitCount == 0) return 1; // Superbucket or inconsistent used-bits + + // Extract reversed split-bits + revBits <<= superUsed; + revBits >>>= 64 - splitCount; + + return (double)(revBits + 1) / (double)(1L << splitCount); + } + + /** + * Checks whether or not a given bucket is certain to be finished. Only + * looks at the super-bucket part of the given bucket ID, so it's possible + * that the bucket has in fact finished on a sub-bucket progress level. + * This does not affect the correctness of the result, however. + * + * During a distribution bit change, the token's buckets may be inconsistent. + * In this scenario, false is always returned since we can't tell for + * sure if the bucket is still active until the buckets have been made + * consistent. + * + * <strong>FIXME: verify correctness with regards to orderdoc et al. + * Don't make this method public until this has been done!</strong> + * + * @param bucket Bucket to check whether or not is finished. + * @return <code>true</code> if <code>bucket</code>'s super-bucket is + * finished, <code>false</code> otherwise. + */ + protected synchronized boolean isBucketFinished(BucketId bucket) { + if (inconsistentState) { + return false; + } + // Token only knows of super-buckets, not sub buckets + BucketId superbucket = new BucketId(distributionBits, bucket.getId()); + // Bucket is done if the current cursor location implies a visitor for + // the associated superbucket has already been sent off at some point + // and there is no pending visitor for the superbucket. The cursor is + // used to directly generate bucket keys, so we can compare against it + // directly. + // Example: given db=3 and cursor=2, the buckets 000 and 100 will have + // been returned by the iterator. By reversing the id and "right- + // aligning" it, we get the cursor location that would be required to + // generate it. + // We also return false if we're inconsistent, since the active/pending + // check is done on exact key values, requiring a uniform distribution + // bit value. + long reverseId = Long.reverse(superbucket.getId()) + >>> (64 - distributionBits); // No count bits + + if (reverseId >= bucketCursor) { + return false; + } + // Bucket has been generated, and it must have been finished if it's + // not listed as active/pending since we always remove finished buckets + BucketEntry entry = buckets.get(bucketToKeyWrapper(superbucket)); + if (entry == null) { + return true; + } + // If key of bucket progress > key of bucket id, we've finished it + long bucketKey = bucketToKey(bucket.getId()); + long progressKey = bucketToKey(entry.getProgress().getId()); + // TODO: verify correctness for all bucket orderings! + return progressKey > bucketKey; + } + + /** + * + * @param bucket BucketId to be split into two buckets. Bucket's used-bits + * do not need to match the ProgressToken's current distribution bit count, + * as it is assumed the client knows what it's doing and will bring the + * token into a consistent state eventually. + */ + protected void splitPendingBucket(BucketId bucket) { + BucketKeyWrapper bucketKey = bucketToKeyWrapper(bucket); + BucketEntry entry = buckets.get(bucketKey); + if (entry == null) { + throw new IllegalArgumentException( + "Attempting to split unknown bucket: " + bucket); + } + if (entry.getState() != BucketState.BUCKET_PENDING) { + throw new IllegalArgumentException( + "Attempting to split non-pending bucket: " + bucket); + } + + int splitDistBits = bucket.getUsedBits() + 1; + // Original bucket is replaced by two split children + BucketId splitLeft = new BucketId(splitDistBits, bucket.getId()); + // Right split sibling becomes logically at location original_bucket*2 in the + // bucket space due to the key ordering and setting the MSB of the split + BucketId splitRight = new BucketId(splitDistBits, bucket.getId() + | (1L << bucket.getUsedBits())); + + addBucket(splitLeft, entry.getProgress(), BucketState.BUCKET_PENDING); + addBucket(splitRight, entry.getProgress(), BucketState.BUCKET_PENDING); + + // Remove old bucket + buckets.remove(bucketKey); + --pendingBucketCount; + } + + protected void mergePendingBucket(BucketId bucket) { + BucketKeyWrapper bucketKey = bucketToKeyWrapper(bucket); + BucketEntry entry = buckets.get(bucketKey); + if (entry == null) { + throw new IllegalArgumentException( + "Attempting to join unknown bucket: " + bucket); + } + if (entry.getState() != BucketState.BUCKET_PENDING) { + throw new IllegalArgumentException( + "Attempting to join non-pending bucket: " + bucket); + } + + int usedBits = bucket.getUsedBits(); + // If MSB is 0, we should look for the bucket's right sibling. If not, + // we know that there's no left sibling, as it should otherwise have been + // merged already by the caller, due to it being ordered before the + // right sibling in the pending mapping + if ((bucket.getId() & (1L << (usedBits - 1))) == 0) { + BucketId rightCheck = new BucketId(usedBits, bucket.getId() | (1L << (usedBits - 1))); + BucketEntry rightSibling = buckets.get(bucketToKeyWrapper(rightCheck)); + // Must not merge if sibling isn't pending + if (rightSibling != null) { + assert(rightSibling.getState() == BucketState.BUCKET_PENDING); + if (log.isLoggable(LogLevel.SPAM)) { + log.log(LogLevel.SPAM, "Merging " + bucket + " with rhs " + rightCheck); + } + // If right sibling has progress, it will unfortunately have to + // be discarded + if (rightSibling.getProgress().getUsedBits() != 0 + && log.isLoggable(LogLevel.DEBUG)) { + log.log(LogLevel.DEBUG, "Bucket progress for " + rightCheck + + " will be lost due to merging; potential for duplicates in result-set"); + } + buckets.remove(bucketToKeyWrapper(rightCheck)); + --pendingBucketCount; + } + } else { + BucketId leftSanityCheck = new BucketId(usedBits, bucket.getId() & ~(1L << (usedBits - 1))); + BucketEntry leftSibling = buckets.get(bucketToKeyWrapper(leftSanityCheck)); + assert(leftSibling == null) : "bucket merge sanity checking failed"; + } + + BucketId newMerged = new BucketId(usedBits - 1, bucket.getId()); + addBucket(newMerged, entry.getProgress(), BucketState.BUCKET_PENDING); + // Remove original bucket, leaving only the merged bucket + buckets.remove(bucketKey); + --pendingBucketCount; + assert(pendingBucketCount > 0); + } + + protected void setAllBucketsToState(BucketState state) { + for (Map.Entry<BucketKeyWrapper, ProgressToken.BucketEntry> entry + : buckets.entrySet()) { + entry.getValue().setState(state); + } + } + + protected void clearAllBuckets() { + buckets.clear(); + pendingBucketCount = 0; + activeBucketCount = 0; + } +} diff --git a/documentapi/src/main/java/com/yahoo/documentapi/RemoveResponse.java b/documentapi/src/main/java/com/yahoo/documentapi/RemoveResponse.java new file mode 100644 index 00000000000..f712240e7f9 --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/RemoveResponse.java @@ -0,0 +1,47 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi; + +/** + * This response is provided for successful document remove operations. Use the + * wasFound() method to check whether or not the document was actually found. + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class RemoveResponse extends Response { + + private final boolean wasFound; + + public RemoveResponse(long requestId, boolean wasFound) { + super(requestId); + this.wasFound = wasFound; + } + + public boolean wasFound() { + return wasFound; + } + + @Override + public int hashCode() { + return super.hashCode() + Boolean.valueOf(wasFound).hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof RemoveResponse)) { + return false; + } + if (!super.equals(obj)) { + return false; + } + RemoveResponse rhs = (RemoveResponse)obj; + if (wasFound != rhs.wasFound) { + return false; + } + return true; + } + + @Override + public String toString() { + return "Remove" + super.toString() + " " + wasFound; + } +} diff --git a/documentapi/src/main/java/com/yahoo/documentapi/Response.java b/documentapi/src/main/java/com/yahoo/documentapi/Response.java new file mode 100644 index 00000000000..5a079ec8580 --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/Response.java @@ -0,0 +1,81 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi; + +/** + * <p>An asynchronous response from the document api. + * Subclasses of this provide additional response information for particular operations.</p> + * + * <p>This is a <i>value object</i>.</p> + * + * @author <a href="mailto:bratseth@yahoo-inc.com">Jon Bratseth</a> + */ +public class Response { + + private long requestId; + private String textMessage = null; + private boolean success = true; + + /** Creates a successful response containing no information */ + public Response(long requestId) { + this(requestId, null, true); + } + + /** + * Creates a successful response containing a textual message + * + * @param textMessage the message to encapsulate in the Response + */ + public Response(long requestId, String textMessage) { + this(requestId, textMessage, true); + } + + /** + * Creates a response containing a textual message + * + * @param textMessage the message to encapsulate in the Response + * @param success true if the response represents a successful call + */ + public Response(long requestId, String textMessage, boolean success) { + this.requestId = requestId; + this.textMessage = textMessage; + this.success = success; + } + + /** + * Returns the text message of this response or null if there is none + * + * @return the message, or null + */ + public String getTextMessage() { return textMessage; } + + /** + * Returns whether this response encodes a success or a failure + * + * @return true if success + */ + public boolean isSuccess() { return success; } + + public long getRequestId() { return requestId; } + + public int hashCode() { + return (new Long(requestId).hashCode()) + (textMessage == null ? 0 : textMessage.hashCode()) + + (success ? 1 : 0); + } + + public boolean equals(Object o) { + if (!(o instanceof Response)) { + return false; + } + Response other = (Response) o; + + return requestId == other.requestId && success == other.success && ( + textMessage == null && other.textMessage == null || + textMessage != null && other.textMessage != null && textMessage.equals(other.textMessage)); + } + + public String toString() { + return "Response " + requestId + (textMessage == null ? "" : textMessage) + + (success ? " SUCCESSFUL" : " UNSUCCESSFUL"); + } + +} diff --git a/documentapi/src/main/java/com/yahoo/documentapi/ResponseHandler.java b/documentapi/src/main/java/com/yahoo/documentapi/ResponseHandler.java new file mode 100644 index 00000000000..05d6973e4b0 --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/ResponseHandler.java @@ -0,0 +1,16 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public interface ResponseHandler { + + /** + * This method is called once for each document api operation invoked on a {@link AsyncSession}. There is no + * guarantee as to which thread calls this, so any implementation must be thread-safe. + * + * @param response The response to process. + */ + public void handleResponse(Response response); +} diff --git a/documentapi/src/main/java/com/yahoo/documentapi/Result.java b/documentapi/src/main/java/com/yahoo/documentapi/Result.java new file mode 100644 index 00000000000..e5982d297f7 --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/Result.java @@ -0,0 +1,85 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi; + +/** + * The <i>synchronous</i> result of submitting an asynchronous operation. + * A result is either a success or not. If it is not a success, it will contain an explanation of why. + * Document repositories may return subclasses which contain more information. + * + * @author <a href="mailto:bratseth@yahoo-inc.com">Jon Bratseth</a> + */ +public class Result { + + /** Null if this is a success, set to the error occurring if this is a failure */ + private Error error = null; + + /** The id of this operation */ + private long requestId; + + private ResultType type = ResultType.SUCCESS; + + /** + * Creates a successful result + * + * @param requestId the ID of the request + */ + public Result(long requestId) { + this.requestId = requestId; + } + + /** + * Creates a unsuccessful result + * + * @param type the type of failure + * @param error the error to encapsulate in this Result + * @see com.yahoo.documentapi.Result.ResultType + */ + public Result(ResultType type, Error error) { + this.type = type; + this.error = error; + } + + /** + * Returns whether this operation is a success. + * If it is a success, the operation is accepted and one or more responses are guaranteed + * to arrive within this sessions timeout limit. + * If this is not a success, this operation has no further consequences. + * + * @return true if success + */ + public boolean isSuccess() { return type == ResultType.SUCCESS; } + + /** + * Returns the error causes by this. If this was not a success, this method always returns an error + * If this was a success, this method returns null. + * + * @return the Error, or null + */ + public Error getError() { return error; } + + /** + * Returns the id of this operation. The asynchronous response to this operation + * will contain the same id to allow clients who desire to, to match operations to responses. + * + * @return the if of this operation + */ + public long getRequestId() { return requestId; } + + /** + * Returns the type of result. + * + * @return the type of result, typically if this is an error or a success, and what kind of error + * @see com.yahoo.documentapi.Result.ResultType + */ + public ResultType getType() { return type; } + + /** The types that a Result can have. */ + public enum ResultType { + /** The request was successful, no error information is attached. */ + SUCCESS, + /** The request failed, but may be successful if retried at a later time. */ + TRANSIENT_ERROR, + /** The request failed, and retrying is pointless. */ + FATAL_ERROR + } +} diff --git a/documentapi/src/main/java/com/yahoo/documentapi/Session.java b/documentapi/src/main/java/com/yahoo/documentapi/Session.java new file mode 100644 index 00000000000..d0fef420f1d --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/Session.java @@ -0,0 +1,39 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi; + +/** + * Superclass of all document api sessions. A session provides a handle through + * which an application can work with a document repository. There are various + * session subclasses for various types of interaction with the repository. + * <p> + * Each session can be used by multiple client application threads, i.e they are + * multithread safe. + * + * @author bratseth + */ +public interface Session { + + /** + * Returns the next response of this session. This method returns immediately. + * + * @return the next response, or null if no response is ready at this time + */ + public Response getNext(); + + /** + * Returns the next response of this session. This will block until a response is ready + * or until the given timeout is reached + * + * @param timeoutMilliseconds the max time to wait for a response. + * @return the next response, or null if no response becomes ready before the timeout expires + * @throws InterruptedException if this thread is interrupted while waiting + */ + public Response getNext(int timeoutMilliseconds) throws InterruptedException; + + /** + * Destroys this session and frees up any resources it has held. Making further calls on a destroyed + * session causes a runtime exception. + */ + public void destroy(); + +} diff --git a/documentapi/src/main/java/com/yahoo/documentapi/SimpleVisitorDocumentQueue.java b/documentapi/src/main/java/com/yahoo/documentapi/SimpleVisitorDocumentQueue.java new file mode 100644 index 00000000000..3930bd1b7ec --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/SimpleVisitorDocumentQueue.java @@ -0,0 +1,41 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi; + +import com.yahoo.document.Document; +import com.yahoo.document.DocumentId; +import com.yahoo.vdslib.DocumentList; + +import java.util.LinkedList; +import java.util.List; + +/** + * A simple document queue that queues up all results and automatically acks + * them. + * <p> + * Retrieving the list is not thread safe, so wait until visitor is done. This + * is a simple class merely meant for testing. + * + * @author <a href="mailto:humbe@yahoo-inc.com">Håkon Humberset</a> + */ +public class SimpleVisitorDocumentQueue extends DumpVisitorDataHandler { + private final List<Document> documents = new LinkedList<Document>(); + + // Inherit doc from VisitorDataHandler + public void reset() { + super.reset(); + documents.clear(); + } + + @Override + public void onDocument(Document doc, long timestamp) { + documents.add(doc); + } + + public void onRemove(DocumentId docId) {} + + /** @return a list of all documents retrieved so far */ + public List<Document> getDocuments() { + return documents; + } + +} diff --git a/documentapi/src/main/java/com/yahoo/documentapi/SubscriptionParameters.java b/documentapi/src/main/java/com/yahoo/documentapi/SubscriptionParameters.java new file mode 100644 index 00000000000..abd24099ff9 --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/SubscriptionParameters.java @@ -0,0 +1,10 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi; + +/** + * Parameters for creating or opening a visitor session + * + * @author <a href="mailto:bratseth@yahoo-inc.com">Jon Bratseth</a> + */ +public class SubscriptionParameters extends Parameters { +} diff --git a/documentapi/src/main/java/com/yahoo/documentapi/SubscriptionSession.java b/documentapi/src/main/java/com/yahoo/documentapi/SubscriptionSession.java new file mode 100644 index 00000000000..588a5e6f118 --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/SubscriptionSession.java @@ -0,0 +1,19 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi; + +/** + * This class provides document <i>subscription</i> - accessing document changes to a + * document repository. + * + * @author <a href="mailto:bratseth@yahoo-inc.com">Jon Bratseth</a> + */ +public interface SubscriptionSession extends Session { + + /** + * Closes this subscription session without closing the subscription + * registered on the document repository. + * The same subscription can be accessed later by another subscription session. + */ + public void close(); + +} diff --git a/documentapi/src/main/java/com/yahoo/documentapi/SyncParameters.java b/documentapi/src/main/java/com/yahoo/documentapi/SyncParameters.java new file mode 100755 index 00000000000..24b68613208 --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/SyncParameters.java @@ -0,0 +1,11 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi;
+
+/**
+ * Parameters for creating a synchronous session
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class SyncParameters extends Parameters {
+ // empty
+}
diff --git a/documentapi/src/main/java/com/yahoo/documentapi/SyncSession.java b/documentapi/src/main/java/com/yahoo/documentapi/SyncSession.java new file mode 100755 index 00000000000..f864898fb5b --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/SyncSession.java @@ -0,0 +1,101 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi;
+
+import com.yahoo.document.Document;
+import com.yahoo.document.DocumentId;
+import com.yahoo.document.DocumentPut;
+import com.yahoo.document.DocumentRemove;
+import com.yahoo.document.DocumentType;
+import com.yahoo.document.DocumentUpdate;
+import com.yahoo.document.TestAndSetCondition;
+import com.yahoo.documentapi.messagebus.protocol.DocumentProtocol;
+
+/**
+ * <p>A session for synchronous access to a document repository. This class
+ * provides simple document access where throughput is not a concern.</p>
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public interface SyncSession extends Session {
+
+ /**
+ * <p>Puts a document. When this method returns, the document is safely
+ * received. This enables setting condition compared to using Document.</p>
+ *
+ * @param documentPut The DocumentPut operation
+ */
+ void put(DocumentPut documentPut);
+
+ /**
+ * <p>Puts a document. When this method returns, the document is safely
+ * received.</p>
+ *
+ * @param documentPut The DocumentPut operation
+ * @param priority The priority with which to perform this operation.
+ */
+ void put(DocumentPut documentPut, DocumentProtocol.Priority priority);
+
+ /**
+ * <p>Gets a document.</p>
+ *
+ * @param id The id of the document to get.
+ * @return The known document having this id, or null if there is no
+ * document having this id.
+ * @throws UnsupportedOperationException Thrown if this access does not
+ * support retrieving.
+ */
+ Document get(DocumentId id);
+
+ /**
+ * <p>Gets a document.</p>
+ *
+ * @param id The id of the document to get.
+ * @param fieldSet A comma-separated list of fields to retrieve
+ * @param priority The priority with which to perform this operation.
+ * @return The known document having this id, or null if there is no
+ * document having this id.
+ * @throws UnsupportedOperationException Thrown if this access does not
+ * support retrieving.
+ */
+ Document get(DocumentId id, String fieldSet, DocumentProtocol.Priority priority);
+
+ /**
+ * <p>Removes a document if it is present and condition is fulfilled.</p>
+ * @param documentRemove document to delete
+ * @return true If the document with this id was removed, false otherwise.
+ */
+ boolean remove(DocumentRemove documentRemove);
+
+ /**
+ * <p>Removes a document if it is present.</p>
+ *
+ * @param documentRemove Document remove operation
+ * @param priority The priority with which to perform this operation.
+ * @return true If the document with this id was removed, false otherwise.
+ * @throws UnsupportedOperationException Thrown if this access does not
+ * support removal.
+ */
+ boolean remove(DocumentRemove documentRemove, DocumentProtocol.Priority priority);
+
+ /**
+ * <p>Updates a document.</p>
+ *
+ * @param update The updates to perform.
+ * @return True, if the document was found and updated.
+ * @throws UnsupportedOperationException Thrown if this access does not
+ * support update.
+ */
+ boolean update(DocumentUpdate update);
+
+ /**
+ * <p>Updates a document.</p>
+ *
+ * @param update The updates to perform.
+ * @param priority The priority with which to perform this operation.
+ * @return True, if the document was found and updated.
+ * @throws UnsupportedOperationException Thrown if this access does not
+ * support update.
+ */
+ boolean update(DocumentUpdate update, DocumentProtocol.Priority priority);
+
+}
diff --git a/documentapi/src/main/java/com/yahoo/documentapi/ThroughputLimitQueue.java b/documentapi/src/main/java/com/yahoo/documentapi/ThroughputLimitQueue.java new file mode 100644 index 00000000000..a24dbc07bfa --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/ThroughputLimitQueue.java @@ -0,0 +1,164 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi; + +import com.yahoo.concurrent.SystemTimer; +import com.yahoo.concurrent.Timer; + +import java.util.Collection; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; +import java.util.logging.Logger; + +/** + * Queue that limits it's size based on the throughput. Allows the queue to keep a certain number of + * seconds of processing in its queue. + */ +public class ThroughputLimitQueue<M> extends LinkedBlockingQueue<M> { + private static Logger log = Logger.getLogger(ThroughputLimitQueue.class.getName()); + + double averageWaitTime = 0; + long maxWaitTime = 0; + long startTime; + int capacity = 2; + Timer timer; + + /** + * Creates a new queue. + * + * @param queueSizeInMs The maximum time we wish to have objects waiting in the queue. + */ + public ThroughputLimitQueue(long queueSizeInMs) { + this(SystemTimer.INSTANCE, queueSizeInMs); + } + + /** + * Creates a new queue. Used for unit testing. + * + * @param t Used to measure time spent in the queue. Subclass for unit testing, or use SystemTimer for regular use. + * @param queueSizeInMs The maximum time we wish to have objects waiting in the queue. + */ + public ThroughputLimitQueue(Timer t, long queueSizeInMs) { + maxWaitTime = queueSizeInMs; + timer = t; + } + + // Doc inherited from BlockingQueue + public boolean add(M m) { + if (!offer(m)) { + throw new IllegalStateException("Queue full"); + } + return true; + } + + // Doc inherited from BlockingQueue + public boolean offer(M m) { + return remainingCapacity() > 0 && super.offer(m); + } + + /** + * Calculates the average waiting time and readjusts the queue capacity. + * + * @param m The last message that was read from queue, if any. + * @return Returns m. + */ + private M calculateAverage(M m) { + if (m == null) { + startTime = 0; + return null; + } + + if (startTime != 0) { + long waitTime = timer.milliTime() - startTime; + + if (averageWaitTime == 0) { + averageWaitTime = waitTime; + } else { + averageWaitTime = 0.99 * averageWaitTime + 0.01 * waitTime; + } + + int newCapacity = Math.max(2, (int)Math.round(maxWaitTime / averageWaitTime)); + if (newCapacity != capacity) { + log.fine("Capacity of throughput queue changed from " + capacity + " to " + newCapacity); + capacity = newCapacity; + } + } + + if (!isEmpty()) { + startTime = timer.milliTime(); + } else { + startTime = 0; + } + + return m; + } + + // Doc inherited from BlockingQueue + public M poll() { + return calculateAverage(super.poll()); + } + + // Doc inherited from BlockingQueue + public void put(M m) throws InterruptedException { + offer(m, Long.MAX_VALUE, TimeUnit.SECONDS); + } + + // Doc inherited from BlockingQueue + public boolean offer(M m, long l, TimeUnit timeUnit) throws InterruptedException { + long timeWaited = 0; + while (timeWaited < timeUnit.toMillis(l)) { + if (offer(m)) { + return true; + } + + Thread.sleep(10); + timeWaited += 10; + } + + return false; + } + + // Doc inherited from BlockingQueue + public M take() throws InterruptedException { + return poll(Long.MAX_VALUE, TimeUnit.SECONDS); + } + + // Doc inherited from BlockingQueue + public M poll(long l, TimeUnit timeUnit) throws InterruptedException { + long timeWaited = 0; + while (timeWaited < timeUnit.toMillis(l)) { + M elem = poll(); + if (elem != null) { + return elem; + } + + Thread.sleep(10); + timeWaited += 10; + } + + return null; + } + + /** + * @return Returns the maximum capacity of the queue + */ + public int capacity() { + return capacity; + } + + // Doc inherited from BlockingQueue + public int remainingCapacity() { + int sz = capacity - size(); + return (sz > 0) ? sz : 0; + } + + // Doc inherited from BlockingQueue + public boolean addAll(Collection<? extends M> ms) { + for (M m : ms) { + if (!offer(m)) { + return false; + } + } + + return true; + } +} diff --git a/documentapi/src/main/java/com/yahoo/documentapi/UpdateResponse.java b/documentapi/src/main/java/com/yahoo/documentapi/UpdateResponse.java new file mode 100644 index 00000000000..ed96234ba64 --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/UpdateResponse.java @@ -0,0 +1,47 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi; + +/** + * This response is provided for successful document update operations. Use the + * wasFound() method to check whether or not the document was actually found. + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class UpdateResponse extends Response { + + private final boolean wasFound; + + public UpdateResponse(long requestId, boolean wasFound) { + super(requestId); + this.wasFound = wasFound; + } + + public boolean wasFound() { + return wasFound; + } + + @Override + public int hashCode() { + return super.hashCode() + Boolean.valueOf(wasFound).hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof UpdateResponse)) { + return false; + } + if (!super.equals(obj)) { + return false; + } + UpdateResponse rhs = (UpdateResponse)obj; + if (wasFound != rhs.wasFound) { + return false; + } + return true; + } + + @Override + public String toString() { + return "Update" + super.toString() + " " + wasFound; + } +} diff --git a/documentapi/src/main/java/com/yahoo/documentapi/VisitorControlHandler.java b/documentapi/src/main/java/com/yahoo/documentapi/VisitorControlHandler.java new file mode 100644 index 00000000000..b46308e0daf --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/VisitorControlHandler.java @@ -0,0 +1,160 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi; + +import com.yahoo.vdslib.VisitorStatistics; + +/** + * A class for controlling a visitor supplied through visitor parameters when + * creating the visitor session. The class defines callbacks for reporting + * progress and that the visitor is done. If you want to reimplement the + * default behavior of those callbacks, you can write your own subclass. + * + * @author <a href="mailto:humbe@yahoo-inc.com">Håkon Humberset</a> + */ +public class VisitorControlHandler { + /** Possible completion codes for visiting. */ + public enum CompletionCode { + /** Visited all specified data successfully. */ + SUCCESS, + /** Aborted by user. */ + ABORTED, + /** Failure */ + FAILURE, + /** Create visitor reply did not return within the specified timeframe. */ + TIMEOUT + }; + + /** + * The result of the visitor, containing a completion code and an optional + * error message. + */ + public class Result { + public CompletionCode code; + public String message; + + public String toString() { + switch(code) { + case SUCCESS: + return "OK: " + message; + case ABORTED: + return "ABORTED: " + message; + case FAILURE: + return "FAILURE: " + message; + case TIMEOUT: + return "TIMEOUT: " + message; + } + + return "Unknown error"; + } + }; + + private VisitorControlSession session; + private ProgressToken currentProgress; + private boolean completed = false; + private Result result; + private VisitorStatistics currentStatistics; + + /** + * Called before the visitor starts. Override this method if you need + * to reset local data. Remember to call the superclass' method as well. + */ + public void reset() { + synchronized (this) { + session = null; + currentProgress = null; + completed = false; + result = null; + } + } + + /** + * Callback called when progress has changed. + * + * @param token the most recent progress token for this visitor + */ + public void onProgress(ProgressToken token) { + currentProgress = token; + } + + /** + * Callback for visitor error messages. + * + * @param message the error message + */ + public void onVisitorError(String message) { + } + + /** + * Callback for visitor statistics updates. + * + * @param vs The current statistics for this visitor. + */ + public void onVisitorStatistics(VisitorStatistics vs) { + currentStatistics = vs; + } + + /** + * Callback called when the visitor is done. + * + * @param code the completion code + * @param message an optional error message + */ + public void onDone(CompletionCode code, String message) { + synchronized (this) { + completed = true; + result = new Result(); + result.code = code; + result.message = message; + notifyAll(); + } + } + + /** @param session the visitor session used for this visitor */ + public void setSession(VisitorControlSession session) { + this.session = session; + } + + /** @return Retrieves the last progress token gotten for this visitor. If visitor has not been started, returns null.*/ + public ProgressToken getProgress() { return currentProgress; } + + public VisitorStatistics getVisitorStatistics() { return currentStatistics; } + + /** @return True if the visiting is done (either by error or success). */ + public boolean isDone() { + synchronized (this) { + return completed; + } + } + + /** + * Waits until visiting is done, or the given timeout (in ms) expires. + * Will wait forever if timeout is 0. + * + * @param timeoutMs The maximum amount of milliseconds to wait. + * @return True if visiting is done (either by error or success). + * @throws InterruptedException If an interrupt signal was received while waiting. + */ + public boolean waitUntilDone(long timeoutMs) throws InterruptedException { + synchronized (this) { + if (completed) return true; + if (timeoutMs == 0) { + while (!completed) { + wait(); + } + } else { + wait(timeoutMs); + } + return completed; + } + } + + /** + * Abort this visitor + */ + public void abort() { session.abort(); } + + /** + @return The result of the visiting, if done. If not done, returns null. + */ + public Result getResult() { return result; }; +} diff --git a/documentapi/src/main/java/com/yahoo/documentapi/VisitorControlSession.java b/documentapi/src/main/java/com/yahoo/documentapi/VisitorControlSession.java new file mode 100644 index 00000000000..4296407d633 --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/VisitorControlSession.java @@ -0,0 +1,53 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi; + +/** + * Superclass for document <i>visiting</i> functionality - accessing + * documents in an order decided by the document repository. This allows much + * higher read throughput than random access. + * <p> + * The class supplies an interface for functions that are common for different + * kinds of visitor sessions, such as acking visitor data and aborting the + * session. + * + * @author <a href="mailto:humbe@yahoo-inc.com">Håkon Humberset</a> + */ +public interface VisitorControlSession { + /** + * Acknowledges a response previously retrieved by the <code>getNext</code> + * method. + * + * @param token The ack token. You must get this from the visitor response + * returned by the <code>getNext</code> method. + */ + public void ack(AckToken token); + + /** + * Aborts the session. + */ + public void abort(); + + /** + * Returns the next response of this session. This method returns immediately. + * + * @return the next response, or null if no response is ready at this time + */ + public VisitorResponse getNext(); + + /** + * Returns the next response of this session. This will block until a response is ready + * or until the given timeout is reached + * + * @param timeoutMilliseconds the max time to wait for a response. If the number is 0, this will block + * without any timeout limit + * @return the next response, or null if no response becomes ready before the timeout expires + * @throws InterruptedException if this thread is interrupted while waiting + */ + public VisitorResponse getNext(int timeoutMilliseconds) throws InterruptedException; + + /** + * Destroys this session and frees up any resources it has held. + */ + public void destroy(); + +} diff --git a/documentapi/src/main/java/com/yahoo/documentapi/VisitorDataHandler.java b/documentapi/src/main/java/com/yahoo/documentapi/VisitorDataHandler.java new file mode 100644 index 00000000000..4cee27a9fda --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/VisitorDataHandler.java @@ -0,0 +1,106 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi; + +import com.yahoo.document.Document; +import com.yahoo.document.DocumentId; +import com.yahoo.documentapi.messagebus.protocol.*; +import com.yahoo.messagebus.Message; +import com.yahoo.vdslib.DocumentList; +import com.yahoo.vdslib.Entry; +import com.yahoo.vdslib.SearchResult; +import com.yahoo.vdslib.DocumentSummary; +import com.yahoo.document.BucketId; +import java.util.List; + +/** + * A data handler is a class that handles responses from a visitor. + * Different clients might want different interfaces. + * Some might want a callback interface, some might want a polling interface. + * Some want good control of acking, while others just want something simple. + * <p> + * Use a data handler that fits your needs to be able to use visiting easily. + * + * @author <a href="mailto:humbe@yahoo-inc.com">Håkon Humberset</a> + */ +public abstract class VisitorDataHandler { + protected VisitorControlSession session; + + /** Creates a new visitor data handler. */ + public VisitorDataHandler() { + } + + /** + * Called before the visitor starts. Override this method if you need + * to reset local data. Remember to call the superclass' method as well. + */ + public void reset() { + session = null; + } + + /** + * Sets which session this visitor data handler belongs to. This is done by + * the session itself and should not be called manually. The session is + * needed for ack to work. + * + * @param session the session currently using this data handler + */ + public void setSession(VisitorControlSession session) { + this.session = session; + } + + /** + * Returns the next response of this session. This method returns + * immediately. + * + * @return the next response, or null if no response is ready at this time + * @throws UnsupportedOperationException if data handler does not support + * the operation + */ + public VisitorResponse getNext() { + throw new UnsupportedOperationException("This datahandler doesn't support polling"); + } + + /** + * Returns the next response of this session. This will block until a + * response is ready or the given timeout is reached. + * + * @param timeoutMilliseconds the max time to wait for a response. If the + * number is 0, this will block without any + * timeout limit + * @return the next response, or null if no response becomes ready before + * the timeout expires + * @throws InterruptedException if this thread is interrupted while waiting + * @throws UnsupportedOperationException if data handler does not support + * the operation + */ + public VisitorResponse getNext(int timeoutMilliseconds) throws InterruptedException { + throw new UnsupportedOperationException("This datahandler doesn't support polling"); + } + + /** + * Called when visiting is done, to notify clients waiting on getNext(). + */ + public void onDone() {} + + /** + * Called when a data message is received. + * + * May be called concurrently from multiple threads. Any internal state + * mutations must be done in a thread-safe manner. + * + * @param m The message received + * @param token A token to reply with when finished processing the message. + */ + public abstract void onMessage(Message m, AckToken token); + + /** + * Function used to ack data. You need to ack data periodically, as storage + * will halt visiting when it has too much client requests pending. + * + * @param token The token to ack. Gotten from an earlier callback. + */ + public void ack(AckToken token) { + session.ack(token); + } + +} diff --git a/documentapi/src/main/java/com/yahoo/documentapi/VisitorDataQueue.java b/documentapi/src/main/java/com/yahoo/documentapi/VisitorDataQueue.java new file mode 100644 index 00000000000..5e65ee534ba --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/VisitorDataQueue.java @@ -0,0 +1,82 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi; + +import com.yahoo.document.BucketId; +import com.yahoo.documentapi.messagebus.protocol.DocumentListEntry; +import com.yahoo.messagebus.Message; +import com.yahoo.vdslib.DocumentList; + +import java.util.LinkedList; +import java.util.List; + + +/** + * A visitor data handler that queues up documents in visitor responses and + * implements the <code>getNext</code> methods, thus implementing the polling + * API defined in VisitorDataHandler. + * <p> + * Visitor responses containing document lists should be polled for with the + * <code>getNext</code> methods and need to be acked when processed for + * visiting not to halt. The class is thread safe. + * + * @author <a href="mailto:humbe@yahoo-inc.com">HÃ¥kon Humberset</a> + */ +public class VisitorDataQueue extends VisitorDataHandler { + + final LinkedList<VisitorResponse> pendingResponses = new LinkedList<VisitorResponse>(); + + /** Creates a new visitor data queue. */ + public VisitorDataQueue() { + } + + // Inherit doc from VisitorDataHandler + public void reset() { + super.reset(); + synchronized (pendingResponses) { + pendingResponses.clear(); + } + } + + public void onMessage(Message m, AckToken token) { + } + + // Inherit doc from VisitorDataHandler + public void onDocuments(DocumentList docs, AckToken token) { + synchronized (pendingResponses) { + pendingResponses.add(new DocumentListVisitorResponse(docs, token)); + pendingResponses.notifyAll(); + } + } + + // Inherit doc from VisitorDataHandler + public VisitorResponse getNext() { + synchronized (pendingResponses) { + return (pendingResponses.isEmpty() + ? null : pendingResponses.removeFirst()); + } + } + + // Inherit doc from VisitorDataHandler + public VisitorResponse getNext(int timeoutMilliseconds) throws InterruptedException { + synchronized (pendingResponses) { + if (pendingResponses.isEmpty()) { + if (timeoutMilliseconds == 0) { + while (pendingResponses.isEmpty()) { + pendingResponses.wait(); + } + } else { + pendingResponses.wait(timeoutMilliseconds); + } + } + return (pendingResponses.isEmpty() + ? null : pendingResponses.removeFirst()); + } + } + + @Override + public void onDone() { + synchronized (pendingResponses) { + pendingResponses.notifyAll(); + } + } +} diff --git a/documentapi/src/main/java/com/yahoo/documentapi/VisitorDestinationParameters.java b/documentapi/src/main/java/com/yahoo/documentapi/VisitorDestinationParameters.java new file mode 100644 index 00000000000..a27e1bb405c --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/VisitorDestinationParameters.java @@ -0,0 +1,29 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi; + +/** + * Parameters for creating or opening a visitor destination session. + * + * @author <a href="mailto:thomasg@yahoo-inc.com">Thomas Gundersen</a> + */ +public class VisitorDestinationParameters extends Parameters { + private String sessionName; + private VisitorDataHandler dataHandler; + + /** + * Creates visitor destination parameters from the supplied parameters. + * + * @param sessionName The name of the destination session. + * @param handler The data handler. + */ + public VisitorDestinationParameters(String sessionName, VisitorDataHandler handler) { + this.sessionName = sessionName; + dataHandler = handler; + } + + /** @return the name of the destination session */ + public String getSessionName() { return sessionName; }; + + /** @return the data handler */ + public VisitorDataHandler getDataHandler() { return dataHandler; }; +} diff --git a/documentapi/src/main/java/com/yahoo/documentapi/VisitorDestinationSession.java b/documentapi/src/main/java/com/yahoo/documentapi/VisitorDestinationSession.java new file mode 100644 index 00000000000..bb2b3975292 --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/VisitorDestinationSession.java @@ -0,0 +1,10 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi; + +/** + * A visitor destination session for receiving data from a visitor. + * + * @author <a href="mailto:thomasg@yahoo-inc.com">Thomas Gundersen</a> + */ +public interface VisitorDestinationSession extends VisitorControlSession { +} diff --git a/documentapi/src/main/java/com/yahoo/documentapi/VisitorIterator.java b/documentapi/src/main/java/com/yahoo/documentapi/VisitorIterator.java new file mode 100755 index 00000000000..cde434df141 --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/VisitorIterator.java @@ -0,0 +1,797 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi;
+
+import com.yahoo.document.BucketId;
+import com.yahoo.document.BucketIdFactory;
+import com.yahoo.document.select.BucketSelector;
+import com.yahoo.document.select.parser.ParseException;
+import com.yahoo.log.LogLevel;
+
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.logging.Logger;
+
+/**
+ * <p>Enables transparent iteration of super/sub-buckets</p>
+ *
+ * <p>Thread safety: safe for threads to hold their own iterators (no shared state),
+ * as long as they also hold the ProgressToken object associated with it. No two
+ * VisitorIterator instances may share the same progress token instance at the
+ * same time.
+ * Concurrent access to a single VisitorIterator instance is not safe and must
+ * be handled atomically by the caller.</p>
+ *
+ * @author <a href="mailto:vekterli@yahoo-inc.com">Tor Brede Vekterli</a>
+ */
+public class VisitorIterator {
+ private ProgressToken progressToken;
+ private BucketSource bucketSource;
+ private int distributionBitCount;
+
+ private static final Logger log = Logger.getLogger(VisitorIterator.class.getName());
+
+ public static class BucketProgress {
+ private BucketId superbucket;
+ private BucketId progress;
+
+ public BucketProgress(BucketId superbucket, BucketId progress) {
+ this.superbucket = superbucket;
+ this.progress = progress;
+ }
+
+ public BucketId getProgress() {
+ return progress;
+ }
+
+ public BucketId getSuperbucket() {
+ return superbucket;
+ }
+ }
+
+ /**
+ * Provides an abstract interface to <code>VisitorIterator</code> for
+ * how pending buckets are acquired, decoupling this from the iteration
+ * itself.
+ *
+ * <em>Important</em>: it is the responsibility of the {@link BucketSource} implementation
+ * to ensure that progress information is honored for (partially) finished buckets.
+ * From the point of view of the iterator itself, it should not have to deal with
+ * filtering away already finished buckets, as this is a detail best left to
+ * bucket sources.
+ */
+ protected static interface BucketSource {
+ public boolean hasNext();
+ public boolean shouldYield();
+ public boolean visitsAllBuckets();
+ public BucketProgress getNext();
+ public long getTotalBucketCount();
+ public int getDistributionBitCount();
+ public void setDistributionBitCount(int distributionBitCount,
+ ProgressToken progress);
+ public void update(BucketId superbucket, BucketId progress,
+ ProgressToken token);
+ }
+
+ /**
+ * Provides a bucket source that encompasses the entire range available
+ * through a given value of distribution bits
+ */
+ protected static class DistributionRangeBucketSource implements BucketSource {
+ private boolean flushActive = false;
+ private int distributionBitCount;
+ // Wouldn't need this if this were a non-static class, but do it for
+ // the sake of keeping things identical in Java and C++
+ private ProgressToken progressToken;
+
+ public DistributionRangeBucketSource(int distributionBitCount,
+ ProgressToken progress) {
+ progressToken = progress;
+
+ // New progress token (could also be empty, in which this is a
+ // no-op anyway)
+ if (progressToken.getTotalBucketCount() == 0) {
+ assert(progressToken.isEmpty()) : "inconsistent progress state";
+ progressToken.setTotalBucketCount(1L << distributionBitCount);
+ progressToken.setDistributionBitCount(distributionBitCount);
+ progressToken.setBucketCursor(0);
+ progressToken.setFinishedBucketCount(0);
+ this.distributionBitCount = distributionBitCount;
+ }
+ else {
+ this.distributionBitCount = progressToken.getDistributionBitCount();
+ // Quick consistency check to ensure the user isn't trying to eg.
+ // pass a progress token for an explicit document selection
+ if (progressToken.getTotalBucketCount() != (1L << progressToken.getDistributionBitCount())) {
+ throw new IllegalArgumentException("Total bucket count in existing progress is not "
+ + "consistent with that of the current document selection");
+ }
+ }
+
+ if (!progress.isFinished()) {
+ if (log.isLoggable(LogLevel.DEBUG)) {
+ log.log(LogLevel.DEBUG, "Importing unfinished progress token with " +
+ "bits: " + progressToken.getDistributionBitCount() +
+ ", active: " + progressToken.getActiveBucketCount() +
+ ", pending: " + progressToken.getPendingBucketCount() +
+ ", cursor: " + progressToken.getBucketCursor() +
+ ", finished: " + progressToken.getFinishedBucketCount() +
+ ", total: " + progressToken.getTotalBucketCount());
+ }
+ if (!progress.isEmpty()) {
+ // Lower all active to pending
+ if (progressToken.getActiveBucketCount() > 0) {
+ if (log.isLoggable(LogLevel.DEBUG)) {
+ log.log(LogLevel.DEBUG, "Progress token had active buckets upon range " +
+ "construction. Setting these as pending");
+ }
+ progressToken.setAllBucketsToState(ProgressToken.BucketState.BUCKET_PENDING);
+ }
+ // Fixup for any buckets that were active when progress was written
+ // but are now pending and with wrong dist bits (used-bits). Buckets
+ // split here may very well be split/merged again if we set a new dist
+ // bit count, but that is the desired process
+ correctInconsistentPending(progressToken.getDistributionBitCount());
+ // Fixup for bucket cursor in case of bucket space downscaling
+ correctTruncatedBucketCursor();
+
+ if (log.isLoggable(LogLevel.DEBUG)) {
+ log.log(LogLevel.DEBUG, "Partial bucket space progress; continuing "+
+ "from position " + progressToken.getBucketCursor());
+ }
+ }
+ progressToken.setFinishedBucketCount(progressToken.getBucketCursor() -
+ progressToken.getPendingBucketCount());
+ } else {
+ assert(progressToken.getBucketCursor() == progressToken.getTotalBucketCount());
+ }
+ // Should be all fixed up and good to go
+ progressToken.setInconsistentState(false);
+ }
+
+ protected boolean isLosslessResetPossible() {
+ // #pending must be equal to cursor, i.e. all buckets ever fetched
+ // must be located in the set of pending
+ if (progressToken.getPendingBucketCount() != progressToken.getBucketCursor()) {
+ return false;
+ }
+ // Check if all pending buckets have a progress of 0
+ for (Map.Entry<ProgressToken.BucketKeyWrapper, ProgressToken.BucketEntry> entry
+ : progressToken.getBuckets().entrySet()) {
+ if (entry.getValue().getState() != ProgressToken.BucketState.BUCKET_PENDING) {
+ return false;
+ }
+ if (entry.getValue().getProgress().getId() != 0) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Ensure that a given <code>ProgressToken</code> instance only has
+ * buckets pending that have a used-bits count of that of the
+ * <code>targetDistCits</code>. This is done by splitting or merging
+ * all inconsistent buckets until the desired state is reached.
+ *
+ * Time complexity is approx <i>O(4bn)</i> where <i>b</i> is the maximum
+ * delta of bits to change anywhere in the set of pending and <i>n</i>
+ * is the number of pending. This includes the time spent making shallow
+ * map copies.
+ *
+ * @param targetDistBits The desired distribution bit count of the buckets
+ */
+ private void correctInconsistentPending(int targetDistBits) {
+ boolean maybeInconsistent = true;
+ long bucketsSplit = 0, bucketsMerged = 0;
+ long pendingBefore = progressToken.getPendingBucketCount();
+ ProgressToken p = progressToken;
+
+ // Optimization: before doing any splitting/merging at all, we check
+ // to see if we can't simply just reset the entire internal state
+ // with the new distribution bit count. This ensures that if we go
+ // from eg. 1 bit to 20 bits, we won't have to perform a grueling
+ // half a million splits to cover the same bucket space as that 1
+ // single-bit bucket once did
+ if (isLosslessResetPossible()) {
+ if (log.isLoggable(LogLevel.DEBUG)) {
+ log.log(LogLevel.DEBUG, "At start of bucket space and all " +
+ "buckets have no progress; doing a lossless reset " +
+ "instead of splitting/merging");
+ }
+ assert(p.getActiveBucketCount() == 0);
+ p.clearAllBuckets();
+ p.setBucketCursor(0);
+ return;
+ }
+
+ while (maybeInconsistent) {
+ BucketId lastMergedBucket = null;
+ maybeInconsistent = false;
+ // Make a shallow working copy of the bucket map. BucketKeyWrapper
+ // keys are considered immutable, and should thus not be at risk
+ // for being changed during the inner loop
+ // Do separate passes for splitting and merging just to make
+ // absolutely sure that the two ops won't step on each others'
+ // toes. This isn't wildly efficient, but the data sets in question
+ // are presumed to be low in size and this is presumed to be a very
+ // infrequent operation
+ TreeMap<ProgressToken.BucketKeyWrapper, ProgressToken.BucketEntry> buckets
+ = new TreeMap<ProgressToken.BucketKeyWrapper, ProgressToken.BucketEntry>(p.getBuckets());
+ for (Map.Entry<ProgressToken.BucketKeyWrapper, ProgressToken.BucketEntry> entry
+ : buckets.entrySet()) {
+ assert(entry.getValue().getState() == ProgressToken.BucketState.BUCKET_PENDING);
+ BucketId pending = new BucketId(ProgressToken.keyToBucketId(entry.getKey().getKey()));
+ if (pending.getUsedBits() < targetDistBits) {
+ if (pending.getUsedBits() + 1 < targetDistBits) {
+ maybeInconsistent = true; // Do another pass
+ }
+ p.splitPendingBucket(pending);
+ ++bucketsSplit;
+ }
+ }
+
+ // Make new map copy with potentially split buckets
+ buckets = new TreeMap<ProgressToken.BucketKeyWrapper, ProgressToken.BucketEntry>(p.getBuckets());
+ for (Map.Entry<ProgressToken.BucketKeyWrapper, ProgressToken.BucketEntry> entry
+ : buckets.entrySet()) {
+ assert(entry.getValue().getState() == ProgressToken.BucketState.BUCKET_PENDING);
+ BucketId pending = new BucketId(ProgressToken.keyToBucketId(entry.getKey().getKey()));
+ if (pending.getUsedBits() > targetDistBits) {
+ // If this is the right sibling of an already merged left sibling,
+ // it's already been merged away, so we should skip it
+ if (lastMergedBucket != null) {
+ BucketId rightCheck = new BucketId(lastMergedBucket.getUsedBits(),
+ lastMergedBucket.getId() | (1L << (lastMergedBucket.getUsedBits() - 1)));
+ if (pending.equals(rightCheck)) {
+ if (log.isLoggable(LogLevel.SPAM)) {
+ log.log(LogLevel.SPAM, "Skipped " + pending +
+ ", as it was right sibling of " + lastMergedBucket);
+ }
+ continue;
+ }
+ }
+ if (pending.getUsedBits() - 1 > targetDistBits) {
+ maybeInconsistent = true; // Do another pass
+ }
+ p.mergePendingBucket(pending);
+ ++bucketsMerged;
+
+ lastMergedBucket = pending;
+ }
+ }
+ }
+ if ((bucketsSplit > 0 || bucketsMerged > 0) && log.isLoggable(LogLevel.DEBUG)) {
+ log.log(LogLevel.DEBUG, "Existing progress' pending buckets had inconsistent " +
+ "distribution bits; performed " + bucketsSplit + " split ops and " +
+ bucketsMerged + " merge ops. Pending: " + pendingBefore + " -> " +
+ p.getPendingBucketCount());
+ }
+ }
+
+ private void correctTruncatedBucketCursor() {
+ // We've truncated the bucket cursor, but in doing so we might
+ // have moved back beyond where there are pending buckets. Consider
+ // having a cursor value of 3 at 31 bits and then moving to 11 bits.
+ // With 1 pending we'll normally reach a cursor of 0, even though it
+ // should be 1
+ for (ProgressToken.BucketKeyWrapper bucketKey
+ : progressToken.getBuckets().keySet()) {
+ BucketId bid = bucketKey.toBucketId();
+ long idx = bucketKey.getKey() >>> (64 - bid.getUsedBits());
+ if (bid.getUsedBits() == distributionBitCount
+ && idx >= progressToken.getBucketCursor()) {
+ progressToken.setBucketCursor(idx + 1);
+ }
+ }
+ if (log.isLoggable(LogLevel.SPAM)) {
+ log.log(LogLevel.SPAM, "New range bucket cursor is " +
+ progressToken.getBucketCursor());
+ }
+ }
+
+ public boolean hasNext() {
+ return progressToken.getBucketCursor() < (1L << distributionBitCount);
+ }
+
+ public boolean shouldYield() {
+ // If we need to flush all active buckets, stall the iteration until
+ // this has been done
+ return flushActive;
+ }
+
+ public boolean visitsAllBuckets() {
+ return true;
+ }
+
+ public long getTotalBucketCount() {
+ return 1L << distributionBitCount;
+ }
+
+ public BucketProgress getNext() {
+ assert(hasNext()) : "getNext() called with hasNext() == false";
+ long currentPosition = progressToken.getBucketCursor();
+ long key = ProgressToken.makeNthBucketKey(currentPosition, distributionBitCount);
+ ++currentPosition;
+ progressToken.setBucketCursor(currentPosition);
+ return new BucketProgress(
+ new BucketId(ProgressToken.keyToBucketId(key)),
+ new BucketId());
+ }
+
+ public int getDistributionBitCount() {
+ return distributionBitCount;
+ }
+
+ public void setDistributionBitCount(int distributionBitCount,
+ ProgressToken progress)
+ {
+ this.distributionBitCount = distributionBitCount;
+
+ // There might be a case where we're waiting for active buckets
+ // already when a new distribution bit change comes in. If so,
+ // don't do anything at all yet with the set of pending
+ if (progressToken.getActiveBucketCount() > 0) {
+ flushActive = true;
+ if (log.isLoggable(LogLevel.DEBUG)) {
+ log.log(LogLevel.DEBUG, "Holding off new/pending buckets and consistency " +
+ "correction until all " + progress.getActiveBucketCount() +
+ " active buckets have been updated");
+ }
+ progressToken.setInconsistentState(true);
+ } else {
+ // Only perform the actual distribution bit bucket ops if we've
+ // got no pending buckets
+ int delta = distributionBitCount - progressToken.getDistributionBitCount();
+
+ // Must do this before setting the bucket cursor to allow
+ // reset-checking to be performed
+ correctInconsistentPending(distributionBitCount);
+ if (delta > 0) {
+ if (log.isLoggable(LogLevel.DEBUG)) {
+ log.log(LogLevel.DEBUG, "Increasing distribution bits for full bucket " +
+ "space range source from " + progressToken.getDistributionBitCount() + " to " +
+ distributionBitCount);
+ }
+ progressToken.setFinishedBucketCount(progressToken.getFinishedBucketCount() << delta);
+ // By n-doubling the position, the bucket key ordering ensures
+ // we go from eg. 3:0x02 to 4:0x02 to 5:02 etc.
+ progressToken.setBucketCursor(progressToken.getBucketCursor() << delta);
+ } else if (delta < 0) {
+ if (log.isLoggable(LogLevel.DEBUG)) {
+ log.log(LogLevel.DEBUG, "Decreasing distribution bits for full bucket " +
+ "space range source from " + progressToken.getDistributionBitCount() +
+ " to " + distributionBitCount + " bits");
+ }
+ // Scale down bucket space and cursor
+ progressToken.setBucketCursor(progressToken.getBucketCursor() >>> -delta);
+ progressToken.setFinishedBucketCount(progressToken.getFinishedBucketCount() >>> -delta);
+ }
+
+ progressToken.setTotalBucketCount(1L << distributionBitCount);
+ progressToken.setDistributionBitCount(distributionBitCount);
+
+ correctTruncatedBucketCursor();
+ progressToken.setInconsistentState(false);
+ }
+ }
+
+ public void update(BucketId superbucket, BucketId progress,
+ ProgressToken token) {
+ progressToken.updateProgress(superbucket, progress);
+
+ if (superbucket.getUsedBits() != distributionBitCount) {
+ if (!progress.equals(ProgressToken.FINISHED_BUCKET)) {
+ // We should now always flush active buckets before doing a
+ // consistency fix. This simplifies things greatly
+ assert(flushActive);
+ if (log.isLoggable(LogLevel.DEBUG)) {
+ log.log(LogLevel.DEBUG, "Received non-finished bucket " +
+ superbucket + " with wrong distribution bit count (" +
+ superbucket.getUsedBits() + "). Waiting to correct " +
+ "until all active are done");
+ }
+ } else {
+ if (log.isLoggable(LogLevel.DEBUG)) {
+ log.log(LogLevel.DEBUG, "Received finished bucket " +
+ superbucket + " with wrong distribution bit count (" +
+ superbucket.getUsedBits() + "). Waiting to correct " +
+ "until all active are done");
+ }
+ }
+ }
+
+ if (progressToken.getActiveBucketCount() == 0) {
+ if (flushActive) {
+ if (log.isLoggable(LogLevel.DEBUG)) {
+ log.log(LogLevel.DEBUG, "All active buckets flushed, " +
+ "correcting progress token and continuing normal operation");
+ }
+ // Trigger the actual bucket state change this time
+ setDistributionBitCount(distributionBitCount, progressToken);
+ assert(progressToken.getDistributionBitCount() == distributionBitCount);
+ }
+ flushActive = false;
+ // Update #finished since we might have had inconsistent active
+ // buckets that have prevent us from getting a correct value. At
+ // this point, however, all pending buckets should presumably be
+ // at the same, correct dist bit count, so we can safely compute
+ // a new count
+ // TODO: ensure this is consistent
+ if (progressToken.getPendingBucketCount() <= progressToken.getBucketCursor()) {
+ progressToken.setFinishedBucketCount(progressToken.getBucketCursor() -
+ progressToken.getPendingBucketCount());
+ }
+ }
+ }
+ }
+
+ /**
+ * Provides an explicit set of bucket IDs to iterate over. Will immediately
+ * set these as pending in the {@link ProgressToken}, as it is presumed this set is
+ * rather small. Changing the distribution bit count for this source is
+ * effectively a no-op, as explicit bucket IDs should not be implicitly
+ * changed.
+ */
+ protected static class ExplicitBucketSource implements BucketSource {
+ private int distributionBitCount;
+ private long totalBucketCount = 0;
+
+ public ExplicitBucketSource(Set<BucketId> superbuckets,
+ int distributionBitCount,
+ ProgressToken progress) {
+ this.distributionBitCount = progress.getDistributionBitCount();
+ this.totalBucketCount = superbuckets.size();
+
+ // New progress token?
+ if (progress.getTotalBucketCount() == 0) {
+ progress.setTotalBucketCount(this.totalBucketCount);
+ progress.setDistributionBitCount(distributionBitCount);
+ this.distributionBitCount = distributionBitCount;
+ }
+ else {
+ // Quick consistency check to ensure the user isn't trying to eg.
+ // pass a progress token for another document selection
+ if (progress.getTotalBucketCount() != totalBucketCount
+ || (progress.getFinishedBucketCount() + progress.getPendingBucketCount()
+ + progress.getActiveBucketCount() != totalBucketCount)) {
+ throw new IllegalArgumentException("Total bucket count in existing progress is not " +
+ "consistent with that of the current document selection");
+ }
+ if (progress.getBucketCursor() != 0) {
+ // Trying to use a range source progress file
+ throw new IllegalArgumentException("Cannot use given progress file with the "+
+ "current document selection");
+ }
+ this.distributionBitCount = progress.getDistributionBitCount();
+ }
+
+ if (progress.isFinished() || !progress.isEmpty()) return;
+
+ for (BucketId id : superbuckets) {
+ // Add all superbuckets with zero sub-bucket progress and pending
+ progress.addBucket(id, new BucketId(), ProgressToken.BucketState.BUCKET_PENDING);
+ }
+ }
+
+ public boolean hasNext() {
+ return false;
+ }
+
+ public boolean shouldYield() {
+ return false;
+ }
+
+ public boolean visitsAllBuckets() {
+ return false;
+ }
+
+ public long getTotalBucketCount() {
+ return totalBucketCount;
+ }
+
+ // All explicit buckets should have been placed in the progress
+ // token during construction, so this method should never be called
+ public BucketProgress getNext() {
+ throw new IllegalStateException("getNext() called on ExplicitBucketSource");
+ }
+
+ public int getDistributionBitCount() {
+ return distributionBitCount;
+ }
+
+ public void setDistributionBitCount(int distributionBitCount,
+ ProgressToken progress)
+ {
+ // Setting distribution bits for explicit bucket source is essentially
+ // a no-op, since its buckets already are fixed at 32 used bits.
+ progress.setDistributionBitCount(distributionBitCount);
+ this.distributionBitCount = distributionBitCount;
+ if (log.isLoggable(LogLevel.DEBUG)) {
+ log.log(LogLevel.DEBUG, "Set distribution bit count to "
+ + distributionBitCount + " for explicit bucket source (no-op)");
+ }
+ }
+
+ public void update(BucketId superbucket, BucketId progress,
+ ProgressToken token) {
+ // Simply delegate to ProgressToken, as it maintains all progress state
+ token.updateProgress(superbucket, progress);
+ }
+ }
+
+ /**
+ * @param bucketSource An instance of {@link BucketSource}, providing the working set for
+ * the iterator
+ * @param progressToken A {@link ProgressToken} instance, allowing the progress of
+ * finished or partially finished buckets to be tracked
+ *
+ * @see BucketSource
+ * @see ProgressToken
+ */
+ private VisitorIterator(ProgressToken progressToken,
+ BucketSource bucketSource) {
+ assert(progressToken.getDistributionBitCount() == bucketSource.getDistributionBitCount())
+ : "inconsistent distribution bit counts";
+ this.distributionBitCount = progressToken.getDistributionBitCount();
+ this.progressToken = progressToken;
+ this.bucketSource = bucketSource;
+ }
+
+
+ /**
+ * @return The pair [superbucket, progress] that specifies the next iterable
+ * bucket. When a superbucket is initially returned, the pair is equal to
+ * that of [superbucket, 0], as there has been no progress into its sub-buckets
+ * yet (if they exist).
+ *
+ * Precondition: <code>hasNext() == true</code>
+ */
+ public BucketProgress getNext() {
+ assert(progressToken.getDistributionBitCount() == bucketSource.getDistributionBitCount())
+ : "inconsistent distribution bit counts for progress and source";
+ assert(hasNext());
+ // We prioritize returning buckets in the pending map over those
+ // that may be in the bucket source, since we want to avoid growing
+ // the map too much
+ if (progressToken.hasPending()) {
+ // Find first pending bucket in token
+ TreeMap<ProgressToken.BucketKeyWrapper, ProgressToken.BucketEntry> buckets = progressToken.getBuckets();
+ ProgressToken.BucketEntry pending = null;
+ BucketId superbucket = null;
+ for (Map.Entry<ProgressToken.BucketKeyWrapper, ProgressToken.BucketEntry> entry : buckets.entrySet()) {
+ if (entry.getValue().getState() == ProgressToken.BucketState.BUCKET_PENDING) {
+ pending = entry.getValue();
+ superbucket = new BucketId(ProgressToken.keyToBucketId(entry.getKey().getKey()));
+ break;
+ }
+ }
+ assert(pending != null) : "getNext() called with inconsistent state";
+
+ // Set bucket to active, since it's not awaiting an update
+ pending.setState(ProgressToken.BucketState.BUCKET_ACTIVE);
+
+ progressToken.setActiveBucketCount(progressToken.getActiveBucketCount() + 1);
+ progressToken.setPendingBucketCount(progressToken.getPendingBucketCount() - 1);
+
+ return new BucketProgress(superbucket, pending.getProgress());
+ } else {
+ BucketProgress ret = bucketSource.getNext();
+ progressToken.addBucket(ret.getSuperbucket(), ret.getProgress(),
+ ProgressToken.BucketState.BUCKET_ACTIVE);
+ return ret;
+ }
+ }
+
+ /**
+ * <p>Check whether or not it is valid to call {@link #getNext()} with the current
+ * iterator state.</p>
+ *
+ * <p>There exists a case wherein <code>hasNext</code> may return false before {@link #update} is
+ * called, but true afterwards. This happens when the set of pending buckets is
+ * empty, the bucket source is empty <em>but</em> the set of active buckets is
+ * not. A future progress update on any of the buckets in the active set may
+ * or may not make that bucket available to the pending set again.
+ * This must be handled explicitly by the caller by checking {@link #isDone()}
+ * and ensuring that {@link #update} is called before retrying <code>hasNext</code>.</p>
+ *
+ * <p>This method will also return false if the number of distribution bits have
+ * changed and there are active buckets needing to be flushed before the
+ * iterator will allow new buckets to be handed out.</p>
+ *
+ * @return Whether or not it is valid to call {@link #getNext()} with the current
+ * iterator state.
+ */
+ public boolean hasNext() {
+ return (progressToken.hasPending() || bucketSource.hasNext()) && !bucketSource.shouldYield();
+ }
+
+ /**
+ * Check if the iterator is actually done
+ *
+ * @see #hasNext()
+ *
+ * @return <code>true</code> <em>iff</em> the bucket source is empty and
+ * there are no pending or active buckets in the progress token.
+ */
+ public boolean isDone() {
+ return !(hasNext() || progressToken.hasActive());
+ }
+
+ /**
+ * <p>Tell the iterator that we've finished processing up to <i>and
+ * including</i> <code>progress</code>. <code>progress</code> may be a sub-bucket <i>or</i>
+ * the invalid 0-bucket (in case the caller fails to process the bucket and
+ * must return it to the set of pending) <em>or</em> the special case <code>BucketId(Integer.MAX_VALUE)</code>,
+ * the latter indicating to the iterator that traversal is complete for
+ * <code>superbucket</code>'s tree. The null bucket should only be used if no
+ * non-null updates have yet been given for the superbucket.</p>
+ *
+ * <p>It is a requirement that each superbucket returned by {@link #getNext()} must
+ * eventually result in 1-n update operations, where the last update operation
+ * has the special progress==super case.</p>
+ *
+ * <p>If the document selection used to create the iterator is unknown and there
+ * were active buckets at the time of a distribution bit state change, such
+ * a bucket passed to <code>update()</code> will be in an inconsistent state
+ * with regards to the number of bits it uses. For unfinished buckets, this
+ * is handled by splitting or merging it until it's consistent, depending on
+ * whether or not it had a lower or higher distribution bit count than that of
+ * the current system state. For finished buckets of a lower dist bit count,
+ * the amount of finished buckets in the ProgressToken is adjusted upwards
+ * to compensate for the fact that a bucket using fewer distribution bits
+ * actually covers more of the bucket space than the ones that are currently
+ * in use. For finished buckets of a higher dist bit count, the number of
+ * finished buckets is <em>not</em> increased at that point in time, since
+ * such a bucket doesn't actually cover an entire bucket with the current state.</p>
+ *
+ * <p>All this is done automatically and transparently to the caller once all
+ * active buckets have been updated.</p>
+ *
+ * @param superbucket A valid bucket ID that has been retrieved earlier through
+ * {@link #getNext()}
+ * @param progress A bucket logically contained within <code>super</code>. Subsequent
+ * updates for the same superbucket must have <code>progress</code> be in an increasing
+ * order, where order is defined as the in-order traversal of the bucket split
+ * tree. May also be the null bucket if the superbucket has not seen any "proper"
+ * progress updates yet or the special case Integer.MAX_VALUE. Note that inconsistent
+ * splitting might actually see <code>progress</code> as containing <code>super</code>
+ * rather than vice versa, so this is explicitly allowed to pass by the code.
+ */
+ public void update(BucketId superbucket, BucketId progress) {
+ // Delegate to bucket source, as it knows how to deal with buckets
+ // that are in an inconsistent state wrt distribution bit count
+ bucketSource.update(superbucket, progress, progressToken);
+ }
+
+ /**
+ * @return The total number of iterable buckets that remain to be processed
+ *
+ * Note: currently includes all non-finished (i.e. active and pending
+ * buckets) as well
+ */
+ public long getRemainingBucketCount() {
+ return progressToken.getTotalBucketCount() - progressToken.getFinishedBucketCount();
+ }
+
+ /**
+ * @return Internal bucket source instance. Do <i>NOT</i> modify!
+ */
+ protected BucketSource getBucketSource() {
+ return bucketSource;
+ }
+
+ public ProgressToken getProgressToken() {
+ return progressToken;
+ }
+
+ public int getDistributionBitCount() {
+ return distributionBitCount;
+ }
+
+ /**
+ * <p>Set the distribution bit count for the iterator and the buckets it
+ * currently maintains and will return in the future.</p>
+ *
+ * <p>For document selections that result in a explicit set of buckets, this
+ * is essentially a no-op, so in such a case, disregard the rest of this text.</p>
+ *
+ * <p>Changing the number of distribution bits for an unknown document
+ * selection will effectively scale the bucket space that will be visited;
+ * each bit increase or decrease doubling or halving its size, respectively.
+ * When increasing, any pending buckets will be split to ensure the total
+ * bucket space covered remains the same. Correspondingly, when decreasing,
+ * any pending buckets will be merged appropriately.</p>
+ *
+ * <p>If there are buckets active at the time of the change, the actual
+ * bucket splitting/merging operations are kept on hold until all active
+ * buckets have been updated, at which point they will be automatically
+ * performed. The iterator will force such an update by not giving out
+ * any new or pending buckets until that happens.</p>
+ *
+ * <p><em>Note:</em> when decreasing the number of distribution bits,
+ * there is a chance of losing superbucket progress in a bucket that
+ * is merged with another bucket, leading to potential duplicate
+ * results.</p>
+ *
+ * @param distBits New system state distribution bit count
+ */
+ public void setDistributionBitCount(int distBits) {
+ if (distributionBitCount != distBits) {
+ bucketSource.setDistributionBitCount(distBits, progressToken);
+ distributionBitCount = distBits;
+ if (log.isLoggable(LogLevel.DEBUG)) {
+ log.log(LogLevel.DEBUG, "Set visitor iterator distribution bit count to "
+ + distBits);
+ }
+ }
+ }
+
+ public boolean visitsAllBuckets() {
+ return bucketSource.visitsAllBuckets();
+ }
+
+ /**
+ * Create a new <code>VisitorIterator</code> instance based on the given document
+ * selection string.
+ *
+ * @param documentSelection Document selection string used to create the
+ * <code>VisitorIterator</code> instance. Depending on the characteristics of the
+ * selection, the iterator may iterate over only a small subset of the buckets or
+ * every bucket in the system. Both cases will be handled efficiently.
+ * @param idFactory {@link BucketId} factory specifying the number of distribution bits
+ * to use et al.
+ * @param progress A unique {@link ProgressToken} instance which is used for maintaining the state
+ * of the iterator. Can <em>not</em> be shared with other iterator instances at the same time.
+ * If <code>progress</code> contains work done in an earlier iteration run, the iterator will pick
+ * up from where it left off
+ * @return A new <code>VisitorIterator</code> instance
+ * @throws ParseException if <code>documentSelection</code> fails to properly parse
+ */
+ public static VisitorIterator createFromDocumentSelection(
+ String documentSelection,
+ BucketIdFactory idFactory,
+ int distributionBitCount,
+ ProgressToken progress) throws ParseException {
+ BucketSelector bucketSel = new BucketSelector(idFactory);
+ Set<BucketId> rawBuckets = bucketSel.getBucketList(documentSelection);
+ BucketSource src;
+
+ // Depending on whether the expression yielded an unknown number of
+ // buckets, we create either an explicit bucket source or a distribution
+ // bit-based range source
+ if (rawBuckets == null) {
+ // Range source
+ src = new DistributionRangeBucketSource(distributionBitCount, progress);
+ } else {
+ // Explicit source
+ src = new ExplicitBucketSource(rawBuckets, distributionBitCount, progress);
+ }
+
+ return new VisitorIterator(progress, src);
+ }
+
+ /**
+ * Create a new <code>VisitorIterator</code> instance based on the given
+ * set of buckets. This is supported for internal use only, and is required
+ * by Synchronization. Use {@link #createFromDocumentSelection} instead for
+ * all normal purposes.
+ *
+ * @param bucketsToVisit The set of buckets that will be visited
+ * @param distributionBitCount Number of distribution bits to use
+ * @param progress A unique ProgressToken instance which is used for maintaining the state
+ * of the iterator. Can <em>not</em> be shared with other iterator instances at the same time.
+ * If <code>progress</code> contains work done in an earlier iteration run, the iterator will pick
+ * up from where it left off
+ * @return A new <code>VisitorIterator</code> instance
+ */
+ public static VisitorIterator createFromExplicitBucketSet(
+ Set<BucketId> bucketsToVisit,
+ int distributionBitCount,
+ ProgressToken progress) {
+ // For obvious reasons, always create an explicit source here
+ BucketSource src = new ExplicitBucketSource(bucketsToVisit,
+ distributionBitCount, progress);
+ return new VisitorIterator(progress, src);
+ }
+}
diff --git a/documentapi/src/main/java/com/yahoo/documentapi/VisitorParameters.java b/documentapi/src/main/java/com/yahoo/documentapi/VisitorParameters.java new file mode 100644 index 00000000000..b81025c7286 --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/VisitorParameters.java @@ -0,0 +1,369 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi; + +import com.yahoo.document.BucketId; +import com.yahoo.documentapi.messagebus.loadtypes.LoadType; +import com.yahoo.documentapi.messagebus.protocol.DocumentProtocol; +import com.yahoo.messagebus.ThrottlePolicy; +import com.yahoo.messagebus.routing.Route; +import com.yahoo.text.Utf8; + +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; + +/** + * Parameters for creating or opening a visitor session + * + * @author <a href="mailto:humbe@yahoo-inc.com">Håkon Humberset</a> + */ +public class VisitorParameters extends Parameters { + + private String documentSelection; + private String visitorLibrary = "DumpVisitor"; + private int maxPending = 32; + private long timeoutMs = -1; + private long fromTimestamp = 0; + private long toTimestamp = 0; + boolean visitRemoves = false; + private String fieldSet = "[all]"; + boolean visitInconsistentBuckets = false; + private ProgressToken resumeToken = null; + private String resumeFileName = ""; + private String remoteDataHandler = null; + private VisitorDataHandler localDataHandler; + private VisitorControlHandler controlHandler; + private Map<String, byte []> libraryParameters = new TreeMap<String, byte []>(); + private Route visitRoute = null; + private float weight = 1; + private long maxFirstPassHits = -1; + private long maxTotalHits = -1; + private int visitorOrdering = 0; + private int maxBucketsPerVisitor = 1; + private boolean dynamicallyIncreaseMaxBucketsPerVisitor = false; + private float dynamicMaxBucketsIncreaseFactor = 2; + private LoadType loadType = LoadType.DEFAULT; + private DocumentProtocol.Priority priority = null; + private int traceLevel = 0; + private ThrottlePolicy throttlePolicy = null; + private boolean skipBucketsOnFatalErrors = false; + + // Advanced parameter, only for internal use. + Set<BucketId> bucketsToVisit = null; + + /** + * Creates visitor parameters from a document selection expression, using + * defaults for other parameters. + * + * @param documentSelection document selection expression + */ + public VisitorParameters(String documentSelection) { + this.documentSelection = documentSelection; + } + + /** + * Copy constructor. + * + * @param params object to copy + */ + public VisitorParameters(VisitorParameters params) { + setDocumentSelection(params.getDocumentSelection()); + setVisitorLibrary(params.getVisitorLibrary()); + setMaxPending(params.getMaxPending()); + setTimeoutMs(params.getTimeoutMs()); + setFromTimestamp(params.getFromTimestamp()); + setToTimestamp(params.getToTimestamp()); + visitRemoves(params.visitRemoves()); + fieldSet(params.fieldSet()); + visitInconsistentBuckets(params.visitInconsistentBuckets()); + setLibraryParameters(params.getLibraryParameters()); + setRoute(params.getRoute()); + setResumeFileName(params.getResumeFileName()); + setResumeToken(params.getResumeToken()); + if (params.getRemoteDataHandler() != null) { + setRemoteDataHandler(params.getRemoteDataHandler()); + } else { + setLocalDataHandler(params.getLocalDataHandler()); + } + setControlHandler(params.getControlHandler()); + setMaxFirstPassHits(params.getMaxFirstPassHits()); + setMaxTotalHits(params.getMaxTotalHits()); + setVisitorOrdering(params.getVisitorOrdering()); + setMaxBucketsPerVisitor(params.getMaxBucketsPerVisitor()); + setLoadType(params.getLoadType()); + setPriority(params.getPriority()); + setDynamicallyIncreaseMaxBucketsPerVisitor( + params.getDynamicallyIncreaseMaxBucketsPerVisitor()); + setDynamicMaxBucketsIncreaseFactor( + params.getDynamicMaxBucketsIncreaseFactor()); + setTraceLevel(params.getTraceLevel()); + skipBucketsOnFatalErrors(params.skipBucketsOnFatalErrors()); + } + + // Get functions + + // TODO: s/@return/Returns/ - this javadoc will not contain text in the method overview + + /** @return The selection string used for visiting. */ + public String getDocumentSelection() { return documentSelection; } + + /** @return What visitor library to use for the visiting. The library in question must be installed on each storage node in the target cluster. */ + public String getVisitorLibrary() { return visitorLibrary; } + + /** @return The maximum number of messages each storage visitor will have pending before waiting for acks from client. */ + public int getMaxPending() { return maxPending; } + + /** @return The timeout for the visitor in milliseconds. */ + public long getTimeoutMs() { return timeoutMs; } + + /** @return The minimum timestamp (in microsecs) of documents the visitor will visit. */ + public long getFromTimestamp() { return fromTimestamp; } + + /** @return The maximum timestamp (in microsecs) of documents the visitor will visit. */ + public long getToTimestamp() { return toTimestamp; } + + /** @return If this method returns true, the visitor will visit remove entries as well as documents (you can see what documents have been deleted). */ + public boolean visitRemoves() { return visitRemoves; } + + public boolean getVisitRemoves() { return visitRemoves; } + + public boolean getVisitHeadersOnly() { return "[header]".equals(fieldSet); } + + /** @return The field set to use. */ + public String fieldSet() { return fieldSet; } + + public String getFieldSet() { return fieldSet; } + + /** @return If this method returns true, the visitor will visit inconsistent buckets. */ + public boolean visitInconsistentBuckets() { return visitInconsistentBuckets; } + + public boolean getVisitInconsistentBuckets() { return visitInconsistentBuckets; } + + /** @return Returns a map of string → string of arguments that are passed to the visitor library. */ + public Map<String, byte []> getLibraryParameters() { return libraryParameters; } + + /** @return The progress token, which can be used to resume visitor. */ + public ProgressToken getResumeToken() { return resumeToken; } + + /** @return The filename for reading/storing progress token. */ + public String getResumeFileName() { return resumeFileName; } + + /** @return Address to the remote data handler. */ + public String getRemoteDataHandler() { return remoteDataHandler; } + + /** @return The local data handler. */ + public VisitorDataHandler getLocalDataHandler() { return localDataHandler; } + + /** @return The control handler. */ + public VisitorControlHandler getControlHandler() { return controlHandler; } + + /** @return Whether or not max buckets per visitor value should be dynamically + * increased when using orderdoc and visitors do not return at least half + * the desired amount of documents + */ + public boolean getDynamicallyIncreaseMaxBucketsPerVisitor() { + return dynamicallyIncreaseMaxBucketsPerVisitor; + } + + /** @return Factor with which max buckets are dynamically increased each time */ + public float getDynamicMaxBucketsIncreaseFactor() { + return dynamicMaxBucketsIncreaseFactor; + } + + public DocumentProtocol.Priority getPriority() { + if (priority != null) { + return priority; + } else if (loadType != null) { + return loadType.getPriority(); + } else { + return DocumentProtocol.Priority.NORMAL_3; + } + } + + // Set functions + + /** Set the document selection expression */ + public void setDocumentSelection(String selection) { documentSelection = selection; } + + /** Set which visitor library is used for visiting in storage. DumpVisitor is most common implementation. */ + public void setVisitorLibrary(String library) { visitorLibrary = library; } + + /** Set maximum pending messages one storage visitor will have pending to this client before stalling, waiting for acks. */ + public void setMaxPending(int maxPending) { this.maxPending = maxPending; } + + /** Set the timeout for the visitor in milliseconds. */ + public void setTimeoutMs(long timeoutMs) { this.timeoutMs = timeoutMs; } + + /** Set from timestamp in microseconds. Documents put/updated before this timestamp will not be visited. */ + public void setFromTimestamp(long timestamp) { fromTimestamp = timestamp; } + + /** Set to timestamp in microseconds. Documents put/updated after this timestamp will not be visited. */ + public void setToTimestamp(long timestamp) { toTimestamp = timestamp; } + + /** Set whether to visit remove entries. That is, entries saying that some document has been removed. */ + public void visitRemoves(boolean visitRemoves) { this.visitRemoves = visitRemoves; } + + public void setVisitRemoves(boolean visitRemoves) { this.visitRemoves = visitRemoves; } + + public void setVisitHeadersOnly(boolean headersOnly) { this.fieldSet = headersOnly ? "[header]" : "[all]"; } + + /** Set field set to use. */ + public void fieldSet(String fieldSet) { this.fieldSet = fieldSet; } + public void setFieldSet(String fieldSet) { this.fieldSet = fieldSet; } + + /** Set whether to visit inconsistent buckets. */ + public void visitInconsistentBuckets(boolean visitInconsistentBuckets) { this.visitInconsistentBuckets = visitInconsistentBuckets; } + + public void setVisitInconsistentBuckets(boolean visitInconsistentBuckets) { this.visitInconsistentBuckets = visitInconsistentBuckets; } + + /** Set a visitor library specific parameter. */ + public void setLibraryParameter(String param, String value) { + libraryParameters.put(param, Utf8.toBytes(value)); + } + + /** Set a visitor library specific parameter. */ + public void setLibraryParameter(String param, byte [] value) { libraryParameters.put(param, value); } + + /** Set all visitor library specific parameters. */ + public void setLibraryParameters(Map<String, byte []> params) { libraryParameters = params; } + + /** Set progress token, which can be used to resume visitor. */ + public void setResumeToken(ProgressToken token) { resumeToken = token; } + + /** + * Set filename for reading/storing progress token. If the file exists and + * contains progress data, visitor should resume visiting from this point. + */ + public void setResumeFileName(String fileName) { resumeFileName = fileName; } + + /** Set address for the remote data handler. */ + public void setRemoteDataHandler(String remoteDataHandler) { this.remoteDataHandler = remoteDataHandler; localDataHandler = null; } + + /** Set local data handler. */ + public void setLocalDataHandler(VisitorDataHandler localDataHandler) { this.localDataHandler = localDataHandler; remoteDataHandler = null; } + + /** Set control handler. */ + public void setControlHandler(VisitorControlHandler controlHandler) { this.controlHandler = controlHandler; } + + /** Set the name of the storage cluster route to visit. Default is "storage/cluster.storage". */ + public void setRoute(String route) { setRoute(Route.parse(route)); } + + /** Set the route to visit. */ + public void setRoute(Route route) { visitRoute = route; } + + /** @return Returns the name of the storage cluster to visit. */ + // TODO: Document: Where is the default - does this ever return null, or does it return "storage" if input is null? + public Route getRoute() { return visitRoute; } + + /** Set the maximum number of documents to visit (max documents returned by the visitor) */ + public void setMaxFirstPassHits(long max) { maxFirstPassHits = max; } + + /** @return Returns the maximum number of documents to visit (max documents returned by the visitor) */ + public long getMaxFirstPassHits() { return maxFirstPassHits; } + + /** Set the maximum number of documents to visit (max documents returned by the visitor) */ + public void setMaxTotalHits(long max) { maxTotalHits = max; } + + /** @return Returns the maximum number of documents to visit (max documents returned by the visitor) */ + public long getMaxTotalHits() { return maxTotalHits; } + + public Set<BucketId> getBucketsToVisit() { return bucketsToVisit; } + + public void setBucketsToVisit(Set<BucketId> buckets) { bucketsToVisit = buckets; } + + public int getVisitorOrdering() { return visitorOrdering; } + + public void setVisitorOrdering(int order) { visitorOrdering = order; } + + public int getMaxBucketsPerVisitor() { return maxBucketsPerVisitor; } + + public void setMaxBucketsPerVisitor(int max) { maxBucketsPerVisitor = max; } + + public void setTraceLevel(int traceLevel) { this.traceLevel = traceLevel; } + + public int getTraceLevel() { return traceLevel; } + + public void setPriority(DocumentProtocol.Priority priority) { + this.priority = priority; + } + + public ThrottlePolicy getThrottlePolicy() { + return throttlePolicy; + } + + public void setThrottlePolicy(ThrottlePolicy policy) { + throttlePolicy = policy; + } + + public void setLoadType(LoadType loadType) { + this.loadType = loadType; + } + + public LoadType getLoadType() { + return loadType; + } + + public boolean skipBucketsOnFatalErrors() { return skipBucketsOnFatalErrors; } + + public void skipBucketsOnFatalErrors(boolean skipBucketsOnFatalErrors) { this.skipBucketsOnFatalErrors = skipBucketsOnFatalErrors; } + + /** + * Set whether or not max buckets per visitor value should be dynamically + * increased when using orderdoc and visitors do not return at least half + * the desired amount of documents + * + * @param dynamicallyIncreaseMaxBucketsPerVisitor whether or not to increase + */ + public void setDynamicallyIncreaseMaxBucketsPerVisitor(boolean dynamicallyIncreaseMaxBucketsPerVisitor) { + this.dynamicallyIncreaseMaxBucketsPerVisitor = dynamicallyIncreaseMaxBucketsPerVisitor; + } + + /** + * Set factor with which max buckets are dynamically increased each time + * @param dynamicMaxBucketsIncreaseFactor increase factor (must be 1 or more) + */ + public void setDynamicMaxBucketsIncreaseFactor(float dynamicMaxBucketsIncreaseFactor) { + this.dynamicMaxBucketsIncreaseFactor = dynamicMaxBucketsIncreaseFactor; + } + + // Inherit docs from Object + public String toString() { + StringBuffer sb = new StringBuffer(); + sb.append("VisitorParameters(\n") + .append(" Document selection: ").append(documentSelection).append('\n') + .append(" Visitor library: ").append(visitorLibrary).append('\n') + .append(" Max pending: ").append(maxPending).append('\n') + .append(" Timeout (ms): ").append(timeoutMs).append('\n') + .append(" Time period: ").append(fromTimestamp).append(" - ").append(toTimestamp).append('\n'); + if (visitRemoves) { + sb.append(" Visiting remove entries\n"); + } + if (visitInconsistentBuckets) { + sb.append(" Visiting inconsistent buckets\n"); + } + if (libraryParameters.size() > 0) { + sb.append(" Visitor library parameters:\n"); + for (Map.Entry<String, byte[]> e : libraryParameters.entrySet()) { + sb.append(" ").append(e.getKey()).append(" : "); + sb.append(Utf8.toString(e.getValue())).append('\n'); + } + } + sb.append(" Field set: ").append(fieldSet).append('\n'); + sb.append(" Route: ").append(visitRoute).append('\n'); + sb.append(" Weight: ").append(weight).append('\n'); + sb.append(" Max firstpass hits: ").append(maxFirstPassHits).append('\n'); + sb.append(" Max total hits: ").append(maxTotalHits).append('\n'); + sb.append(" Visitor ordering: ").append(visitorOrdering).append('\n'); + sb.append(" Max buckets: ").append(maxBucketsPerVisitor).append('\n'); + sb.append(" Priority: ").append(getPriority().toString()).append('\n'); + if (dynamicallyIncreaseMaxBucketsPerVisitor) { + sb.append(" Dynamically increasing max buckets per visitor\n"); + sb.append(" Increase factor: ") + .append(dynamicMaxBucketsIncreaseFactor) + .append('\n'); + } + sb.append(')'); + + return sb.toString(); + } +} diff --git a/documentapi/src/main/java/com/yahoo/documentapi/VisitorResponse.java b/documentapi/src/main/java/com/yahoo/documentapi/VisitorResponse.java new file mode 100644 index 00000000000..87a44caceb5 --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/VisitorResponse.java @@ -0,0 +1,24 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi; + +/** + * Common class for all visitor responses. All visitor responses have ack + * tokens that must be acked. + * + * @author <a href="mailto:humbe@yahoo-inc.com">Håkon Humberset</a> + */ +public class VisitorResponse { + private AckToken token; + + /** + * Creates visitor response containing an ack token. + * + * @param token the ack token + */ + public VisitorResponse(AckToken token) { + this.token = token; + } + + /** @return The ack token. */ + public AckToken getAckToken() { return token; } +} diff --git a/documentapi/src/main/java/com/yahoo/documentapi/VisitorSession.java b/documentapi/src/main/java/com/yahoo/documentapi/VisitorSession.java new file mode 100644 index 00000000000..55fd2790fe5 --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/VisitorSession.java @@ -0,0 +1,43 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi; + +import com.yahoo.messagebus.Trace; + +/** + * A session for tracking progress for and potentially receiving data from a + * visitor. + * + * @author <a href="mailto:thomasg@yahoo-inc.com">Thomas Gundersen</a> + */ +public interface VisitorSession extends VisitorControlSession { + /** + * Checks if visiting is done. + * + * @return True if visiting is done (either by error or success). + */ + public boolean isDone(); + + /** + * Retrieves the last progress token gotten for this visitor. + * + * @return The progress token. + */ + public ProgressToken getProgress(); + + /** + * Returns the tracing information so far about the visitor. + * + * @return Returns the trace. + */ + public Trace getTrace(); + + /** + * Waits until visiting is done, or the given timeout (in ms) expires. + * Will wait forever if timeout is 0. + * + * @param timeoutMs The maximum amount of milliseconds to wait. + * @return True if visiting is done (either by error or success). + * @throws InterruptedException If an interrupt signal was received while waiting. + */ + public boolean waitUntilDone(long timeoutMs) throws InterruptedException; +} diff --git a/documentapi/src/main/java/com/yahoo/documentapi/local/LocalAsyncSession.java b/documentapi/src/main/java/com/yahoo/documentapi/local/LocalAsyncSession.java new file mode 100644 index 00000000000..423c1d9f913 --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/local/LocalAsyncSession.java @@ -0,0 +1,137 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi.local; + +import com.yahoo.document.Document; +import com.yahoo.document.DocumentId; +import com.yahoo.document.DocumentOperation; +import com.yahoo.document.DocumentPut; +import com.yahoo.document.DocumentRemove; +import com.yahoo.document.DocumentUpdate; +import com.yahoo.documentapi.*; +import com.yahoo.documentapi.messagebus.protocol.DocumentProtocol; + +import java.util.LinkedList; +import java.util.List; +import java.util.Random; + +/** + * @author bratseth + */ +public class LocalAsyncSession implements AsyncSession { + + private final List<Response> responses = new LinkedList<>(); + private final ResponseHandler handler; + private final LocalDocumentAccess access; + private final SyncSession syncSession; + private long requestId = 0; + private Random random = new Random(); + + private synchronized long getNextRequestId() { + requestId++; + return requestId; + } + + public LocalAsyncSession(AsyncParameters params, LocalDocumentAccess access) { + this.access = access; + this.handler = params.getResponseHandler(); + random.setSeed(System.currentTimeMillis()); + syncSession = access.createSyncSession(new SyncParameters()); + } + + @Override + public double getCurrentWindowSize() { + return 1000; + } + + @Override + public Result put(Document document) { + return put(document, DocumentProtocol.Priority.NORMAL_3); + } + + @Override + public Result put(Document document, DocumentProtocol.Priority pri) { + long req = getNextRequestId(); + try { + syncSession.put(new DocumentPut(document), pri); + addResponse(new DocumentResponse(req)); + } catch (Exception e) { + addResponse(new DocumentResponse(req, document, e.getMessage(), false)); + } + return new Result(req); + } + + @Override + public Result get(DocumentId id) { + return get(id, false, DocumentProtocol.Priority.NORMAL_3); + } + + @Override + public Result get(DocumentId id, boolean headersOnly, DocumentProtocol.Priority pri) { + long req = getNextRequestId(); + try { + addResponse(new DocumentResponse(req, syncSession.get(id))); + } catch (Exception e) { + addResponse(new DocumentResponse(req, e.getMessage(), false)); + } + return new Result(req); + } + + @Override + public Result remove(DocumentId id) { + return remove(id, DocumentProtocol.Priority.NORMAL_3); + } + + @Override + public Result remove(DocumentId id, DocumentProtocol.Priority pri) { + long req = getNextRequestId(); + if (syncSession.remove(new DocumentRemove(id), pri)) { + addResponse(new RemoveResponse(req, true)); + } else { + addResponse(new DocumentIdResponse(req, id, "Document not found.", false)); + } + return new Result(req); + } + + @Override + public Result update(DocumentUpdate update) { + return update(update, DocumentProtocol.Priority.NORMAL_3); + } + + @Override + public Result update(DocumentUpdate update, DocumentProtocol.Priority pri) { + long req = getNextRequestId(); + if (syncSession.update(update, pri)) { + addResponse(new UpdateResponse(req, true)); + } else { + addResponse(new DocumentUpdateResponse(req, update, "Document not found.", false)); + } + return new Result(req); + } + + @Override + public Response getNext() { + if (responses.isEmpty()) { + return null; + } + int index = random.nextInt(responses.size()); + return responses.remove(index); + } + + @Override + public Response getNext(int timeout) { + return getNext(); + } + + @Override + public void destroy() { + // empty + } + + private void addResponse(Response response) { + if (handler != null) { + handler.handleResponse(response); + } else { + responses.add(response); + } + } +} diff --git a/documentapi/src/main/java/com/yahoo/documentapi/local/LocalDocumentAccess.java b/documentapi/src/main/java/com/yahoo/documentapi/local/LocalDocumentAccess.java new file mode 100644 index 00000000000..5ac77abd3ae --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/local/LocalDocumentAccess.java @@ -0,0 +1,60 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi.local; + +import com.yahoo.document.Document; +import com.yahoo.document.DocumentId; +import com.yahoo.documentapi.*; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * The main class of the local implementation of the document api + * + * @author <a href="mailto:bratseth@yahoo-inc.com">Jon Bratseth</a> + */ +public class LocalDocumentAccess extends DocumentAccess { + + Map<DocumentId, Document> documents = new LinkedHashMap<DocumentId, Document>(); + + public LocalDocumentAccess(DocumentAccessParams params) { + super(params); + } + + @Override + public void shutdown() { + if (documentTypeManagerConfig != null) { + documentTypeManagerConfig.close(); + } + } + + @Override + public SyncSession createSyncSession(SyncParameters parameters) { + return new LocalSyncSession(this); + } + + @Override + public AsyncSession createAsyncSession(AsyncParameters parameters) { + return new LocalAsyncSession(parameters, this); + } + + @Override + public VisitorSession createVisitorSession(VisitorParameters parameters) { + throw new UnsupportedOperationException("Not supported yet"); + } + + @Override + public VisitorDestinationSession createVisitorDestinationSession(VisitorDestinationParameters parameters) { + throw new UnsupportedOperationException("Not supported yet"); + } + + @Override + public SubscriptionSession createSubscription(SubscriptionParameters parameters) { + throw new UnsupportedOperationException("Not supported yet"); + } + + @Override + public SubscriptionSession openSubscription(SubscriptionParameters parameters) { + throw new UnsupportedOperationException("Not supported yet"); + } +} diff --git a/documentapi/src/main/java/com/yahoo/documentapi/local/LocalSyncSession.java b/documentapi/src/main/java/com/yahoo/documentapi/local/LocalSyncSession.java new file mode 100755 index 00000000000..966caa46969 --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/local/LocalSyncSession.java @@ -0,0 +1,103 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi.local;
+
+import com.yahoo.document.Document;
+import com.yahoo.document.DocumentId;
+import com.yahoo.document.DocumentPut;
+import com.yahoo.document.DocumentRemove;
+import com.yahoo.document.DocumentType;
+import com.yahoo.document.DocumentUpdate;
+import com.yahoo.document.TestAndSetCondition;
+import com.yahoo.documentapi.Response;
+import com.yahoo.documentapi.Result;
+import com.yahoo.documentapi.SyncSession;
+import com.yahoo.documentapi.messagebus.protocol.*;
+
+/**
+ * @author bratseth
+ */
+public class LocalSyncSession implements SyncSession {
+
+ private LocalDocumentAccess access;
+
+ public LocalSyncSession(LocalDocumentAccess access) {
+ this.access = access;
+ }
+
+ @Override
+ public void put(DocumentPut documentPut) {
+ if (documentPut.getCondition().isPresent()) {
+ throw new UnsupportedOperationException("test-and-set is not supported.");
+ }
+
+ access.documents.put(documentPut.getId(), documentPut.getDocument());
+ }
+
+ @Override
+ public void put(DocumentPut documentPut, DocumentProtocol.Priority priority) {
+ access.documents.put(documentPut.getId(), documentPut.getDocument());
+ }
+
+ @Override
+ public Document get(DocumentId id) {
+ return access.documents.get(id);
+ }
+
+ @Override
+ public Document get(DocumentId id, String fieldSet, DocumentProtocol.Priority pri) {
+ // FIXME: More than half the get() methods are deprecated, but they all
+ // call exactly the same method, including this one, throwing away most
+ // of the parameters
+ return access.documents.get(id);
+ }
+
+ @Override
+ public boolean remove(DocumentRemove documentRemove) {
+ if (documentRemove.getCondition().isPresent()) {
+ throw new UnsupportedOperationException("test-and-set is not supported.");
+ }
+ access.documents.remove(documentRemove.getId());
+ return true;
+ }
+
+ @Override
+ public boolean remove(DocumentRemove documentRemove, DocumentProtocol.Priority priority) {
+ return remove(documentRemove);
+ }
+
+ @Override
+ public boolean update(DocumentUpdate update) {
+ Document document = access.documents.get(update.getId());
+ if (document == null) {
+ return false;
+ }
+ update.applyTo(document);
+ return true;
+ }
+
+ @Override
+ public boolean update(DocumentUpdate update, DocumentProtocol.Priority pri) {
+ Document document = access.documents.get(update.getId());
+ if (document == null) {
+ return false;
+ }
+ update.applyTo(document);
+ return true;
+ }
+
+ @Override
+ public Response getNext() {
+ throw new UnsupportedOperationException("Queue not supported.");
+ }
+
+ @Override
+ public Response getNext(int timeout) {
+ throw new UnsupportedOperationException("Queue not supported.");
+ }
+
+ @Override
+ public void destroy() {
+ access = null;
+ }
+
+}
diff --git a/documentapi/src/main/java/com/yahoo/documentapi/local/package-info.java b/documentapi/src/main/java/com/yahoo/documentapi/local/package-info.java new file mode 100644 index 00000000000..edaf8ac3a94 --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/local/package-info.java @@ -0,0 +1,7 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +@ExportPackage +@PublicApi +package com.yahoo.documentapi.local; + +import com.yahoo.api.annotations.PublicApi; +import com.yahoo.osgi.annotation.ExportPackage; diff --git a/documentapi/src/main/java/com/yahoo/documentapi/messagebus/MessageBusAsyncSession.java b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/MessageBusAsyncSession.java new file mode 100644 index 00000000000..7b895845f3d --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/MessageBusAsyncSession.java @@ -0,0 +1,300 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi.messagebus; + +import com.yahoo.document.Document; +import com.yahoo.document.DocumentId; +import com.yahoo.document.DocumentPut; +import com.yahoo.document.DocumentType; +import com.yahoo.document.DocumentUpdate; +import com.yahoo.documentapi.*; +import com.yahoo.documentapi.Result; +import com.yahoo.documentapi.messagebus.protocol.*; +import com.yahoo.log.LogLevel; +import com.yahoo.messagebus.*; + +import java.lang.Error; +import java.util.Set; +import java.util.HashSet; +import java.util.Queue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; +import java.util.logging.Logger; + +/** + * An access session which wraps a messagebus source session sending document messages. + * The sessions are multithread safe. + * + * @author <a href="mailto:bratseth@yahoo-inc.com">Jon Bratseth</a> + * @author <a href="mailto:einarmr@yahoo-inc.com">Einar Rosenvinge</a> + */ +public class MessageBusAsyncSession implements MessageBusSession, AsyncSession { + + private static final Logger log = Logger.getLogger(MessageBusAsyncSession.class.getName()); + private final AtomicLong requestId = new AtomicLong(0); + private final BlockingQueue<Response> responses = new LinkedBlockingQueue<Response>(); + private final ThrottlePolicy throttlePolicy; + private final SourceSession session; + private String route; + private int traceLevel; + + /** + * Creates a new async session running on message bus logic. + * + * @param asyncParams Common asyncsession parameters, not used. + * @param bus The message bus on which to run. + * @param mbusParams Parameters concerning message bus configuration. + */ + MessageBusAsyncSession(AsyncParameters asyncParams, MessageBus bus, MessageBusParams mbusParams) { + this(asyncParams, bus, mbusParams, null); + } + + /** + * Creates a new async session running on message bus logic with a specified reply handler. + * + * @param asyncParams Common asyncsession parameters, not used. + * @param bus The message bus on which to run. + * @param mbusParams Parameters concerning message bus configuration. + * @param handler The external reply handler. + */ + MessageBusAsyncSession(AsyncParameters asyncParams, MessageBus bus, MessageBusParams mbusParams, + ReplyHandler handler) { + route = mbusParams.getRoute(); + traceLevel = mbusParams.getTraceLevel(); + throttlePolicy = mbusParams.getSourceSessionParams().getThrottlePolicy(); + if (handler == null) { + handler = new MyReplyHandler(asyncParams.getResponseHandler(), responses); + } + session = bus.createSourceSession(handler, mbusParams.getSourceSessionParams()); + } + + @Override + public Result put(Document document) { + return put(document, DocumentProtocol.Priority.NORMAL_3); + } + + @Override + public Result put(Document document, DocumentProtocol.Priority pri) { + PutDocumentMessage msg = new PutDocumentMessage(new DocumentPut(document)); + msg.setPriority(pri); + return send(msg); + } + + @Override + public Result get(DocumentId id) { + return get(id, false, DocumentProtocol.Priority.NORMAL_1); + } + + @Override + public Result get(DocumentId id, boolean headersOnly, DocumentProtocol.Priority pri) { + GetDocumentMessage msg = new GetDocumentMessage(id, headersOnly ? "[header]" : "[all]"); + msg.setPriority(pri); + return send(msg); + } + + @Override + public Result remove(DocumentId id) { + return remove(id, DocumentProtocol.Priority.NORMAL_2); + } + + @Override + public Result remove(DocumentId id, DocumentProtocol.Priority pri) { + RemoveDocumentMessage msg = new RemoveDocumentMessage(id); + msg.setPriority(pri); + return send(msg); + } + + @Override + public Result update(DocumentUpdate update) { + return update(update, DocumentProtocol.Priority.NORMAL_2); + } + + @Override + public Result update(DocumentUpdate update, DocumentProtocol.Priority pri) { + UpdateDocumentMessage msg = new UpdateDocumentMessage(update); + msg.setPriority(pri); + return send(msg); + } + + /** + * A convenience method for assigning the internal trace level and route string to a message before sending it + * through the internal mbus session object. + * + * @param msg The message to send. + * @return The document api result object. + */ + public Result send(Message msg) { + try { + long reqId = requestId.incrementAndGet(); + msg.setContext(reqId); + msg.getTrace().setLevel(traceLevel); + if (route != null) { + return toResult(reqId, session.send(msg, route, true)); + } else { + return toResult(reqId, session.send(msg)); + } + } catch (Exception e) { + return new Result(Result.ResultType.FATAL_ERROR, new Error(e.getMessage(), e)); + } + } + + @Override + public Response getNext() { + return responses.poll(); + } + + @Override + public Response getNext(int timeoutMilliseconds) throws InterruptedException { + return responses.poll(timeoutMilliseconds, TimeUnit.MILLISECONDS); + } + + @Override + public void destroy() { + session.destroy(); + } + + @Override + public String getRoute() { + return route; + } + + @Override + public void setRoute(String route) { + this.route = route; + } + + @Override + public int getTraceLevel() { + return traceLevel; + } + + @Override + public void setTraceLevel(int traceLevel) { + this.traceLevel = traceLevel; + } + + @Override + public double getCurrentWindowSize() { + if (throttlePolicy instanceof StaticThrottlePolicy) { + return ((StaticThrottlePolicy)throttlePolicy).getMaxPendingCount(); + } + return 0; + } + + /** + * Returns a concatenated error string from the errors contained in a reply. + * + * @param reply The reply whose errors to concatenate. + * @return The error string. + */ + static String getErrorMessage(Reply reply) { + if (!reply.hasErrors()) { + return null; + } + StringBuilder errors = new StringBuilder(); + for (int i = 0; i < reply.getNumErrors(); ++i) { + errors.append(reply.getError(i)).append(" "); + } + return errors.toString(); + } + + static Set<Integer> + getErrorCodes(Reply reply) { + Set<Integer> errorCodes = new HashSet<>(); + for (int i = 0; i < reply.getNumErrors(); ++i) { + errorCodes.add(reply.getError(i).getCode()); + } + return errorCodes; + } + + private static Result toResult(long reqId, com.yahoo.messagebus.Result mbusResult) { + if (mbusResult.isAccepted()) { + return new Result(reqId); + } else if (mbusResult.getError().getCode() == ErrorCode.SEND_QUEUE_FULL) { + return new Result(Result.ResultType.TRANSIENT_ERROR, + new Error(mbusResult.getError().getMessage() + " (" + mbusResult.getError().getCode() + ")")); + } else { + return new Result(Result.ResultType.FATAL_ERROR, + new Error(mbusResult.getError().getMessage() + " (" + mbusResult.getError().getCode() + ")")); + } + } + + private static Response toResponse(Reply reply) { + long reqId = (Long)reply.getContext(); + return reply.hasErrors() ? toError(reply, reqId) : toSuccess(reply, reqId); + } + + private static Response toError(Reply reply, long reqId) { + Message msg = reply.getMessage(); + String err = getErrorMessage(reply); + switch (msg.getType()) { + case DocumentProtocol.MESSAGE_PUTDOCUMENT: + return new DocumentResponse(reqId, ((PutDocumentMessage)msg).getDocumentPut().getDocument(), err, false); + case DocumentProtocol.MESSAGE_UPDATEDOCUMENT: + return new DocumentUpdateResponse(reqId, ((UpdateDocumentMessage)msg).getDocumentUpdate(), err, false); + case DocumentProtocol.MESSAGE_REMOVEDOCUMENT: + return new DocumentIdResponse(reqId, ((RemoveDocumentMessage)msg).getDocumentId(), err, false); + case DocumentProtocol.MESSAGE_GETDOCUMENT: + return new DocumentIdResponse(reqId, ((GetDocumentMessage)msg).getDocumentId(), err, false); + default: + return new Response(reqId, err, false); + } + } + + @SuppressWarnings("deprecation") + private static Response toSuccess(Reply reply, long reqId) { + switch (reply.getType()) { + case DocumentProtocol.REPLY_GETDOCUMENT: + GetDocumentReply docReply = ((GetDocumentReply) reply); + Document getDoc = docReply.getDocument(); + if (getDoc != null) { + getDoc.setLastModified(docReply.getLastModified()); + } + return new DocumentResponse(reqId, getDoc); + case DocumentProtocol.REPLY_REMOVEDOCUMENT: + return new RemoveResponse(reqId, ((RemoveDocumentReply)reply).wasFound()); + case DocumentProtocol.REPLY_UPDATEDOCUMENT: + return new UpdateResponse(reqId, ((UpdateDocumentReply)reply).wasFound()); + case DocumentProtocol.REPLY_PUTDOCUMENT: + break; + default: + return new Response(reqId); + } + Message msg = reply.getMessage(); + switch (msg.getType()) { + case DocumentProtocol.MESSAGE_PUTDOCUMENT: + return new DocumentResponse(reqId, ((PutDocumentMessage)msg).getDocumentPut().getDocument()); + case DocumentProtocol.MESSAGE_REMOVEDOCUMENT: + return new DocumentIdResponse(reqId, ((RemoveDocumentMessage)msg).getDocumentId()); + case DocumentProtocol.MESSAGE_UPDATEDOCUMENT: + return new DocumentUpdateResponse(reqId, ((UpdateDocumentMessage)msg).getDocumentUpdate()); + default: + return new Response(reqId); + } + } + + private static class MyReplyHandler implements ReplyHandler { + + final ResponseHandler handler; + final Queue<Response> queue; + + MyReplyHandler(ResponseHandler handler, Queue<Response> queue) { + this.handler = handler; + this.queue = queue; + } + + @Override + public void handleReply(Reply reply) { + if (reply.getTrace().getLevel() > 0) { + log.log(LogLevel.INFO, reply.getTrace().toString()); + } + Response response = toResponse(reply); + if (handler != null) { + handler.handleResponse(response); + } else { + queue.add(response); + } + } + } +} diff --git a/documentapi/src/main/java/com/yahoo/documentapi/messagebus/MessageBusDocumentAccess.java b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/MessageBusDocumentAccess.java new file mode 100644 index 00000000000..818bc204784 --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/MessageBusDocumentAccess.java @@ -0,0 +1,134 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi.messagebus; + +import com.yahoo.concurrent.DaemonThreadFactory; +import com.yahoo.concurrent.ThreadFactoryFactory; +import com.yahoo.document.select.parser.ParseException; +import com.yahoo.documentapi.*; +import com.yahoo.documentapi.messagebus.protocol.DocumentProtocol; +import com.yahoo.messagebus.MessageBus; +import com.yahoo.messagebus.RPCMessageBus; +import com.yahoo.messagebus.network.Network; +import com.yahoo.messagebus.routing.RoutingTable; + +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; + +/** + * This class implements the {@link DocumentAccess} interface using message bus for communication. + * + * @author <a href="mailto:einarmr@yahoo-inc.com">Einar Rosenvinge</a> + * @author bratseth + */ +public class MessageBusDocumentAccess extends DocumentAccess { + + private final RPCMessageBus bus; + private final MessageBusParams params; + // TODO: make pool size configurable? ScheduledExecutorService is not dynamic + private final ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool( + Runtime.getRuntime().availableProcessors(), ThreadFactoryFactory.getDaemonThreadFactory("mbus.access.scheduler")); + + /** + * Creates a new document access using default values for all parameters. + */ + public MessageBusDocumentAccess() { + this(new MessageBusParams()); + } + + /** + * Creates a new document access using the supplied parameters. + * + * @param params All parameters for construction. + */ + public MessageBusDocumentAccess(MessageBusParams params) { + super(params); + this.params = params; + try { + com.yahoo.messagebus.MessageBusParams mbusParams = new com.yahoo.messagebus.MessageBusParams(params.getMessageBusParams()); + mbusParams.addProtocol(new DocumentProtocol(getDocumentTypeManager(), params.getProtocolConfigId(), params.getLoadTypes())); + bus = new RPCMessageBus(mbusParams, + params.getRPCNetworkParams(), + params.getRoutingConfigId()); + } + catch (Exception e) { + throw new DocumentAccessException(e); + } + } + + @Override + public void shutdown() { + bus.destroy(); + if (documentTypeManagerConfig != null) { + documentTypeManagerConfig.close(); + } + scheduledExecutorService.shutdownNow(); + } + + @Override + public MessageBusSyncSession createSyncSession(SyncParameters parameters) { + return new MessageBusSyncSession(parameters, bus.getMessageBus(), this.params); + } + + @Override + public MessageBusAsyncSession createAsyncSession(AsyncParameters parameters) { + return new MessageBusAsyncSession(parameters, bus.getMessageBus(), this.params); + } + + @Override + public MessageBusVisitorSession createVisitorSession(VisitorParameters params) throws ParseException, IllegalArgumentException { + MessageBusVisitorSession.AsyncTaskExecutor executor = new MessageBusVisitorSession.ThreadAsyncTaskExecutor(scheduledExecutorService); + MessageBusVisitorSession.MessageBusSenderFactory senderFactory = new MessageBusVisitorSession.MessageBusSenderFactory(bus.getMessageBus()); + MessageBusVisitorSession.MessageBusReceiverFactory receiverFactory = new MessageBusVisitorSession.MessageBusReceiverFactory(bus.getMessageBus()); + RoutingTable table = bus.getMessageBus().getRoutingTable(DocumentProtocol.NAME); + + MessageBusVisitorSession session = new MessageBusVisitorSession(params, executor, senderFactory, receiverFactory, table); + session.start(); + return session; + } + + @Override + public MessageBusVisitorDestinationSession createVisitorDestinationSession(VisitorDestinationParameters params) { + return new MessageBusVisitorDestinationSession(params, bus.getMessageBus()); + } + + @Override + public SubscriptionSession createSubscription(SubscriptionParameters parameters) { + throw new UnsupportedOperationException("Subscriptions not supported."); + } + + @Override + public SubscriptionSession openSubscription(SubscriptionParameters parameters) { + throw new UnsupportedOperationException("Subscriptions not supported."); + } + + /** + * Returns the internal message bus object so that clients can use it directly. + * + * @return The internal message bus. + */ + public MessageBus getMessageBus() { + return bus.getMessageBus(); + } + + /** + * Returns the network layer of the internal message bus object so that clients can use it directly. This may seem + * abit arbitrary, but the fact is that the RPCNetwork actually implements the IMirror API as well as exposing the + * SystemState object. + * + * @return The network layer. + */ + public Network getNetwork() { + return bus.getRPCNetwork(); + } + + /** + * Returns the parameter object that controls the underlying message bus. Changes to these parameters do not affect + * previously created sessions. + * + * @return The parameter object. + */ + public MessageBusParams getParams() { + return params; + } + +} diff --git a/documentapi/src/main/java/com/yahoo/documentapi/messagebus/MessageBusParams.java b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/MessageBusParams.java new file mode 100755 index 00000000000..ecf6927f20c --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/MessageBusParams.java @@ -0,0 +1,193 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi.messagebus; + +import com.yahoo.documentapi.DocumentAccessParams; +import com.yahoo.documentapi.messagebus.loadtypes.LoadTypeSet; +import com.yahoo.messagebus.SourceSessionParams; +import com.yahoo.messagebus.network.rpc.RPCNetworkParams; + +/** + * @author <a href="mailto:einarmr@yahoo-inc.com">Einar M R Rosenvinge</a> + */ +public class MessageBusParams extends DocumentAccessParams { + + private String routingConfigId = null; + private String protocolConfigId = null; + private String route = "route:default"; + private int traceLevel = 0; + private RPCNetworkParams rpcNetworkParams = new RPCNetworkParams(); + private com.yahoo.messagebus.MessageBusParams mbusParams = new com.yahoo.messagebus.MessageBusParams(); + private SourceSessionParams sourceSessionParams = new SourceSessionParams(); + private LoadTypeSet loadTypes; + + public MessageBusParams() { + this(new LoadTypeSet()); + } + + public MessageBusParams(LoadTypeSet loadTypes) { + this.loadTypes = loadTypes; + } + + /** + * + * @return Returns the set of load types accepted by this Vespa installation + */ + public LoadTypeSet getLoadTypes() { + return loadTypes; + } + + /** + * Returns the id to resolve to routing config. + * + * @return The config id. + */ + public String getRoutingConfigId() { + return routingConfigId; + } + + /** + * Sets the id to resolve to routing config. This has a proper default value that holds for Vespa applications, and + * can therefore be left unset. + * + * @param configId The config id. + * @return This object for chaining. + */ + public MessageBusParams setRoutingConfigId(String configId) { + routingConfigId = configId; + return this; + } + + /** + * Returns the id to resolve to protocol config. + * + * @return The config id. + */ + public String getProtocolConfigId() { + return protocolConfigId; + } + + /** + * Sets the id to resolve to protocol config. This has a proper default value that holds for Vespa applications, + * and can therefore be left usnet. + * + * @param configId The config id. + * @return This, to allow chaining. + */ + public MessageBusParams setProtocolConfigId(String configId) { + protocolConfigId = configId; + return this; + } + + /** + * Sets the name of the route to send appropriate requests to. This is a convenience method for prefixing a route + * with "route:", and using {@link #setRoute} instead. + * + * @param routeName The route name. + * @return This object for chaining. + */ + public MessageBusParams setRouteName(String routeName) { + return setRoute("route:" + routeName); + } + + /** + * Sets the route string to send all requests to. This string will be parsed as a route string, so setting a route + * name directly will not necessarily have the intended consequences. Use "route:<routename>" syntax for route + * names, or the convenience method {@link #setRouteName} for this. + * + * @param route The route string. + * @return This object for chaining. + */ + public MessageBusParams setRoute(String route) { + this.route = route; + return this; + } + + /** + * Returns the route string that all requests will be sent to. + * + * @return The route string. + */ + public String getRoute() { + return route; + } + + /** + * Returns the trace level to use when sending. + * + * @return The trace level. + */ + public int getTraceLevel() { + return traceLevel; + } + + /** + * Sets the trace level to use when sending. + * + * @param traceLevel The trace level. + * @return This object for chaining. + */ + public MessageBusParams setTraceLevel(int traceLevel) { + this.traceLevel = traceLevel; + return this; + } + + /** + * Returns the params object used to instantiate the rpc network layer for message bus. + * + * @return The params object. + */ + public RPCNetworkParams getRPCNetworkParams() { + return rpcNetworkParams; + } + + /** + * Sets the params object used to instantiate the rpc network layer for message bus. + * + * @param params The params object. + * @return This object for chaining. + */ + public MessageBusParams setRPCNetworkParams(RPCNetworkParams params) { + rpcNetworkParams = new RPCNetworkParams(params); + return this; + } + + /** + * Returns the params object used to instantiate the message bus. + * + * @return The params object. + */ + public com.yahoo.messagebus.MessageBusParams getMessageBusParams() { + return mbusParams; + } + + /** + * Sets the params object used to instantiate the message bus. + * + * @param params The params object. + * @return This object for chaining. + */ + public MessageBusParams setMessageBusParams(com.yahoo.messagebus.MessageBusParams params) { + mbusParams = new com.yahoo.messagebus.MessageBusParams(params); + return this; + } + + /** + * Returns a reference to the extended source session params object. + * + * @return The params object. + */ + public SourceSessionParams getSourceSessionParams() { + return sourceSessionParams; + } + + /** + * Sets the extended source session params. + * + * @param params The params object. + * @return This object for chaining. + */ + public MessageBusParams setSourceSessionParams(SourceSessionParams params) { + sourceSessionParams = new SourceSessionParams(params); + return this; + } +} diff --git a/documentapi/src/main/java/com/yahoo/documentapi/messagebus/MessageBusSession.java b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/MessageBusSession.java new file mode 100755 index 00000000000..7f5544a93ce --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/MessageBusSession.java @@ -0,0 +1,38 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi.messagebus;
+
+/**
+ * This class defines a common interface for message bus sessions.
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public interface MessageBusSession {
+
+ /**
+ * Returns the route to send all messages to when sending through this session.
+ *
+ * @return The route string.
+ */
+ public String getRoute();
+
+ /**
+ * Sets the route to send all messages to when sending through this session.
+ *
+ * @param route The route string.
+ */
+ public void setRoute(String route);
+
+ /**
+ * Returns the trace level used when sending messages through this session.
+ *
+ * @return The trace level.
+ */
+ public int getTraceLevel();
+
+ /**
+ * Sets the trace level used when sending messages through this session.
+ *
+ * @param traceLevel The trace level to set.
+ */
+ public void setTraceLevel(int traceLevel);
+}
diff --git a/documentapi/src/main/java/com/yahoo/documentapi/messagebus/MessageBusSyncSession.java b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/MessageBusSyncSession.java new file mode 100755 index 00000000000..5be94564556 --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/MessageBusSyncSession.java @@ -0,0 +1,225 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.documentapi.messagebus;
+
+import com.yahoo.document.Document;
+import com.yahoo.document.DocumentId;
+import com.yahoo.document.DocumentPut;
+import com.yahoo.document.DocumentRemove;
+import com.yahoo.document.DocumentUpdate;
+import com.yahoo.documentapi.AsyncParameters;
+import com.yahoo.documentapi.DocumentAccessException;
+import com.yahoo.documentapi.Response;
+import com.yahoo.documentapi.Result;
+import com.yahoo.documentapi.SyncParameters;
+import com.yahoo.documentapi.SyncSession;
+import com.yahoo.documentapi.messagebus.protocol.*;
+import com.yahoo.documentapi.messagebus.protocol.DocumentProtocol;
+import com.yahoo.messagebus.Message;
+import com.yahoo.messagebus.MessageBus;
+import com.yahoo.messagebus.Reply;
+import com.yahoo.messagebus.ReplyHandler;
+
+/**
+ * An implementation of the SyncSession interface running over message bus.
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class MessageBusSyncSession implements MessageBusSession, SyncSession, ReplyHandler {
+
+ private MessageBusAsyncSession session;
+
+ /**
+ * Creates a new sync session running on message bus logic.
+ *
+ * @param syncParams Common syncsession parameters, not used.
+ * @param bus The message bus on which to run.
+ * @param mbusParams Parameters concerning message bus configuration.
+ */
+ MessageBusSyncSession(SyncParameters syncParams, MessageBus bus, MessageBusParams mbusParams) {
+ session = new MessageBusAsyncSession(new AsyncParameters(), bus, mbusParams, this);
+ }
+
+ @Override
+ public void handleReply(Reply reply) {
+ if (reply.getContext() instanceof RequestMonitor) {
+ ((RequestMonitor)reply.getContext()).replied(reply);
+ } else {
+ ReplyHandler handler = reply.getCallStack().pop(reply);
+ handler.handleReply(reply); // not there yet
+ }
+ }
+
+ @Override
+ public Response getNext() {
+ throw new UnsupportedOperationException("Queue not supported.");
+ }
+
+ @Override
+ public Response getNext(int timeout) {
+ throw new UnsupportedOperationException("Queue not supported.");
+ }
+
+ @Override
+ public void destroy() {
+ session.destroy();
+ }
+
+ /**
+ * Perform a synchronous sending of a message. This method block until the message is successfuly sent and a
+ * corresponding reply has been received.
+ *
+ * @param msg The message to send.
+ * @return The reply received.
+ */
+ public Reply syncSend(Message msg) {
+ try {
+ RequestMonitor monitor = new RequestMonitor();
+ msg.setContext(monitor);
+ msg.pushHandler(this); // store monitor
+ Result result = null;
+ while (result == null || result.getType() == Result.ResultType.TRANSIENT_ERROR) {
+ result = session.send(msg);
+ if (result != null && result.isSuccess()) {
+ break;
+ }
+ Thread.sleep(100);
+ }
+ if (!result.isSuccess()) {
+ throw new DocumentAccessException(result.getError().toString());
+ }
+ return monitor.waitForReply();
+ } catch (InterruptedException e) {
+ throw new DocumentAccessException(e);
+ }
+ }
+
+ @Override
+ public void put(DocumentPut documentPut) {
+ put(documentPut, DocumentProtocol.Priority.NORMAL_3);
+ }
+
+ @Override
+ public void put(DocumentPut documentPut, DocumentProtocol.Priority priority) {
+ PutDocumentMessage msg = new PutDocumentMessage(documentPut);
+ msg.setPriority(priority);
+ syncSendPutDocumentMessage(msg);
+ }
+
+ @Override
+ public Document get(DocumentId id) {
+ return get(id, "[all]", DocumentProtocol.Priority.NORMAL_1);
+ }
+
+ @Override
+ public Document get(DocumentId id, String fieldSet, DocumentProtocol.Priority pri) {
+ GetDocumentMessage msg = new GetDocumentMessage(id, fieldSet);
+ msg.setPriority(pri);
+
+ Reply reply = syncSend(msg);
+ if (reply.hasErrors()) {
+ throw new DocumentAccessException(MessageBusAsyncSession.getErrorMessage(reply));
+ }
+ if (reply.getType() != DocumentProtocol.REPLY_GETDOCUMENT) {
+ throw new DocumentAccessException("Received unknown response: " + reply);
+ }
+ GetDocumentReply docReply = ((GetDocumentReply)reply);
+ Document doc = docReply.getDocument();
+ if (doc != null) {
+ doc.setLastModified(docReply.getLastModified());
+ }
+ return doc;
+ }
+
+ @Override
+ public boolean remove(DocumentRemove documentRemove) {
+ RemoveDocumentMessage msg = new RemoveDocumentMessage(documentRemove.getId());
+ msg.setCondition(documentRemove.getCondition());
+ return remove(msg);
+ }
+
+ @Override
+ public boolean remove(DocumentRemove documentRemove, DocumentProtocol.Priority pri) {
+ RemoveDocumentMessage msg = new RemoveDocumentMessage(documentRemove.getId());
+ msg.setPriority(pri);
+ msg.setCondition(documentRemove.getCondition());
+ return remove(msg);
+ }
+
+ private boolean remove(RemoveDocumentMessage msg) {
+ Reply reply = syncSend(msg);
+ if (reply.hasErrors()) {
+ throw new DocumentAccessException(MessageBusAsyncSession.getErrorMessage(reply));
+ }
+ if (reply.getType() != DocumentProtocol.REPLY_REMOVEDOCUMENT) {
+ throw new DocumentAccessException("Received unknown response: " + reply);
+ }
+ return ((RemoveDocumentReply)reply).wasFound();
+ }
+
+ @Override
+ public boolean update(DocumentUpdate update) {
+ return update(update, DocumentProtocol.Priority.NORMAL_2);
+ }
+
+ @Override
+ public boolean update(DocumentUpdate update, DocumentProtocol.Priority pri) {
+ UpdateDocumentMessage msg = new UpdateDocumentMessage(update);
+ msg.setPriority(pri);
+ Reply reply = syncSend(msg);
+ if (reply.hasErrors()) {
+ throw new DocumentAccessException(MessageBusAsyncSession.getErrorMessage(reply),
+ MessageBusAsyncSession.getErrorCodes(reply));
+ }
+ if (reply.getType() != DocumentProtocol.REPLY_UPDATEDOCUMENT) {
+ throw new DocumentAccessException("Received unknown response: " + reply);
+ }
+ return ((UpdateDocumentReply)reply).wasFound();
+ }
+
+ @Override
+ public String getRoute() {
+ return session.getRoute();
+ }
+
+ @Override
+ public void setRoute(String route) {
+ session.setRoute(route);
+ }
+
+ @Override
+ public int getTraceLevel() {
+ return session.getTraceLevel();
+ }
+
+ @Override
+ public void setTraceLevel(int traceLevel) {
+ session.setTraceLevel(traceLevel);
+ }
+
+ /**
+ * This class implements a monitor for waiting for a reply to arrive.
+ */
+ static class RequestMonitor {
+ private Reply reply = null;
+
+ synchronized Reply waitForReply() throws InterruptedException {
+ while (reply == null) {
+ wait();
+ }
+ return reply;
+ }
+
+ synchronized void replied(Reply reply) {
+ this.reply = reply;
+ notify();
+ }
+ }
+
+ private void syncSendPutDocumentMessage(PutDocumentMessage putDocumentMessage) {
+ Reply reply = syncSend(putDocumentMessage);
+ if (reply.hasErrors()) {
+ throw new DocumentAccessException(MessageBusAsyncSession.getErrorMessage(reply),
+ MessageBusAsyncSession.getErrorCodes(reply));
+ }
+ }
+}
diff --git a/documentapi/src/main/java/com/yahoo/documentapi/messagebus/MessageBusVisitorDestinationSession.java b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/MessageBusVisitorDestinationSession.java new file mode 100644 index 00000000000..d12ee0a35df --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/MessageBusVisitorDestinationSession.java @@ -0,0 +1,83 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi.messagebus; + +import com.yahoo.documentapi.AckToken; +import com.yahoo.documentapi.VisitorDestinationParameters; +import com.yahoo.documentapi.VisitorDestinationSession; +import com.yahoo.documentapi.VisitorResponse; +import com.yahoo.documentapi.messagebus.protocol.*; +import com.yahoo.log.LogLevel; +import com.yahoo.messagebus.*; + +import java.util.logging.Logger; + +/** + * A visitor destination session for receiving data from a visitor using a + * messagebus destination session. The default behaviour of the visitor session + * is to control visiting and receive the data. As an alternative, you may set + * up one or more visitor destination sessions and tell the visitor to send + * data to the remote destination(s). This is convenient if you want to receive + * data decoupled from controlling the visitor, but also to avoid a single data + * destination becoming a bottleneck. + * <p> + * Create the visitor destination session by calling the + * <code>MessageBusDocumentAccess.createVisitorDestinationSession</code> + * method. The visitor must be started by calling the + * <code>MessageBusDocumentAccess.createVisitorSession</code> method and + * progress tracked through the resulting visitor session. + * + * @author <a href="mailto:thomasg@yahoo-inc.com">Thomas Gundersen</a> + */ +public class MessageBusVisitorDestinationSession implements VisitorDestinationSession,MessageHandler +{ + private static final Logger log = Logger.getLogger(MessageBusVisitorDestinationSession.class.getName()); + + private DestinationSession session; + private VisitorDestinationParameters params; + + /** + * Creates a message bus visitor destination session. + * + * @param params the parameters for the visitor destination session + * @param bus the message bus to use + */ + public MessageBusVisitorDestinationSession(VisitorDestinationParameters params, MessageBus bus) { + this.params = params; + session = bus.createDestinationSession(params.getSessionName(), true, this); + params.getDataHandler().setSession(this); + } + + public void handleMessage(Message message) { + Reply reply = ((DocumentMessage)message).createReply(); + message.swapState(reply); + + params.getDataHandler().onMessage(message, new AckToken(reply)); + } + + public void ack(AckToken token) { + try { + log.log(LogLevel.DEBUG, "Sending ack " + token.ackObject); + session.reply((Reply) token.ackObject); + } catch (Exception e) { + e.printStackTrace(); + } + } + + public void destroy() { + session.destroy(); + session = null; + } + + public void abort() { + destroy(); + } + + public VisitorResponse getNext() { + return params.getDataHandler().getNext(); + } + + public VisitorResponse getNext(int timeoutMilliseconds) throws InterruptedException { + return params.getDataHandler().getNext(timeoutMilliseconds); + } + +} diff --git a/documentapi/src/main/java/com/yahoo/documentapi/messagebus/MessageBusVisitorSession.java b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/MessageBusVisitorSession.java new file mode 100755 index 00000000000..a784ccd61e4 --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/MessageBusVisitorSession.java @@ -0,0 +1,1071 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi.messagebus; + +import com.yahoo.document.BucketId; +import com.yahoo.document.BucketIdFactory; +import com.yahoo.document.select.parser.ParseException; +import com.yahoo.documentapi.*; +import com.yahoo.documentapi.messagebus.protocol.*; +import com.yahoo.log.LogLevel; +import com.yahoo.messagebus.*; +import com.yahoo.messagebus.Error; +import com.yahoo.messagebus.Result; +import com.yahoo.messagebus.routing.RoutingTable; +import com.yahoo.vdslib.VisitorStatistics; +import com.yahoo.vdslib.state.ClusterState; + +import java.util.Arrays; +import java.util.concurrent.*; +import java.util.logging.Logger; + +/** + * <p> + * A visitor session for tracking progress for and potentially receiving data from + * a visitor using a MessageBus source and destination session. The source session + * is used to initiate visiting by sending create visitor messages to storage and + * the destination session is used for receiving progress. If the visitor is not + * set up to send data to a remote destination, data will also be received through + * the destination session. + * </p> + * <p> + * Create the visitor session by calling the + * <code>DocumentAccess.createVisitorSession</code> method. + * </p> + */ +public class MessageBusVisitorSession implements VisitorSession { + /** + * Abstract away notion of source session into a generic Sender + * interface to allow easy mocking. + */ + public static interface Sender { + public Result send(Message msg); + public int getPendingCount(); + public void destroy(); + } + + public static interface SenderFactory { + public Sender createSender(ReplyHandler replyHandler, VisitorParameters visitorParameters); + } + + /** + * Abstract away notion of destination session into a generic Receiver + * interface to allow easy mocking. + * The implementation must be thread safe since reply() can be invoked + * from an arbitrary thread. + */ + public static interface Receiver { + public void reply(Reply reply); + public void destroy(); + /** + * Get connection spec that can be used by other clients to send + * messages to this Receiver. + * @return connection spec + */ + public String getConnectionSpec(); + } + + public static interface ReceiverFactory { + public Receiver createReceiver(MessageHandler messageHandler, String sessionName); + } + + public static interface AsyncTaskExecutor { + public void submitTask(Runnable event); + public void scheduleTask(Runnable event, long delay, TimeUnit unit); + } + + public static class VisitingProgress { + private final VisitorIterator iterator; + private final ProgressToken token; + + public VisitingProgress(VisitorIterator iterator, ProgressToken token) { + this.iterator = iterator; + this.token = token; + } + + public VisitorIterator getIterator() { + return iterator; + } + + public ProgressToken getToken() { + return token; + } + } + + public enum State { + NOT_STARTED(false), + WORKING(false), + COMPLETED(false), + ABORTED(true), + FAILED(true), + TIMED_OUT(true); + + private final boolean failure; + private State(boolean failure) { + this.failure = failure; + } + + public boolean isFailure() { + return failure; + } + } + + public class StateDescription { + private final State state; + private final String description; + + public StateDescription(State state, String description) { + this.state = state; + this.description = description; + } + + public StateDescription(State state) { + this.state = state; + this.description = ""; + } + + public State getState() { + return state; + } + + public String getDescription() { + return description; + } + + VisitorControlHandler.CompletionCode toCompletionCode() { + switch (state) { + case COMPLETED: return VisitorControlHandler.CompletionCode.SUCCESS; + case ABORTED: return VisitorControlHandler.CompletionCode.ABORTED; + case FAILED: return VisitorControlHandler.CompletionCode.FAILURE; + case TIMED_OUT: return VisitorControlHandler.CompletionCode.TIMEOUT; + default: + throw new IllegalStateException("Current state did not have a valid value: " + state); + } + } + + public boolean failed() { + return state.isFailure(); + } + + public String toString() { + return state + ": " + description; + } + } + + /** + * Message bus implementations of interfaces + */ + + public static class MessageBusSender implements Sender { + private final SourceSession sourceSession; + + public MessageBusSender(SourceSession sourceSession) { + this.sourceSession = sourceSession; + } + + @Override + public Result send(Message msg) { + return sourceSession.send(msg); + } + + @Override + public int getPendingCount() { + return sourceSession.getPendingCount(); + } + + @Override + public void destroy() { + sourceSession.destroy(); + } + } + + public static class MessageBusSenderFactory implements SenderFactory { + private final MessageBus messageBus; + + public MessageBusSenderFactory(MessageBus messageBus) { + this.messageBus = messageBus; + } + + private SourceSessionParams createSourceSessionParams(VisitorParameters visitorParameters) { + SourceSessionParams sourceParams = new SourceSessionParams(); + + if (visitorParameters.getThrottlePolicy() != null) { + sourceParams.setThrottlePolicy(visitorParameters.getThrottlePolicy()); + } else { + sourceParams.setThrottlePolicy(new DynamicThrottlePolicy()); + } + + return sourceParams; + } + + @Override + public Sender createSender(ReplyHandler replyHandler, VisitorParameters visitorParameters) { + messageBus.setMaxPendingCount(0); + SourceSessionParams sessionParams = createSourceSessionParams(visitorParameters); + return new MessageBusSender(messageBus.createSourceSession(replyHandler, sessionParams)); + } + } + + public static class MessageBusReceiver implements Receiver { + private final DestinationSession destinationSession; + + public MessageBusReceiver(DestinationSession destinationSession) { + this.destinationSession = destinationSession; + } + + @Override + public void reply(Reply reply) { + destinationSession.reply(reply); + } + + @Override + public void destroy() { + destinationSession.destroy(); + } + + @Override + public String getConnectionSpec() { + return destinationSession.getConnectionSpec(); + } + } + + public static class MessageBusReceiverFactory implements ReceiverFactory { + private final MessageBus messageBus; + + public MessageBusReceiverFactory(MessageBus messageBus) { + this.messageBus = messageBus; + } + + private DestinationSessionParams createDestinationParams(MessageHandler messageHandler, String visitorName) { + DestinationSessionParams destparams = new DestinationSessionParams(); + destparams.setName(visitorName); + destparams.setBroadcastName(false); + destparams.setMessageHandler(messageHandler); + return destparams; + } + + @Override + public Receiver createReceiver(MessageHandler messageHandler, String sessionName) { + DestinationSessionParams destinationParams = createDestinationParams(messageHandler, sessionName); + return new MessageBusReceiver(messageBus.createDestinationSession(destinationParams)); + } + } + + public static class ThreadAsyncTaskExecutor implements AsyncTaskExecutor { + private final ScheduledExecutorService executor; + + public ThreadAsyncTaskExecutor(ScheduledExecutorService executor) { + this.executor = executor; + } + + @Override + public void submitTask(Runnable task) { + executor.submit(task); + } + + @Override + public void scheduleTask(Runnable task, long delay, TimeUnit unit) { + executor.schedule(task, delay, unit); + } + } + + private static final Logger log = Logger.getLogger(MessageBusVisitorSession.class.getName()); + + private static long sessionCounter = 0; + private static synchronized long getNextSessionId() { + return ++sessionCounter; + } + private static String createSessionName() { + StringBuilder sb = new StringBuilder(); + sb.append("visitor-").append(getNextSessionId()).append('-').append(System.currentTimeMillis()); + return sb.toString(); + } + + private final VisitorParameters params; + private final Sender sender; + private final Receiver receiver; + private final AsyncTaskExecutor taskExecutor; + private final VisitingProgress progress; + private final VisitorStatistics statistics; + private final String sessionName = createSessionName(); + private final String dataDestination; + private StateDescription state; + private long visitorCounter = 0; + private boolean scheduledSendCreateVisitors = false; + private boolean done = false; + private boolean destroying = false; // For testing and sanity checking + private final Object completionMonitor = new Object(); + private Trace trace; + /** + * We keep our own track of pending messages since the sender's pending + * count cannot be relied on in an async task execution context. This + * because it is decremented before the message is actually processed. + */ + private int pendingMessageCount = 0; + + public MessageBusVisitorSession(VisitorParameters visitorParameters, + AsyncTaskExecutor taskExecutor, + SenderFactory senderFactory, + ReceiverFactory receiverFactory, + RoutingTable routingTable) + throws ParseException + { + this.params = visitorParameters; // TODO(vekterli): make copy? legacy impl does not copy + initializeRoute(routingTable); + this.sender = senderFactory.createSender(createReplyHandler(), this.params); + this.receiver = receiverFactory.createReceiver(createMessageHandler(), sessionName); + this.taskExecutor = taskExecutor; + this.progress = createVisitingProgress(params); + this.statistics = new VisitorStatistics(); + this.state = new StateDescription(State.NOT_STARTED); + initializeHandlers(); + trace = new Trace(visitorParameters.getTraceLevel()); + dataDestination = (params.getLocalDataHandler() == null + ? params.getRemoteDataHandler() + : receiver.getConnectionSpec()); + + validateSessionParameters(); + + // If we're already done, no need to do anything at all! + if (progress.getIterator().isDone()) { + markSessionCompleted(); + } + } + + private void validateSessionParameters() { + if (dataDestination == null) { + throw new IllegalStateException("No data destination specified"); + } + } + + public void start() { + synchronized (progress.getToken()) { + if (progress.getIterator().isDone()) { + log.log(LogLevel.DEBUG, sessionName + ": progress token indicates " + + "session is done before it could even start; no-op"); + return; + } + transitionTo(new StateDescription(State.WORKING)); + taskExecutor.submitTask(new SendCreateVisitorsTask()); + } + } + + /** + * Attempt to transition to a new state. Depending on the current state, + * some transitions may be disallowed, such as transitioning from ABORTED + * to COMPLETED, since failures take precedence. Transitioning multiple + * times to the same state is a no-op in order to conserve the textual + * description given by the first transition to said state (which most + * likely is the most useful one for the end-user). + * + * @param newState State to attempt to transition to. + * @return State which is current after the transition. If transition was + * successful, will be equal to newState. + */ + private StateDescription transitionTo(StateDescription newState) { + log.log(LogLevel.DEBUG, sessionName + ": attempting transition to state " + newState); + if (newState.getState() == State.WORKING) { + assert(state.getState() == State.NOT_STARTED); + state = newState; + } else if (newState.getState() == State.COMPLETED) { + if (state.getState() != State.ABORTED && state.getState() != State.FAILED) { + state = newState; + } // else: don't override aborted state + } else if (newState.getState() == State.ABORTED) { + state = newState; + } else if (newState.getState() == State.FAILED) { + if (state.getState() != State.FAILED) { + state = newState; + } // else: don't override failed state + } else { + assert(false); + } + log.log(LogLevel.DEBUG, "Session '" + sessionName + "' is now in state " + state); + return state; + } + + private ReplyHandler createReplyHandler() { + return new ReplyHandler() { + @Override + public void handleReply(Reply reply) { + // Generally, handleReply will run in the context of the + // underlying transport layer's processing thread(s), so we + // schedule our own reply handling task to avoid blocking it. + try { + taskExecutor.submitTask(new HandleReplyTask(reply)); + } catch (RejectedExecutionException e) { + // We cannot reliably handle reply tasks failing to be submitted, since + // the reply task performs all our internal state handling logic. As such, + // we just immediately go into a failure destruction mode as soon as this + // happens, in which we do not wait for any active messages to be replied + // to. + log.log(LogLevel.WARNING, "Visitor session '" + sessionName + + "': failed to submit reply task to executor service! " + + "Session cannot reliably continue; terminating it early.", e); + + synchronized (progress.getToken()) { + transitionTo(new StateDescription(State.FAILED, "Failed to submit reply task to executor service: " + e.getMessage())); + if (!done) { + markSessionCompleted(); + } + } + } + } + }; + } + + private MessageHandler createMessageHandler() { + return new MessageHandler() { + @Override + public void handleMessage(Message message) { + try { + taskExecutor.submitTask(new HandleMessageTask(message)); + } catch (RejectedExecutionException e) { + Reply reply = ((DocumentMessage)message).createReply(); + message.swapState(reply); + reply.addError(new Error( + DocumentProtocol.ERROR_ABORTED, + "Visitor session has been aborted")); + receiver.reply(reply); + } + } + }; + } + + private void initializeRoute(RoutingTable routingTable) { + // If no cluster route has been set by user arguments, attempt to retrieve it from mbus config. + if (params.getRoute() == null || !params.getRoute().hasHops()) { + params.setRoute(getClusterRoute(routingTable)); + log.log(LogLevel.DEBUG, "No route specified; resolved implicit " + + "storage cluster: " + params.getRoute().toString()); + } + } + + private String getClusterRoute(RoutingTable routingTable) throws IllegalArgumentException{ + String route = null; + for (RoutingTable.RouteIterator it = routingTable.getRouteIterator(); + it.isValid(); it.next()) + { + String str = it.getName(); + if (str.startsWith("storage/cluster.")) { + if (route != null) { + throw new IllegalArgumentException( + "There are multiple storage clusters in your application, " + + "please specify which one to visit."); + } + route = str; + } + } + if (route == null) { + throw new IllegalArgumentException("No storage cluster found in your application."); + } + return route; + } + + /** + * Called from the constructor to ensure control and data handlers + * are set and initialized. + */ + private void initializeHandlers() { + if (this.params.getLocalDataHandler() != null) { + this.params.getLocalDataHandler().reset(); + this.params.getLocalDataHandler().setSession(this); + } else if (this.params.getRemoteDataHandler() == null) { + this.params.setLocalDataHandler(new VisitorDataQueue()); + this.params.getLocalDataHandler().setSession(this); + } + + if (params.getControlHandler() != null) { + params.getControlHandler().reset(); + } else { + params.setControlHandler(new VisitorControlHandler()); + } + params.getControlHandler().setSession(this); + } + + private VisitingProgress createVisitingProgress(VisitorParameters params) + throws ParseException + { + ProgressToken progressToken; + if (params.getResumeToken() != null) { + progressToken = params.getResumeToken(); + } else { + progressToken = new ProgressToken(); + } + VisitorIterator visitorIterator; + + if (params.getBucketsToVisit() == null + || params.getBucketsToVisit().isEmpty()) + { + // Use 1 distribution bit as a starting point. This will almost certainly + // trigger a ERROR_WRONG_DISTRIBUTION reply immediately, meaning that we'll + // get a fresh system state from the start. Since no buckets should ever + // return with a OK result in such a case, we recognize this as a special + // case in the iterator and simply reset its entire internal state using + // the new db count rather than doing any splitting. + BucketIdFactory bucketIdFactory = new BucketIdFactory(); + visitorIterator = VisitorIterator.createFromDocumentSelection( + params.getDocumentSelection(), + bucketIdFactory, + 1, + progressToken); + } else { + if (log.isLoggable(LogLevel.DEBUG)) { + log.log(LogLevel.DEBUG, "parameters specify explicit bucket set " + + "to visit; using it rather than document selection (" + + params.getBucketsToVisit().size() + " buckets given)"); + } + // Allow override of target buckets iff an explicit set of buckets + // to visit is given by the visitor parameters. This was primarily + // used for the defunct synchronization functionality, but since it's + // so easy to support, don't deprecate it just yet. + visitorIterator = VisitorIterator.createFromExplicitBucketSet( + params.getBucketsToVisit(), + 1, + progressToken); + } + return new VisitingProgress(visitorIterator, progressToken); + } + + private class SendCreateVisitorsTask implements Runnable { + // All private methods in this task must be protected by a lock around + // the progress token! + + private String getNextVisitorId() { + StringBuilder sb = new StringBuilder(); + ++visitorCounter; + sb.append(sessionName).append('-').append(visitorCounter); + return sb.toString(); + } + + private CreateVisitorMessage createMessage(VisitorIterator.BucketProgress bucket) { + CreateVisitorMessage msg = new CreateVisitorMessage( + params.getVisitorLibrary(), + getNextVisitorId(), + receiver.getConnectionSpec(), + dataDestination); + + msg.getTrace().setLevel(params.getTraceLevel()); + msg.setTimeRemaining(params.getTimeoutMs() != -1 + ? params.getTimeoutMs() + : 5 * 60 * 1000); + msg.setBuckets(Arrays.asList(bucket.getSuperbucket(), bucket.getProgress())); + msg.setDocumentSelection(params.getDocumentSelection()); + msg.setFromTimestamp(params.getFromTimestamp()); + msg.setToTimestamp(params.getToTimestamp()); + msg.setMaxPendingReplyCount(params.getMaxPending()); + msg.setFieldSet(params.fieldSet()); + msg.setVisitInconsistentBuckets(params.visitInconsistentBuckets()); + msg.setVisitRemoves(params.visitRemoves()); + msg.setParameters(params.getLibraryParameters()); + msg.setRoute(params.getRoute()); + msg.setVisitorOrdering(params.getVisitorOrdering()); + msg.setMaxBucketsPerVisitor(params.getMaxBucketsPerVisitor()); + msg.setLoadType(params.getLoadType()); + msg.setPriority(params.getPriority()); + + msg.setRetryEnabled(false); + + return msg; + } + + public void run() { + // Must sync around token as legacy API exposes it to handlers + // and they expect to be able to sync around it. + synchronized (progress.getToken()) { + try { + scheduledSendCreateVisitors = false; + while (progress.getIterator().hasNext()) { + VisitorIterator.BucketProgress bucket = progress.getIterator().getNext(); + Result result = sender.send(createMessage(bucket)); + if (result.isAccepted()) { + log.log(LogLevel.DEBUG, sessionName + ": sent CreateVisitor for bucket " + + bucket.getSuperbucket() + " with progress " + bucket.getProgress()); + ++pendingMessageCount; + } else { + // Must reinsert bucket without progress into iterator since + // we failed to send visitor. + progress.getIterator().update(bucket.getSuperbucket(), bucket.getProgress()); + break; + } + } + } catch (Exception e) { + String msg = "Got exception of type " + e.getClass().getName() + + " with message '" + e.getMessage() + + "' while attempting to send visitors"; + log.log(LogLevel.WARNING, msg); + transitionTo(new StateDescription(State.FAILED, msg)); + // It's likely that the exception caused a failure to send a + // visitor message, meaning we won't get a reply task in the + // future from which we can execute logic to complete the + // session. Thusly, we have to do this here and now. + continueVisiting(); + } catch (Throwable t) { + // We can't reliably handle this; take a nosedive + com.yahoo.protect.Process.logAndDie("Caught unhandled error when trying to send visitors", t); + } + } + } + } + + private void continueVisiting() { + if (visitingCompleted()) { + markSessionCompleted(); + } else { + scheduleSendCreateVisitorsIfApplicable(); + } + } + + private void markSessionCompleted() { + // 'done' is only ever written when token mutex is held, so safe to check + // outside of completionMonitor lock. + assert(!done) : "Session was marked as completed more than once"; + log.log(LogLevel.DEBUG, "Visitor session '" + sessionName + "' has completed"); + if (params.getLocalDataHandler() != null) { + params.getLocalDataHandler().onDone(); + } + // If skipFatalErrors is set and a fatal error did occur, fail + // the session now with the first encountered error message. + if (progress.getToken().containsFailedBuckets()) { + transitionTo(new StateDescription(State.FAILED, progress.getToken().getFirstErrorMsg())); + } + // NOTE: transitioning to COMPLETED will not override a failure + // state, so it's safe to always do this. + transitionTo(new StateDescription(State.COMPLETED)); + params.getControlHandler().onDone(state.toCompletionCode(), state.getDescription()); + synchronized (completionMonitor) { + done = true; + completionMonitor.notifyAll(); + } + } + + private class HandleReplyTask implements Runnable { + private Reply reply; + public HandleReplyTask(Reply reply) { + this.reply = reply; + } + + @Override + public void run() { + synchronized (progress.getToken()) { + try { + assert(pendingMessageCount > 0); + --pendingMessageCount; + if (reply.hasErrors()) { + handleErrorReply(reply); + } else if (reply instanceof CreateVisitorReply) { + handleCreateVisitorReply((CreateVisitorReply)reply); + } else { + String msg = "Received reply we do not know how to handle: " + + reply.getClass().getName(); + log.log(LogLevel.ERROR, msg); + transitionTo(new StateDescription(State.FAILED, msg)); + } + } catch (Exception e) { + String msg = "Got exception of type " + e.getClass().getName() + + " with message '" + e.getMessage() + + "' while processing reply in visitor session"; + e.printStackTrace(); + transitionTo(new StateDescription(State.FAILED, msg)); + } catch (Throwable t) { + // We can't reliably handle this; take a nosedive + com.yahoo.protect.Process.logAndDie("Caught unhandled error when running reply task", t); + } finally { + continueVisiting(); + } + } + } + } + + private class HandleMessageTask implements Runnable { + private Message message; + + private HandleMessageTask(Message message) { + this.message = message; + } + + @Override + public void run() { + if (log.isLoggable(LogLevel.DEBUG)) { + log.log(LogLevel.DEBUG, "Visitor session " + sessionName + ": Received message " + message); + } + try { + if (message instanceof VisitorInfoMessage) { + handleVisitorInfoMessage((VisitorInfoMessage)message); // always replies + } else { + handleDocumentMessage((DocumentMessage)message); // always replies on error + } + } catch (Throwable t) { + com.yahoo.protect.Process.logAndDie("Caught unhandled error when processing message", t); + } + } + } + + private void handleMessageProcessingException(Reply reply, Exception e, String what) { + final String errorDesc = formatProcessingException(e, what); + final String fullMsg = formatIdentifyingVisitorErrorString(errorDesc); + log.log(LogLevel.ERROR, fullMsg, e); + int errorCode; + synchronized (progress.getToken()) { + if (!params.skipBucketsOnFatalErrors()) { + errorCode = ErrorCode.APP_FATAL_ERROR; + transitionTo(new StateDescription(State.FAILED, errorDesc)); + } else { + errorCode = DocumentProtocol.ERROR_UNPARSEABLE; + } + } + reply.addError(new Error(errorCode, errorDesc)); + } + + private String formatProcessingException(Exception e, String whileProcessing) { + return String.format( + "Got exception of type %s with message '%s' while processing %s", + e.getClass().getName(), + e.getMessage(), + whileProcessing); + } + + private String formatIdentifyingVisitorErrorString(String details) { + return String.format( + "Visitor %s (selection '%s'): %s", + sessionName, + params.getDocumentSelection(), + details); + } + + /** + * NOTE: not called from within lock, function must take lock itself + */ + private void handleVisitorInfoMessage(VisitorInfoMessage msg) { + + Reply reply = msg.createReply(); + msg.swapState(reply); + + if (log.isLoggable(LogLevel.DEBUG)) { + log.log(LogLevel.DEBUG, "Visitor session " + sessionName + + ": Received VisitorInfo with " + + msg.getFinishedBuckets().size() + " finished buckets"); + } + + try { + if (msg.getErrorMessage().length() > 0) { + params.getControlHandler().onVisitorError(msg.getErrorMessage()); + } + synchronized (progress.getToken()) { + // NOTE: control handlers shall sync on token themselves if + // they want to access it, but recursive locking is OK and by + // always locking we make screwing it up harder. + if (!isDone()) { + params.getControlHandler().onProgress(progress.getToken()); + } else { + reply.addError(new Error(ErrorCode.APP_FATAL_ERROR, "Visitor has been shut down")); + } + } + } catch (Exception e) { + handleMessageProcessingException(reply, e, "VisitorInfoMessage"); + } finally { + receiver.reply(reply); + } + } + + private void handleDocumentMessage(DocumentMessage msg) { + Reply reply = msg.createReply(); + msg.swapState(reply); + + if (params.getLocalDataHandler() == null) { + log.log(LogLevel.ERROR, sessionName + ": Got visitor data back to client with no local data destination."); + reply.addError(new Error(ErrorCode.APP_FATAL_ERROR, "Visitor data with no local data destination")); + receiver.reply(reply); + return; + } + try { + params.getLocalDataHandler().onMessage(msg, new AckToken(reply)); + } catch (Exception e) { + handleMessageProcessingException(reply, e, "DocumentMessage"); + // Immediately reply since we cannot count on AckToken being registered + receiver.reply(reply); + } + } + + private boolean isFatalError(Reply reply) { + Error error = reply.getError(0); + switch (error.getCode()) { + case ErrorCode.TIMEOUT: + case DocumentProtocol.ERROR_BUCKET_NOT_FOUND: + case DocumentProtocol.ERROR_WRONG_DISTRIBUTION: + return false; + } + return error.isFatal(); + } + + /** + * Return whether a (transient) error shall be exempt from visitor + * error reporting. This to prevent spamming handlers and output with + * errors for things that are happening naturally in the system. + * @return true if the error should be reported + */ + private boolean shouldReportError(Reply reply) { + Error error = reply.getError(0); + switch (error.getCode()) { + case DocumentProtocol.ERROR_BUCKET_NOT_FOUND: + case DocumentProtocol.ERROR_BUCKET_DELETED: + return false; + } + return true; + } + + private static String getErrorMessage(Error r) { + return DocumentProtocol.getErrorName(r.getCode()) + ": " + r.getMessage(); + } + + private static boolean isErrorOfType(Reply reply, int errorCode) { + return reply.getError(0).getCode() == errorCode; + } + + private void reportVisitorError(String message) { + params.getControlHandler().onVisitorError(message); + } + + private void handleErrorReply(Reply reply) { + CreateVisitorMessage msg = (CreateVisitorMessage)reply.getMessage(); + // Must reset bucket progress back to what it was before sending. + BucketId bucket = msg.getBuckets().get(0); + BucketId subProgress = msg.getBuckets().get(1); + progress.getIterator().update(bucket, subProgress); + + String message = getErrorMessage(reply.getError(0)); + log.log(LogLevel.DEBUG, sessionName + ": received error reply for bucket " + + bucket + " with message '" + message + "'"); + + if (isFatalError(reply)) { + if (params.skipBucketsOnFatalErrors()) { + progress.getToken().addFailedBucket(bucket, subProgress, message); + progress.getIterator().update(bucket, ProgressToken.FINISHED_BUCKET); + } else { + reportVisitorError(message); + transitionTo(new StateDescription(State.FAILED, message)); + return; // no additional visitors will be scheduled post-failure + } + } + if (isErrorOfType(reply, DocumentProtocol.ERROR_WRONG_DISTRIBUTION)) { + handleWrongDistributionReply((WrongDistributionReply)reply); + } else { + if (shouldReportError(reply)) { + reportVisitorError(message); + } + // Wait 100ms before new visitor task is executed. Will prevent + // visitors from being scheduled from caller. + scheduleSendCreateVisitorsIfApplicable(100, TimeUnit.MILLISECONDS); + } + } + + private boolean enoughHitsReceived() { + if (params.getMaxFirstPassHits() != -1 + && statistics.getDocumentsReturned() >= params.getMaxFirstPassHits()) + { + return true; + } + if (params.getMaxTotalHits() != -1 + && ((statistics.getDocumentsReturned() + + statistics.getSecondPassDocumentsReturned()) + >= params.getMaxTotalHits())) + { + return true; + } + return false; + } + + /** + * A session is considered completed if one or more of the following holds true: + * - All buckets have been visited (i.e. no active or pending visitors). + * - Visiting has failed fatally (or has been aborted) AND there are no + * active visitors remaining. 'Active' here means that we're waiting + * for a reply. + * - We have received sufficient number of documents (set via visitor + * parameters) from the buckets already visited AND there are no + * active visitors remaining. + * @return true if visiting has completed, false otherwise + */ + private boolean visitingCompleted() { + return (pendingMessageCount == 0) + && (progress.getIterator().isDone() + || state.failed() + || enoughHitsReceived()); + } + + /** + * Schedule a new SendCreateVisitors task iff there are still buckets to + * visit, the visiting has not failed fatally and we haven't already + * scheduled such a task. + */ + private void scheduleSendCreateVisitorsIfApplicable(long delay, TimeUnit unit) { + if (scheduledSendCreateVisitors + || !progress.getIterator().hasNext() + || state.failed() + || enoughHitsReceived()) + { + return; + } + taskExecutor.scheduleTask(new SendCreateVisitorsTask(), delay, unit); + scheduledSendCreateVisitors = true; + } + + private void scheduleSendCreateVisitorsIfApplicable() { + scheduleSendCreateVisitorsIfApplicable(0, TimeUnit.MILLISECONDS); + } + + private void handleCreateVisitorReply(CreateVisitorReply reply) { + CreateVisitorMessage msg = (CreateVisitorMessage)reply.getMessage(); + + BucketId superbucket = msg.getBuckets().get(0); + BucketId subBucketProgress = reply.getLastBucket(); + + log.log(LogLevel.DEBUG, sessionName + ": received CreateVisitorReply for bucket " + + superbucket + " with progress " + subBucketProgress); + + progress.getIterator().update(superbucket, subBucketProgress); + params.getControlHandler().onProgress(progress.getToken()); + statistics.add(reply.getVisitorStatistics()); + params.getControlHandler().onVisitorStatistics(statistics); + trace.getRoot().addChild(reply.getTrace().getRoot()); + + if (params.getDynamicallyIncreaseMaxBucketsPerVisitor() + && (reply.getVisitorStatistics().getDocumentsReturned() + < params.getMaxFirstPassHits() / 2.0)) + { + // Attempt to increase parallelism to reduce latency of visiting + // Ensure new count is within [1, 128] + int newMaxBuckets = Math.max(Math.min((int)(params.getMaxBucketsPerVisitor() + * params.getDynamicMaxBucketsIncreaseFactor()), 128), 1); + params.setMaxBucketsPerVisitor(newMaxBuckets); + log.log(LogLevel.DEBUG, sessionName + ": increasing max buckets per visitor to " + + params.getMaxBucketsPerVisitor()); + } + } + + private void handleWrongDistributionReply(WrongDistributionReply reply) { + try { + // Classnames clash with documentapi classes, so be explicit + ClusterState newState = new ClusterState(reply.getSystemState()); + int stateBits = newState.getDistributionBitCount(); + if (stateBits != progress.getIterator().getDistributionBitCount()) { + log.log(LogLevel.DEBUG, "System state changed; now at " + + stateBits + " distribution bits"); + // Update the internal state of the visitor iterator. If we're increasing + // the number of distribution bits, this may lead to splitting of pending + // buckets. If we're decreasing, it may lead to merging of pending buckets + // and potential loss of sub-bucket progress. In either way, the iterator + // will not let any new buckets out before all active buckets have been + // updated. + progress.getIterator().setDistributionBitCount(stateBits); + } + } catch (Exception e) { + log.log(LogLevel.ERROR, "Failed to parse new system state string: " + + reply.getSystemState()); + transitionTo(new StateDescription(State.FAILED, "Failed to parse cluster state '" + + reply.getSystemState() + "'")); + } + } + + public String getSessionName() { + return sessionName; + } + + @Override + public boolean isDone() { + synchronized (progress.getToken()) { + return done; + } + } + + @Override + public ProgressToken getProgress() { + return progress.getToken(); + } + + @Override + public Trace getTrace() { + return trace; + } + + @Override + public boolean waitUntilDone(long timeoutMs) throws InterruptedException { + return params.getControlHandler().waitUntilDone(timeoutMs); + } + + @Override + public void ack(AckToken token) { + if (log.isLoggable(LogLevel.DEBUG)) { + log.log(LogLevel.DEBUG, "Visitor session " + sessionName + + ": Sending ack " + token.ackObject); + } + // No locking here; replying should be thread safe in itself + receiver.reply((Reply)token.ackObject); + } + + @Override + public void abort() { + synchronized (progress.getToken()) { + transitionTo(new StateDescription(State.ABORTED, "Visitor aborted by user")); + } + } + + @Override + public VisitorResponse getNext() { + if (params.getLocalDataHandler() == null) { + throw new IllegalStateException("Data has been routed to external source for this visitor"); + } + return params.getLocalDataHandler().getNext(); + } + + @Override + public VisitorResponse getNext(int timeoutMilliseconds) throws InterruptedException { + if (params.getLocalDataHandler() == null) { + throw new IllegalStateException("Data has been routed to external source for this visitor"); + } + return params.getLocalDataHandler().getNext(timeoutMilliseconds); + } + + /** + * For unit test purposes only, not to be used by any external parties. + * @return true if destroy() has been--or is being--invoked. + */ + public boolean isDestroying() { + synchronized (completionMonitor) { + return destroying; + } + } + + @Override + public void destroy() { + log.log(LogLevel.DEBUG, sessionName + ": synchronous destroy() called"); + try { + synchronized (progress.getToken()) { + synchronized (completionMonitor) { + // If we are destroying the session before it has completed (e.g. because + // waitUntilDone timed out or an interactive visiting was interrupted) + // set us to aborted state so that we'll seize + if (!done) { + transitionTo(new StateDescription(State.ABORTED, "Session explicitly destroyed before completion")); + } + } + } + synchronized (completionMonitor) { + assert(!destroying) : "Attempted to destroy VisitorSession more than once"; + destroying = true; + while (!done) { + completionMonitor.wait(); + } + } + } catch (InterruptedException e) { + e.printStackTrace(); + } finally { + try { + sender.destroy(); + receiver.destroy(); + } catch (Exception e) { + log.log(LogLevel.ERROR, "Caught exception destroying communication interfaces", e); + } + log.log(LogLevel.DEBUG, sessionName + ": synchronous destroy() done"); + } + } +} diff --git a/documentapi/src/main/java/com/yahoo/documentapi/messagebus/ScheduledEventQueue.java b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/ScheduledEventQueue.java new file mode 100755 index 00000000000..f15d27e7cd8 --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/ScheduledEventQueue.java @@ -0,0 +1,189 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi.messagebus; + +import com.yahoo.concurrent.SystemTimer; +import com.yahoo.concurrent.Timer; + +import java.util.*; +import java.util.concurrent.RejectedExecutionException; + +/** + * Simple, lightweight event scheduler that does not maintain any executor + * threads of its own, but rather makes it the responsibility of the caller + * to run the events as the queue hands them over. + * + * Fully thread safe for multiple readers and writers. + */ +public class ScheduledEventQueue { + private final Set<Entry> tasks = new TreeSet<Entry>(); + private long sequenceCounter = 0; + private Timer timer; + private volatile boolean waiting = false; + private volatile boolean shutdown = false; + + private static class Entry implements Comparable<Entry> { + private Runnable task; + private long timestamp; + private long sequenceId; + + public Entry(Runnable task, long timestamp, long sequenceId) { + this.task = task; + this.timestamp = timestamp; + this.sequenceId = sequenceId; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Entry entry = (Entry) o; + + if (sequenceId != entry.sequenceId) return false; + if (timestamp != entry.timestamp) return false; + if (!task.equals(entry.task)) return false; + + return true; + } + + public int compareTo(Entry o) { + if (timestamp < o.timestamp) return -1; + if (timestamp > o.timestamp) return 1; + if (sequenceId < o.sequenceId) return -1; + if (sequenceId > o.sequenceId) return 1; + return 0; + } + + public Runnable getTask() { + return task; + } + + public long getTimestamp() { + return timestamp; + } + + public long getSequenceId() { + return sequenceId; + } + } + + public ScheduledEventQueue() { + this.timer = SystemTimer.INSTANCE; + } + + public ScheduledEventQueue(Timer timer) { + this.timer = timer; + } + + public void pushTask(Runnable task) { + synchronized (tasks) { + if (shutdown) { + throw new RejectedExecutionException("Tasks can't be scheduled since queue has been shut down."); + } + + tasks.add(new Entry(task, 0, sequenceCounter++)); + tasks.notifyAll(); + } + } + + public void pushTask(Runnable task, long milliSecondsToWait) { + synchronized (tasks) { + if (shutdown) { + throw new RejectedExecutionException("Tasks can't be scheduled since queue has been shut down."); + } + + tasks.add(new Entry(task, timer.milliTime() + milliSecondsToWait, sequenceCounter++)); + tasks.notifyAll(); + } + } + + public boolean isWaiting() { + synchronized (tasks) { + return waiting; + } + } + + /** + * Waits until the queue has a task that is ready for scheduling, removes that + * task from the queue and returns it. + * + * @return The next task. + */ + public Runnable getNextTask() { + try { + while (true) { + synchronized (tasks) { + Iterator<Entry> iter = tasks.iterator(); + if (!iter.hasNext()) { + if (shutdown) { + return null; + } + // Set flag for unit tests to coordinate with. + waiting = true; + tasks.wait(); + waiting = false; + continue; + } + Entry retEntry = iter.next(); + long timeNow = timer.milliTime(); + if (retEntry.getTimestamp() > timeNow) { + waiting = true; + tasks.wait(retEntry.getTimestamp() - timeNow); + waiting = false; + continue; + } + iter.remove(); + return retEntry.getTask(); + } + } + } catch (InterruptedException e) { + return null; + } + } + + /** + * If there is a task ready for scheduling, remove it from the queue and return it. + * + * @return The next task. + */ + public Runnable popTask() { + synchronized (tasks) { + Iterator<Entry> iter = tasks.iterator(); + if (!iter.hasNext()) { + return null; + } + Entry retEntry = iter.next(); + if (retEntry.getTimestamp() > timer.milliTime()) { + return null; + } + iter.remove(); + return retEntry.getTask(); + } + } + + /** For unit testing only */ + public void wakeTasks() { + synchronized (tasks) { + tasks.notifyAll(); + } + } + + public void shutdown() { + synchronized (tasks) { + shutdown = true; + tasks.notifyAll(); + } + } + + public boolean isShutdown() { + synchronized (tasks) { + return shutdown; + } + } + + public long size() { + synchronized (tasks) { + return tasks.size(); + } + } +} diff --git a/documentapi/src/main/java/com/yahoo/documentapi/messagebus/loadtypes/LoadType.java b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/loadtypes/LoadType.java new file mode 100644 index 00000000000..a8edfe16bb5 --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/loadtypes/LoadType.java @@ -0,0 +1,39 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi.messagebus.loadtypes; + +import com.yahoo.documentapi.messagebus.protocol.DocumentProtocol; + +/** + * @author thomasg + */ +public class LoadType { + private int id; + private String name; + private DocumentProtocol.Priority priority; + + public static LoadType DEFAULT = new LoadType(0, "default", DocumentProtocol.Priority.NORMAL_3); + + public LoadType(int id, String name, DocumentProtocol.Priority priority) { + this.id = id; + this.name = name; + this.priority = priority; + } + + public boolean equals(Object other) { + if (!(other instanceof LoadType)) { + return false; + } + + LoadType o = (LoadType)other; + + return name.equals(o.getName()) && id == o.getId() && priority == o.getPriority(); + } + + public String getName() { return name; } + + public String toString() { return name + " (id " + id + ")"; } + + public DocumentProtocol.Priority getPriority() { return priority; } + + public int getId() { return id; } +} diff --git a/documentapi/src/main/java/com/yahoo/documentapi/messagebus/loadtypes/LoadTypeSet.java b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/loadtypes/LoadTypeSet.java new file mode 100644 index 00000000000..cb453559ab1 --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/loadtypes/LoadTypeSet.java @@ -0,0 +1,100 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi.messagebus.loadtypes; + +import com.yahoo.config.subscription.ConfigGetter; +import com.yahoo.config.subscription.ConfigSubscriber; +import com.yahoo.vespa.config.content.LoadTypeConfig; +import com.yahoo.documentapi.messagebus.protocol.DocumentProtocol; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.HashMap; +import java.util.Map; +import java.util.TreeMap; + +/** + * This class keeps track of all configured load types. + * + * For production use, you should only use the String constructor, + * and supply a valid config id. Only the load types configured will + * be propagated throughout the system, so there is no point in using other + * load types. + * + * For testing, you may want to use the empty constructor and add + * load types yourself with addType(). + */ +public class LoadTypeSet { + class DualMap { + Map<String, LoadType> nameMap = new TreeMap<String, LoadType>(); + Map<Integer, LoadType> idMap = new HashMap<Integer, LoadType>(); + + void put(LoadType l) { + if (nameMap.containsKey(l.getName()) || idMap.containsKey(l.getId())) { + throw new IllegalArgumentException( + "ID or name conflict when adding " + l); + } + + nameMap.put(l.getName(), l); + idMap.put(l.getId(), l); + } + } + + DualMap map; + + public LoadTypeSet() { + map = new DualMap(); + map.put(LoadType.DEFAULT); + } + + public LoadTypeSet(String configId) { + configure(new ConfigGetter<>(LoadTypeConfig.class).getConfig(configId)); + } + + public Map<String, LoadType> getNameMap() { + return map.nameMap; + } + + public Map<Integer, LoadType> getIdMap() { + return map.idMap; + } + + /** + * Used by config to generate priorities for a name, and add them to the load type set. + */ + public void addType(String name, String priority) { + try { + MessageDigest algorithm = MessageDigest.getInstance("MD5"); + algorithm.reset(); + algorithm.update(name.getBytes()); + byte messageDigest[] = algorithm.digest(); + + int id = 0; + for (int i = 0; i < 4; i++) { + int temp = ((int)messageDigest[i] & 0xff); + id <<= 8; + id |= temp; + } + + map.put(new LoadType(id, name, DocumentProtocol.Priority.valueOf(priority != null ? priority : "NORMAL_3"))); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + } + + public void addLoadType(int id, String name, DocumentProtocol.Priority priority) { + map.put(new LoadType(id, name, priority)); + } + + public void configure(LoadTypeConfig config) { + DualMap newMap = new DualMap(); + + // Default should always be available. + newMap.put(LoadType.DEFAULT); + + for (LoadTypeConfig.Type t : config.type()) { + newMap.put(new LoadType(t.id(), t.name(), DocumentProtocol.Priority.valueOf(t.priority()))); + } + + map = newMap; + } +} + diff --git a/documentapi/src/main/java/com/yahoo/documentapi/messagebus/loadtypes/package-info.java b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/loadtypes/package-info.java new file mode 100644 index 00000000000..34608cfc8db --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/loadtypes/package-info.java @@ -0,0 +1,5 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +@ExportPackage +package com.yahoo.documentapi.messagebus.loadtypes; + +import com.yahoo.osgi.annotation.ExportPackage; diff --git a/documentapi/src/main/java/com/yahoo/documentapi/messagebus/package-info.java b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/package-info.java new file mode 100644 index 00000000000..70ed2af38ce --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/package-info.java @@ -0,0 +1,7 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +@ExportPackage +@PublicApi +package com.yahoo.documentapi.messagebus; + +import com.yahoo.api.annotations.PublicApi; +import com.yahoo.osgi.annotation.ExportPackage; diff --git a/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/ANDPolicy.java b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/ANDPolicy.java new file mode 100755 index 00000000000..04818f80672 --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/ANDPolicy.java @@ -0,0 +1,67 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi.messagebus.protocol;
+
+import com.yahoo.messagebus.metrics.MetricSet;
+import com.yahoo.messagebus.routing.Hop;
+import com.yahoo.messagebus.routing.Route;
+import com.yahoo.messagebus.routing.RoutingContext;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * An AND policy is a routing policy that can be used to write simple routes that split a message between multiple other
+ * destinations. It can either be configured in a routing config, which will then produce a policy that always selects
+ * all configured recipients, or it can be configured using the policy parameter (i.e. a string following the name of
+ * the policy). Note that configured recipients take precedence over recipients configured in the parameter string.
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class ANDPolicy implements DocumentProtocolRoutingPolicy {
+
+ // A list of hops that are to always be selected when select() is invoked.
+ private final List<Hop> hops = new ArrayList<Hop>();
+
+ /**
+ * Constructs a new AND policy that requires all recipients to be ok for it to merge their replies to an ok reply.
+ * I.e. all errors in all child replies are copied into the merged reply.
+ *
+ * @param param A string of recipients to select unless recipients have been configured.
+ */
+ public ANDPolicy(String param) {
+ if (param == null || param.isEmpty()) {
+ return;
+ }
+ Route route = Route.parse(param);
+ for (int i = 0; i < route.getNumHops(); ++i) {
+ hops.add(route.getHop(i));
+ }
+ }
+
+ // Inherit doc from RoutingPolicy.
+ public void select(RoutingContext context) {
+ if (hops.isEmpty()) {
+ context.addChildren(context.getAllRecipients());
+ } else {
+ for (Hop hop : hops) {
+ Route route = new Route(context.getRoute());
+ route.setHop(0, hop);
+ context.addChild(route);
+ }
+ }
+ context.setSelectOnRetry(false);
+ context.addConsumableError(DocumentProtocol.ERROR_MESSAGE_IGNORED);
+ }
+
+ // Inherit doc from RoutingPolicy.
+ public void merge(RoutingContext context) {
+ DocumentProtocol.merge(context);
+ }
+
+ public void destroy() {
+ }
+
+ public MetricSet getMetrics() {
+ return null;
+ }
+}
diff --git a/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/AbstractRoutableFactory.java b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/AbstractRoutableFactory.java new file mode 100644 index 00000000000..bd192dea745 --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/AbstractRoutableFactory.java @@ -0,0 +1,44 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi.messagebus.protocol; + +import com.yahoo.text.Utf8; +import com.yahoo.vespa.objects.Deserializer; +import com.yahoo.vespa.objects.Serializer; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen Hult</a> + */ +public abstract class AbstractRoutableFactory implements RoutableFactory { + + /** + * Reads a string from the given buffer that was previously written by {@link #encodeString(String, + * com.yahoo.vespa.objects.Serializer)}. + * + * @param in The byte buffer to read from. + * @return The decoded string. + */ + public static String decodeString(Deserializer in) { + int length = in.getInt(null); + if (length == 0) { + return ""; + } + return Utf8.toString(in.getBytes(null, length)); + } + + /** + * Writes the given string to the given byte buffer in such a way that it can be decoded using {@link + * #decodeString(com.yahoo.vespa.objects.Deserializer)}. + * + * @param str The string to encode. + * @param out The byte buffer to write to. + */ + public static void encodeString(String str, Serializer out) { + if (str == null || str.isEmpty()) { + out.putInt(null, 0); + } else { + byte[] nameBytes = Utf8.toBytes(str); + out.putInt(null, nameBytes.length); + out.put(null, nameBytes); + } + } +} diff --git a/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/AsyncInitializationPolicy.java b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/AsyncInitializationPolicy.java new file mode 100644 index 00000000000..7e4e1d3a5ca --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/AsyncInitializationPolicy.java @@ -0,0 +1,118 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi.messagebus.protocol; + +import com.yahoo.messagebus.*; +import com.yahoo.messagebus.metrics.MetricSet; +import com.yahoo.messagebus.routing.RoutingContext; + +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.concurrent.ScheduledThreadPoolExecutor; + +/** + * Abstract class for routing policies that need asynchronous initialization. + * This is recommended if the routing policy needs configuration, or synchronization with + * other sources. If this policy is not used in those cases, the messagebus thread might hang + * waiting for the other sources, causing messages to other routes to be blocked. + */ +public abstract class AsyncInitializationPolicy implements DocumentProtocolRoutingPolicy, Runnable { + + protected enum InitState { + NOT_STARTED, + RUNNING, + DONE + }; + + InitState initState; + ScheduledThreadPoolExecutor executor; + Exception initException; + boolean syncInit = true; + + public static Map<String, String> parse(String param) { + Map<String, String> map = new TreeMap<String, String>(); + + if (param != null) { + String[] p = param.split(";"); + for (String s : p) { + String[] keyValue = s.split("="); + + if (keyValue.length == 1) { + map.put(keyValue[0], "true"); + } else if (keyValue.length == 2) { + map.put(keyValue[0], keyValue[1]); + } + } + } + + return map; + } + + public AsyncInitializationPolicy(Map<String, String> params) { + initState = InitState.NOT_STARTED; + } + + public void needAsynchronousInitialization() { + syncInit = false; + } + + public abstract void init(); + + public abstract void doSelect(RoutingContext routingContext); + + private synchronized void checkStartInit() { + if (initState == InitState.NOT_STARTED) { + if (syncInit) { + init(); + initState = InitState.DONE; + } else { + executor = new ScheduledThreadPoolExecutor(1); + executor.execute(this); + initState = InitState.RUNNING; + } + } + } + + @Override + public void select(RoutingContext routingContext) { + synchronized (this) { + if (initException != null) { + Reply reply = new EmptyReply(); + reply.addError(new com.yahoo.messagebus.Error(ErrorCode.POLICY_ERROR, initException.getMessage())); + routingContext.setReply(reply); + return; + } + + checkStartInit(); + + if (initState == InitState.RUNNING) { + Reply reply = new EmptyReply(); + reply.addError(new com.yahoo.messagebus.Error(ErrorCode.SESSION_BUSY, "Policy is waiting to be initialized.")); + routingContext.setReply(reply); + return; + } + } + + doSelect(routingContext); + } + + public void run() { + try { + init(); + } catch (Exception e) { + initException = e; + } + + synchronized (this) { + initState = InitState.DONE; + this.notifyAll(); + } + } + + @Override + public void destroy() { + if (executor != null) { + executor.shutdownNow(); + } + } +} diff --git a/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/BatchDocumentUpdateMessage.java b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/BatchDocumentUpdateMessage.java new file mode 100644 index 00000000000..cfc6dc6ef82 --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/BatchDocumentUpdateMessage.java @@ -0,0 +1,184 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi.messagebus.protocol; + +import com.yahoo.document.BucketId; +import com.yahoo.document.BucketIdFactory; +import com.yahoo.document.DocumentId; +import com.yahoo.document.DocumentUpdate; +import com.yahoo.document.idstring.GroupDocIdString; +import com.yahoo.document.idstring.IdString; +import com.yahoo.document.idstring.UserDocIdString; +import com.yahoo.document.serialization.DocumentDeserializer; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class BatchDocumentUpdateMessage extends DocumentMessage { + + private DocumentDeserializer buffer = null; + private List<DocumentUpdate> updates = new ArrayList<DocumentUpdate>(); + private LazyDecoder decoder = null; + private String group = null; + private Long userId = null; + private BucketId bucketId = null; + + public String getGroup() { + return group; + } + + public Long getUserId() { + return userId; + } + + /** + * Constructs a new message for deserialization. + */ + BatchDocumentUpdateMessage() { + // empty + } + + /** + * Constructs a new message from a byte buffer. + * @param decoder The decoder to use for deserialization. + * @param buffer A byte buffer that contains a serialized message. + */ + public BatchDocumentUpdateMessage(long userId, LazyDecoder decoder, DocumentDeserializer buffer) { + this.userId = userId; + this.decoder = decoder; + this.buffer = buffer; + setBucketId(new UserDocIdString("foo", userId, "bar")); + } + + /** + * Constructs a new message from a byte buffer. + * @param decoder The decoder to use for deserialization. + * @param buffer A byte buffer that contains a serialized message. + */ + public BatchDocumentUpdateMessage(String group, LazyDecoder decoder, DocumentDeserializer buffer) { + this.group = group; + this.decoder = decoder; + this.buffer = buffer; + setBucketId(new GroupDocIdString("foo", group, "bar")); + } + + /** + * Constructs a batch document update message, to contain updates for documents for the given user. + */ + public BatchDocumentUpdateMessage(long userId) { + super(); + this.userId = userId; + setBucketId(new UserDocIdString("foo", userId, "bar")); + } + + /** + * Constructs a batch document update message, to contain updates for documents for the given user. + */ + public BatchDocumentUpdateMessage(String group) { + super(); + this.group = group; + setBucketId(new GroupDocIdString("foo", group, "bar")); + } + + void setBucketId(IdString id) { + BucketIdFactory factory = new BucketIdFactory(); + bucketId = factory.getBucketId(new DocumentId(id)); + } + + BucketId getBucketId() { + return bucketId; + } + + /** + * This method will make sure that any serialized content is deserialized into proper message content on first + * entry. Any subsequent entry into this function will do nothing. + */ + private void deserialize() { + if (decoder != null && buffer != null) { + decoder.decode(this, buffer); + decoder = null; + buffer = null; + } + } + + /** + * Returns the list of document updates to perform. + * + * @return The updates. + */ + public List<DocumentUpdate> getUpdates() { + deserialize(); + return updates; + } + + /** + * Adds a document update to perform. + * + * @param upd The document update to set. + */ + public void addUpdate(DocumentUpdate upd) { + buffer = null; + decoder = null; + + verifyUpdate(upd); + updates.add(upd); + } + + /** + * Returns the raw serialized buffer. This buffer is stored as the message is received from accross the network, and + * deserialized from as soon as a member is requested. This method will return null if the buffer has been decoded. + * + * @return The buffer containing the serialized data for this message, or null. + */ + ByteBuffer getSerializedBuffer() { + return buffer != null ? buffer.getBuf().getByteBuffer() : null; + } + + @Override + public DocumentReply createReply() { + return new BatchDocumentUpdateReply(); + } + + @Override + public int getType() { + return DocumentProtocol.MESSAGE_BATCHDOCUMENTUPDATE; + } + + void verifyUpdate(DocumentUpdate update) { + if (update == null) { + throw new IllegalArgumentException("Document update can not be null."); + } + + IdString idString = update.getId().getScheme(); + + if (group != null) { + String idGroup; + + if (idString.hasGroup()) { + idGroup = idString.getGroup(); + } else { + throw new IllegalArgumentException("Batch update message can only contain groupdoc or orderdoc items"); + } + + if (!group.equals(idGroup)) { + throw new IllegalArgumentException("Batch update message can not contain messages from group " + idGroup + " only group " + group); + } + } else { + long foundUserId = 0; + + if (idString.hasNumber()) { + foundUserId = idString.getNumber(); + } else { + throw new IllegalArgumentException("Batch update message can only contain userdoc or orderdoc items"); + } + + if (userId != foundUserId) { + throw new IllegalArgumentException("Batch update message can not contain messages from user " + foundUserId + " only user " + userId); + } + } + } + +} diff --git a/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/BatchDocumentUpdateReply.java b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/BatchDocumentUpdateReply.java new file mode 100755 index 00000000000..48eb41fdb5c --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/BatchDocumentUpdateReply.java @@ -0,0 +1,29 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi.messagebus.protocol;
+
+import java.util.ArrayList;
+
+public class BatchDocumentUpdateReply extends WriteDocumentReply {
+
+ private ArrayList<Boolean> documentsNotFound = new ArrayList<Boolean>();
+
+ /**
+ * Constructs a new reply with no content.
+ */
+ public BatchDocumentUpdateReply() {
+ super(DocumentProtocol.REPLY_BATCHDOCUMENTUPDATE);
+ }
+
+ /**
+ * If all documents to update are found, this vector will be empty. If
+ * one or more documents are not found, this vector will have the size of
+ * the initial number of updates, with entries set to true where the
+ * corresponding update was not found.
+ *
+ * @return Vector containing indices of not found documents, or empty
+ * if all documents were found
+ */
+ public ArrayList<Boolean> getDocumentsNotFound() {
+ return documentsNotFound;
+ }
+}
diff --git a/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/ContentPolicy.java b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/ContentPolicy.java new file mode 100644 index 00000000000..4dd224bcc45 --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/ContentPolicy.java @@ -0,0 +1,43 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi.messagebus.protocol; + +import java.util.Map; + +/** + * Policy to talk to content clusters. + */ +public class ContentPolicy extends StoragePolicy { + + public static class ContentParameters extends Parameters { + + public ContentParameters(Map<String, String> parameters) { + super(parameters); + } + + @Override + public String getDistributionConfigId() { + if (distributionConfigId != null) { + return distributionConfigId; + } + return clusterName; + } + + @Override + public SlobrokHostPatternGenerator createPatternGenerator() { + return new SlobrokHostPatternGenerator(getClusterName()) { + public String getDistributorHostPattern(Integer distributor) { + return "storage/cluster." + getClusterName() + "/distributor/" + (distributor == null ? "*" : distributor) + "/default"; + } + }; + } + } + + public ContentPolicy(Map<String, String> params) { + super(new ContentParameters(params), params); + } + + public ContentPolicy(String parameters) { + this(parse(parameters)); + } + +} diff --git a/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/CreateVisitorMessage.java b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/CreateVisitorMessage.java new file mode 100644 index 00000000000..9e6c5cb793b --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/CreateVisitorMessage.java @@ -0,0 +1,217 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi.messagebus.protocol; + +import com.yahoo.document.BucketId; + +import java.util.*; + +public class CreateVisitorMessage extends DocumentMessage { + + private String libName = "DumpVisitor"; + private String instanceId = ""; + private String controlDestination = ""; + private String dataDestination = ""; + private String docSelection = ""; + private int maxPendingReplyCount = 8; + private List<BucketId> buckets = new ArrayList<>(); + private long fromTime = 0; + private long toTime = 0; + private boolean visitRemoves = false; + private String fieldSet = "[all]"; + private boolean visitInconsistentBuckets = false; + private Map<String, byte[]> params = new TreeMap<>(); + private int version = 42; + private int ordering = 0; + private int maxBucketsPerVisitor = 1; + + CreateVisitorMessage() { + // must be deserialized into + } + + public CreateVisitorMessage(String libraryName, String instanceId, String controlDestination, + String dataDestination) { + libName = libraryName; + this.instanceId = instanceId; + this.controlDestination = controlDestination; + this.dataDestination = dataDestination; + } + + public String getLibraryName() { + return libName; + } + + public void setLibraryName(String libraryName) { + libName = libraryName; + } + + public String getInstanceId() { + return instanceId; + } + + public void setInstanceId(String instanceId) { + this.instanceId = instanceId; + } + + public String getControlDestination() { + return controlDestination; + } + + public void setControlDestination(String controlDestination) { + this.controlDestination = controlDestination; + } + + public String getDataDestination() { + return dataDestination; + } + + public void setDataDestination(String dataDestination) { + this.dataDestination = dataDestination; + } + + public String getDocumentSelection() { + return docSelection; + } + + public void setDocumentSelection(String documentSelection) { + docSelection = documentSelection; + } + + public int getMaxPendingReplyCount() { + return maxPendingReplyCount; + } + + public void setMaxPendingReplyCount(int count) { + maxPendingReplyCount = count; + } + + public Map<String, byte[]> getParameters() { + return params; + } + + public void setParameters(Map<String, byte[]> parameters) { + params = parameters; + } + + public List<BucketId> getBuckets() { + return buckets; + } + + public void setBuckets(List<BucketId> buckets) { + this.buckets = buckets; + } + + public boolean getVisitRemoves() { + return visitRemoves; + } + + public void setVisitRemoves(boolean visitRemoves) { + this.visitRemoves = visitRemoves; + } + + public String getFieldSet() { + return fieldSet; + } + + public void setFieldSet(String fieldSet) { + this.fieldSet = fieldSet; + } + + public boolean getVisitInconsistentBuckets() { + return visitInconsistentBuckets; + } + + public void setVisitInconsistentBuckets(boolean visitInconsistentBuckets) { + this.visitInconsistentBuckets = visitInconsistentBuckets; + } + + public void setFromTimestamp(long from) { + fromTime = from; + } + + public void setToTimestamp(long to) { + toTime = to; + } + + public long getFromTimestamp() { + return fromTime; + } + + public long getToTimestamp() { + return toTime; + } + + public void setVisitorDispatcherVersion(int version) { + this.version = version; + } + + public int getVisitorDispatcherVersion() { + return version; + } + + public void setVisitorOrdering(int ordering) { + this.ordering = ordering; + } + + public int getVisitorOrdering() { + return ordering; + } + + public void setMaxBucketsPerVisitor(int max) { + this.maxBucketsPerVisitor = max; + } + + public int getMaxBucketsPerVisitor() { + return maxBucketsPerVisitor; + } + + @Override + public DocumentReply createReply() { + return new CreateVisitorReply(DocumentProtocol.REPLY_CREATEVISITOR); + } + + @Override + public int getApproxSize() { + return buckets.size() * 8; + } + + @Override + public int getType() { + return DocumentProtocol.MESSAGE_CREATEVISITOR; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder().append("CreateVisitorMessage("); + if (buckets.size() == 0) { + sb.append("No buckets"); + } else if (buckets.size() == 1) { + sb.append("Bucket ").append(buckets.iterator().next().toString()); + } else if (buckets.size() < 65536) { + sb.append(buckets.size()).append(" buckets:"); + Iterator<BucketId> it = buckets.iterator(); + for (int i = 0; it.hasNext() && i < 3; ++i) { + sb.append(' ').append(it.next().toString()); + } + if (it.hasNext()) { + sb.append(" ..."); + } + } else { + sb.append("All buckets"); + } + if (fromTime != 0 || toTime != 0) { + sb.append(", time ").append(fromTime).append('-').append(toTime); + } + sb.append(", selection '").append(docSelection).append('\''); + if (!libName.equals("DumpVisitor")) { + sb.append(", library ").append(libName); + } + if (visitRemoves) { + sb.append(", including removes"); + } + sb.append(", get fields: " + fieldSet); + if (visitInconsistentBuckets) { + sb.append(", visit inconsistent buckets"); + } + return sb.append(')').toString(); + } +} diff --git a/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/CreateVisitorReply.java b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/CreateVisitorReply.java new file mode 100644 index 00000000000..f9350b79cc4 --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/CreateVisitorReply.java @@ -0,0 +1,34 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi.messagebus.protocol; + +import com.yahoo.document.BucketId; +import com.yahoo.vdslib.VisitorStatistics; + +public class CreateVisitorReply extends DocumentReply { + + private BucketId lastBucket; + private VisitorStatistics statistics = new VisitorStatistics(); + + public CreateVisitorReply(int type) { + super(type); + lastBucket = new BucketId(Integer.MAX_VALUE); + } + + public void setLastBucket(BucketId lastBucket) { + this.lastBucket = lastBucket; + } + + public BucketId getLastBucket() { + return lastBucket; + } + + public void setVisitorStatistics(VisitorStatistics statistics) { + this.statistics = statistics; + } + + public VisitorStatistics getVisitorStatistics() { + return statistics; + } + + +} diff --git a/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/DestroyVisitorMessage.java b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/DestroyVisitorMessage.java new file mode 100644 index 00000000000..ab2125600f7 --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/DestroyVisitorMessage.java @@ -0,0 +1,37 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi.messagebus.protocol; + +public class DestroyVisitorMessage extends DocumentMessage { + + private String instanceId; + + DestroyVisitorMessage() { + // must be deserialized into + } + + public DestroyVisitorMessage(String instanceId) { + this.instanceId = instanceId; + } + + public DestroyVisitorMessage(DestroyVisitorMessage cmd) { + instanceId = cmd.instanceId; + } + + public String getInstanceId() { + return instanceId; + } + + public void setInstanceId(String instanceId) { + this.instanceId = instanceId; + } + + @Override + public DocumentReply createReply() { + return new DocumentReply(DocumentProtocol.REPLY_DESTROYVISITOR); + } + + @Override + public int getType() { + return DocumentProtocol.MESSAGE_DESTROYVISITOR; + } +} diff --git a/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/DocumentAcceptedReply.java b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/DocumentAcceptedReply.java new file mode 100644 index 00000000000..f2bbcd281b6 --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/DocumentAcceptedReply.java @@ -0,0 +1,12 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi.messagebus.protocol; + +/** + * Common base class for replies that indicate that a document was routed + * to some recipient. Does not imply that the reply contains no errors! + */ +public abstract class DocumentAcceptedReply extends DocumentReply { + protected DocumentAcceptedReply(int type) { + super(type); + } +} diff --git a/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/DocumentIgnoredReply.java b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/DocumentIgnoredReply.java new file mode 100644 index 00000000000..3993c95f31f --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/DocumentIgnoredReply.java @@ -0,0 +1,8 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi.messagebus.protocol; + +public class DocumentIgnoredReply extends DocumentReply { + public DocumentIgnoredReply() { + super(DocumentProtocol.REPLY_DOCUMENTIGNORED); + } +} diff --git a/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/DocumentListEntry.java b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/DocumentListEntry.java new file mode 100755 index 00000000000..8de0cfd204c --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/DocumentListEntry.java @@ -0,0 +1,47 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi.messagebus.protocol;
+
+import com.yahoo.document.Document;
+import com.yahoo.document.serialization.DocumentDeserializer;
+import com.yahoo.vespa.objects.Serializer;
+
+public class DocumentListEntry {
+
+ private Document doc;
+ private long timestamp;
+ private boolean removeEntry;
+
+ public DocumentListEntry(Document doc, long timestamp, boolean removeEntry) {
+ this.doc = doc;
+ this.timestamp = timestamp;
+ this.removeEntry = removeEntry;
+ }
+
+ public void serialize(Serializer buf) {
+ buf.putLong(null, timestamp);
+ doc.serialize(buf);
+ buf.putByte(null, (byte)(removeEntry ? 1 : 0));
+ }
+
+ public static int getApproxSize() {
+ return 60; // optimzation. approximation is sufficient
+ }
+
+ public DocumentListEntry(DocumentDeserializer buf) {
+ timestamp = buf.getLong(null);
+ doc = new Document(buf);
+ removeEntry = buf.getByte(null) > 0;
+ }
+
+ public Document getDocument() {
+ return doc;
+ }
+
+ public long getTimestamp() {
+ return timestamp;
+ }
+
+ public boolean isRemoveEntry() {
+ return removeEntry;
+ }
+}
diff --git a/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/DocumentListMessage.java b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/DocumentListMessage.java new file mode 100755 index 00000000000..448c2820ec3 --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/DocumentListMessage.java @@ -0,0 +1,54 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi.messagebus.protocol;
+
+import com.yahoo.document.BucketId;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class DocumentListMessage extends VisitorMessage {
+
+ private BucketId bucket = new BucketId(16, 0);
+ private final List<DocumentListEntry> entries = new ArrayList<DocumentListEntry>();
+
+ public DocumentListMessage() {
+ // empty
+ }
+
+ public DocumentListMessage(DocumentListMessage cmd) {
+ bucket = cmd.bucket;
+ entries.addAll(cmd.entries);
+ }
+
+ public BucketId getBucketId() {
+ return bucket;
+ }
+
+ public void setBucketId(BucketId id) {
+ bucket = id;
+ }
+
+ public List<DocumentListEntry> getDocuments() {
+ return entries;
+ }
+
+ @Override
+ public DocumentReply createReply() {
+ return new VisitorReply(DocumentProtocol.REPLY_DOCUMENTLIST);
+ }
+
+ @Override
+ public int getType() {
+ return DocumentProtocol.MESSAGE_DOCUMENTLIST;
+ }
+
+ @Override
+ public int getApproxSize() {
+ return DocumentListEntry.getApproxSize() * entries.size();
+ }
+
+ @Override
+ public String toString() {
+ return "DocumentListMessage(" + entries.toString() + ")";
+ }
+}
diff --git a/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/DocumentMessage.java b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/DocumentMessage.java new file mode 100755 index 00000000000..c4839c87f69 --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/DocumentMessage.java @@ -0,0 +1,85 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi.messagebus.protocol;
+
+import com.yahoo.documentapi.messagebus.loadtypes.LoadType;
+import com.yahoo.messagebus.Message;
+import com.yahoo.messagebus.Routable;
+import com.yahoo.text.Utf8String;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public abstract class DocumentMessage extends Message {
+
+ private DocumentProtocol.Priority priority = DocumentProtocol.Priority.NORMAL_3;
+ private LoadType loadType = LoadType.DEFAULT;
+
+ /**
+ * Constructs a new message with no content.
+ */
+ public DocumentMessage() {
+ // empty
+ }
+
+ /**
+ * Creates and returns a reply to this message.
+ *
+ * @return The created reply.
+ */
+ public abstract DocumentReply createReply();
+
+ @Override
+ public void swapState(Routable rhs) {
+ super.swapState(rhs);
+ if (rhs instanceof DocumentMessage) {
+ DocumentMessage msg = (DocumentMessage)rhs;
+
+ DocumentProtocol.Priority pri = this.priority;
+ this.priority = msg.priority;
+ msg.priority = pri;
+
+ LoadType lt = this.loadType;
+ this.loadType = msg.loadType;
+ msg.loadType = lt;
+ }
+ }
+
+ /**
+ * Returns the priority tag for this message. This is an optional tag added for VDS that is not interpreted by the
+ * document protocol.
+ *
+ * @return The priority.
+ */
+ public DocumentProtocol.Priority getPriority() { return priority; }
+
+ /**
+ * Sets the priority tag for this message.
+ *
+ * @param priority The priority to set.
+ */
+ public void setPriority(DocumentProtocol.Priority priority) {
+ this.priority = priority;
+ }
+
+ public LoadType getLoadType() {
+ return loadType;
+ }
+
+ public void setLoadType(LoadType loadType) {
+ if (loadType != null) {
+ this.loadType = loadType;
+ } else {
+ this.loadType = LoadType.DEFAULT;
+ }
+ }
+
+ @Override
+ public int getApproxSize() {
+ return 4 + 1; // type + priority
+ }
+
+ @Override
+ public Utf8String getProtocol() {
+ return DocumentProtocol.NAME;
+ }
+}
diff --git a/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/DocumentProtocol.java b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/DocumentProtocol.java new file mode 100755 index 00000000000..e4988d3c8b8 --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/DocumentProtocol.java @@ -0,0 +1,578 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi.messagebus.protocol; + +import com.google.common.annotations.Beta; +import com.yahoo.collections.Tuple2; +import com.yahoo.component.Version; +import com.yahoo.component.VersionSpecification; +import com.yahoo.document.DocumentTypeManager; +import com.yahoo.document.DocumentTypeManagerConfigurer; +import com.yahoo.documentapi.messagebus.loadtypes.LoadTypeSet; +import com.yahoo.documentapi.metrics.DocumentProtocolMetricSet; +import com.yahoo.messagebus.*; +import com.yahoo.messagebus.metrics.MetricSet; +import com.yahoo.messagebus.routing.RoutingContext; +import com.yahoo.messagebus.routing.RoutingNodeIterator; +import com.yahoo.messagebus.routing.RoutingPolicy; +import com.yahoo.text.Utf8String; + +import java.util.*; +import java.util.logging.Logger; + +/** + * Implements the message bus protocol that is used by all components of Vespa. + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class DocumentProtocol implements Protocol { + + private static final Logger log = Logger.getLogger(DocumentProtocol.class.getName()); + private final DocumentProtocolMetricSet metrics = new DocumentProtocolMetricSet(); + private final RoutingPolicyRepository routingPolicyRepository = new RoutingPolicyRepository(metrics); + private final RoutableRepository routableRepository; + private final DocumentTypeManager docMan; + + /** + * The name of this protocol. + */ + public static final Utf8String NAME = new Utf8String("document"); + + /** + * All message types that are implemented by this protocol. + */ + public static final int DOCUMENT_MESSAGE = 100000; + public static final int MESSAGE_GETDOCUMENT = DOCUMENT_MESSAGE + 3; + public static final int MESSAGE_PUTDOCUMENT = DOCUMENT_MESSAGE + 4; + public static final int MESSAGE_REMOVEDOCUMENT = DOCUMENT_MESSAGE + 5; + public static final int MESSAGE_UPDATEDOCUMENT = DOCUMENT_MESSAGE + 6; + public static final int MESSAGE_CREATEVISITOR = DOCUMENT_MESSAGE + 7; + public static final int MESSAGE_DESTROYVISITOR = DOCUMENT_MESSAGE + 8; + public static final int MESSAGE_VISITORINFO = DOCUMENT_MESSAGE + 9; + public static final int MESSAGE_SEARCHRESULT = DOCUMENT_MESSAGE + 11; + public static final int MESSAGE_DOCUMENTSUMMARY = DOCUMENT_MESSAGE + 14; + public static final int MESSAGE_MAPVISITOR = DOCUMENT_MESSAGE + 15; + public static final int MESSAGE_GETBUCKETSTATE = DOCUMENT_MESSAGE + 18; + public static final int MESSAGE_STATBUCKET = DOCUMENT_MESSAGE + 19; + public static final int MESSAGE_GETBUCKETLIST = DOCUMENT_MESSAGE + 20; + public static final int MESSAGE_DOCUMENTLIST = DOCUMENT_MESSAGE + 21; + public static final int MESSAGE_EMPTYBUCKETS = DOCUMENT_MESSAGE + 23; + public static final int MESSAGE_REMOVELOCATION = DOCUMENT_MESSAGE + 24; + public static final int MESSAGE_QUERYRESULT = DOCUMENT_MESSAGE + 25; + public static final int MESSAGE_BATCHDOCUMENTUPDATE = DOCUMENT_MESSAGE + 26; + + /** + * All reply types that are implemented by this protocol. + */ + public static final int DOCUMENT_REPLY = 200000; + public static final int REPLY_GETDOCUMENT = DOCUMENT_REPLY + 3; + public static final int REPLY_PUTDOCUMENT = DOCUMENT_REPLY + 4; + public static final int REPLY_REMOVEDOCUMENT = DOCUMENT_REPLY + 5; + public static final int REPLY_UPDATEDOCUMENT = DOCUMENT_REPLY + 6; + public static final int REPLY_CREATEVISITOR = DOCUMENT_REPLY + 7; + public static final int REPLY_DESTROYVISITOR = DOCUMENT_REPLY + 8; + public static final int REPLY_VISITORINFO = DOCUMENT_REPLY + 9; + public static final int REPLY_SEARCHRESULT = DOCUMENT_REPLY + 11; + public static final int REPLY_DOCUMENTSUMMARY = DOCUMENT_REPLY + 14; + public static final int REPLY_MAPVISITOR = DOCUMENT_REPLY + 15; + public static final int REPLY_GETBUCKETSTATE = DOCUMENT_REPLY + 18; + public static final int REPLY_STATBUCKET = DOCUMENT_REPLY + 19; + public static final int REPLY_GETBUCKETLIST = DOCUMENT_REPLY + 20; + public static final int REPLY_DOCUMENTLIST = DOCUMENT_REPLY + 21; + public static final int REPLY_EMPTYBUCKETS = DOCUMENT_REPLY + 23; + public static final int REPLY_REMOVELOCATION = DOCUMENT_REPLY + 24; + public static final int REPLY_QUERYRESULT = DOCUMENT_REPLY + 25; + public static final int REPLY_BATCHDOCUMENTUPDATE = DOCUMENT_REPLY + 26; + public static final int REPLY_WRONGDISTRIBUTION = DOCUMENT_REPLY + 1000; + public static final int REPLY_DOCUMENTIGNORED = DOCUMENT_REPLY + 1001; + + /** + * Important note on adding new error codes to the Document protocol: + * + * Changes to this protocol must be reflected in both the Java and C++ versions + * of the code. Furthermore, ErrorCodesTest must be updated across both languages + * to include the new error code. Otherwise, cross-language correctness may no + * longer be guaranteed. + */ + + /** + * Used by policies to indicate an inappropriate message. + */ + public static final int ERROR_MESSAGE_IGNORED = ErrorCode.APP_FATAL_ERROR + 1; + /** + * Used for error policy when policy creation failed. + */ + public static final int ERROR_POLICY_FAILURE = ErrorCode.APP_FATAL_ERROR + 2; + /** + * Document in operation cannot be found. (VDS Get and Remove) + */ + public static final int ERROR_DOCUMENT_NOT_FOUND = ErrorCode.APP_FATAL_ERROR + 1001; + /** + * Operation cannot be performed because token already exist. (Create bucket, create visitor) + */ + public static final int ERROR_DOCUMENT_EXISTS = ErrorCode.APP_FATAL_ERROR + 1002; + /** + * Node have not implemented support for the given operation. + */ + public static final int ERROR_NOT_IMPLEMENTED = ErrorCode.APP_FATAL_ERROR + 1004; + /** + * Parameters given in request is illegal. + */ + public static final int ERROR_ILLEGAL_PARAMETERS = ErrorCode.APP_FATAL_ERROR + 1005; + /** + * Unknown request received. (New client requesting from old server) + */ + public static final int ERROR_UNKNOWN_COMMAND = ErrorCode.APP_FATAL_ERROR + 1007; + /** + * Request cannot be decoded. + */ + public static final int ERROR_UNPARSEABLE = ErrorCode.APP_FATAL_ERROR + 1008; + /** + * Not enough free space on disk to perform operation. + */ + public static final int ERROR_NO_SPACE = ErrorCode.APP_FATAL_ERROR + 1009; + /** + * Request was not handled correctly. + */ + public static final int ERROR_IGNORED = ErrorCode.APP_FATAL_ERROR + 1010; + /** + * We failed in some way we didn't expect to fail. + */ + public static final int ERROR_INTERNAL_FAILURE = ErrorCode.APP_FATAL_ERROR + 1011; + /** + * Node refuse to perform operation. (Illegally formed message?) + */ + public static final int ERROR_REJECTED = ErrorCode.APP_FATAL_ERROR + 1012; + /** + * Test and set condition (selection) failed. + */ + @Beta + public static final int ERROR_TEST_AND_SET_CONDITION_FAILED = ErrorCode.APP_FATAL_ERROR + 1013; + + /** + * Failed to process the given request. (Used by docproc) + */ + public static final int ERROR_PROCESSING_FAILURE = ErrorCode.APP_FATAL_ERROR + 2001; + /** Unique timestamp specified for new operation is already in use. */ + public static final int ERROR_TIMESTAMP_EXIST = ErrorCode.APP_FATAL_ERROR + 2002; + /** + * Node not ready to perform operation. (Initializing VDS nodes) + */ + public static final int ERROR_NODE_NOT_READY = ErrorCode.APP_TRANSIENT_ERROR + 1001; + /** + * Wrong node to talk to in current state. (VDS system state disagreement) + */ + public static final int ERROR_WRONG_DISTRIBUTION = ErrorCode.APP_TRANSIENT_ERROR + 1002; + /** + * Operation cut short and aborted. (Destroy visitor, node stopping) + */ + public static final int ERROR_ABORTED = ErrorCode.APP_TRANSIENT_ERROR + 1004; + /** + * Node too busy to process request (Typically full queues) + */ + public static final int ERROR_BUSY = ErrorCode.APP_TRANSIENT_ERROR + 1005; + /** + * Lost connection with the node we requested something from. + */ + public static final int ERROR_NOT_CONNECTED = ErrorCode.APP_TRANSIENT_ERROR + 1006; + /** + * We failed accessing the disk, which we think is a disk hardware problem. + */ + public static final int ERROR_DISK_FAILURE = ErrorCode.APP_TRANSIENT_ERROR + 1007; + /** + * We failed during an IO operation, we dont think is a specific disk hardware problem. + */ + public static final int ERROR_IO_FAILURE = ErrorCode.APP_TRANSIENT_ERROR + 1008; + /** + * Bucket given in operation not found due to bucket database + * inconsistencies between storage and distributor nodes. + */ + public static final int ERROR_BUCKET_NOT_FOUND = ErrorCode.APP_TRANSIENT_ERROR + 1009; + /** + * Bucket recently removed, such that operation cannot be performed. + * Differs from BUCKET_NOT_FOUND in that there is no db inconsistency. + */ + public static final int ERROR_BUCKET_DELETED = ErrorCode.APP_TRANSIENT_ERROR + 1012; + /** + * Storage node received a timestamp that is stale. Likely clock skew. + */ + public static final int ERROR_STALE_TIMESTAMP = ErrorCode.APP_TRANSIENT_ERROR + 1013; + + /** + * The given node have gotten a critical error and have suspended itself. + */ + public static final int ERROR_SUSPENDED = ErrorCode.APP_TRANSIENT_ERROR + 2001; + + /** + * <p>Define the different priorities allowed for document api messages. Most user traffic should be fit into the + * NORMAL categories. Traffic in the HIGH end will be usually be prioritized over important maintenance operations. + * Traffic in the LOW end will be prioritized after these operations.</p> + */ + public static enum Priority { + HIGHEST(0), + VERY_HIGH(1), + HIGH_1(2), + HIGH_2(3), + HIGH_3(4), + NORMAL_1(5), + NORMAL_2(6), + NORMAL_3(7), + NORMAL_4(8), + NORMAL_5(9), + NORMAL_6(10), + LOW_1(11), + LOW_2(12), + LOW_3(13), + VERY_LOW(14), + LOWEST(15); + + private final int val; + + private Priority(int val) { + this.val = val; + } + + public int getValue() { + return val; + } + } + + /** + * Get a priority enum instance by its value. + * + * @param val The value of the priority to return. + * @return The priority enum instance. + * @throws IllegalArgumentException If priority value is unknown. + */ + public static Priority getPriority(int val) { + for (Priority pri : Priority.values()) { + if (val == pri.val) { + return pri; + } + } + throw new IllegalArgumentException("Unknown priority: " + val); + } + + /** + * Get priority enum instance by its name. + * + * @param name Name of priority. + * @return Priority enum instance, given that <code>name</code> is valid. + * @throws IllegalArgumentException If priority name is unknown. + */ + public static Priority getPriorityByName(String name) { + return Priority.valueOf(name); + } + + public DocumentProtocol(DocumentTypeManager docMan) { + this(docMan, null, new LoadTypeSet()); + } + + public DocumentProtocol(DocumentTypeManager docMan, String configId) { + this(docMan, configId, new LoadTypeSet()); + } + + public DocumentProtocol(DocumentTypeManager docMan, String configId, LoadTypeSet set) { + // Prepare config string for routing policy factories. + String cfg = (configId == null ? "client" : configId); + if (docMan != null) { + this.docMan = docMan; + } else { + this.docMan = new DocumentTypeManager(); + DocumentTypeManagerConfigurer.configure(this.docMan, cfg); + } + routableRepository = new RoutableRepository(set); + + // When adding factories to this list, please KEEP THEM ORDERED alphabetically like they are now. + putRoutingPolicyFactory("AND", new RoutingPolicyFactories.AndPolicyFactory()); + putRoutingPolicyFactory("Content", new RoutingPolicyFactories.ContentPolicyFactory()); + putRoutingPolicyFactory("DocumentRouteSelector", new RoutingPolicyFactories.DocumentRouteSelectorPolicyFactory(cfg)); + putRoutingPolicyFactory("Extern", new RoutingPolicyFactories.ExternPolicyFactory()); + putRoutingPolicyFactory("LocalService", new RoutingPolicyFactories.LocalServicePolicyFactory()); + putRoutingPolicyFactory("MessageType", new RoutingPolicyFactories.MessageTypePolicyFactory(cfg)); + putRoutingPolicyFactory("RoundRobin", new RoutingPolicyFactories.RoundRobinPolicyFactory()); + putRoutingPolicyFactory("LoadBalancer", new RoutingPolicyFactories.LoadBalancerPolicyFactory()); + putRoutingPolicyFactory("SearchColumn", new RoutingPolicyFactories.SearchColumnPolicyFactory()); + putRoutingPolicyFactory("SearchRow", new RoutingPolicyFactories.SearchRowPolicyFactory()); + putRoutingPolicyFactory("Storage", new RoutingPolicyFactories.StoragePolicyFactory()); + putRoutingPolicyFactory("SubsetService", new RoutingPolicyFactories.SubsetServicePolicyFactory()); + + // Prepare version specifications to use when adding routable factories. + VersionSpecification version50 = new VersionSpecification(5, 0); + VersionSpecification version51 = new VersionSpecification(5, 1); + VersionSpecification version52 = new VersionSpecification(5, 115); + + List<VersionSpecification> from50 = Arrays.asList(version50, version51, version52); + List<VersionSpecification> from51 = Arrays.asList(version51, version52); + List<VersionSpecification> from52 = Arrays.asList(version52); + + // 5.0 serialization (keep alphabetized please) + putRoutableFactory(MESSAGE_BATCHDOCUMENTUPDATE, new RoutableFactories50.BatchDocumentUpdateMessageFactory(), from50); + putRoutableFactory(MESSAGE_CREATEVISITOR, new RoutableFactories50.CreateVisitorMessageFactory(), from50); + putRoutableFactory(MESSAGE_DESTROYVISITOR, new RoutableFactories50.DestroyVisitorMessageFactory(), from50); + putRoutableFactory(MESSAGE_DOCUMENTLIST, new RoutableFactories50.DocumentListMessageFactory(), from50); + putRoutableFactory(MESSAGE_DOCUMENTSUMMARY, new RoutableFactories50.DocumentSummaryMessageFactory(), from50); + putRoutableFactory(MESSAGE_EMPTYBUCKETS, new RoutableFactories50.EmptyBucketsMessageFactory(), from50); + putRoutableFactory(MESSAGE_GETBUCKETLIST, new RoutableFactories50.GetBucketListMessageFactory(), from50); + putRoutableFactory(MESSAGE_GETBUCKETSTATE, new RoutableFactories50.GetBucketStateMessageFactory(), from50); + putRoutableFactory(MESSAGE_GETDOCUMENT, new RoutableFactories50.GetDocumentMessageFactory(), from50); + putRoutableFactory(MESSAGE_MAPVISITOR, new RoutableFactories50.MapVisitorMessageFactory(), from50); + putRoutableFactory(MESSAGE_PUTDOCUMENT, new RoutableFactories50.PutDocumentMessageFactory(), from50); + putRoutableFactory(MESSAGE_QUERYRESULT, new RoutableFactories50.QueryResultMessageFactory(), from50); + putRoutableFactory(MESSAGE_REMOVEDOCUMENT, new RoutableFactories50.RemoveDocumentMessageFactory(), from50); + putRoutableFactory(MESSAGE_REMOVELOCATION, new RoutableFactories50.RemoveLocationMessageFactory(), from50); + putRoutableFactory(MESSAGE_SEARCHRESULT, new RoutableFactories50.SearchResultMessageFactory(), from50); + putRoutableFactory(MESSAGE_STATBUCKET, new RoutableFactories50.StatBucketMessageFactory(), from50); + putRoutableFactory(MESSAGE_UPDATEDOCUMENT, new RoutableFactories50.UpdateDocumentMessageFactory(), from50); + putRoutableFactory(MESSAGE_VISITORINFO, new RoutableFactories50.VisitorInfoMessageFactory(), from50); + putRoutableFactory(REPLY_BATCHDOCUMENTUPDATE, new RoutableFactories50.BatchDocumentUpdateReplyFactory(), from50); + putRoutableFactory(REPLY_CREATEVISITOR, new RoutableFactories50.CreateVisitorReplyFactory(), from50); + putRoutableFactory(REPLY_DESTROYVISITOR, new RoutableFactories50.DestroyVisitorReplyFactory(), from50); + putRoutableFactory(REPLY_DOCUMENTLIST, new RoutableFactories50.DocumentListReplyFactory(), from50); + putRoutableFactory(REPLY_DOCUMENTSUMMARY, new RoutableFactories50.DocumentSummaryReplyFactory(), from50); + putRoutableFactory(REPLY_EMPTYBUCKETS, new RoutableFactories50.EmptyBucketsReplyFactory(), from50); + putRoutableFactory(REPLY_GETBUCKETLIST, new RoutableFactories50.GetBucketListReplyFactory(), from50); + putRoutableFactory(REPLY_GETBUCKETSTATE, new RoutableFactories50.GetBucketStateReplyFactory(), from50); + putRoutableFactory(REPLY_GETDOCUMENT, new RoutableFactories50.GetDocumentReplyFactory(), from50); + putRoutableFactory(REPLY_MAPVISITOR, new RoutableFactories50.MapVisitorReplyFactory(), from50); + putRoutableFactory(REPLY_PUTDOCUMENT, new RoutableFactories50.PutDocumentReplyFactory(), from50); + putRoutableFactory(REPLY_QUERYRESULT, new RoutableFactories50.QueryResultReplyFactory(), from50); + putRoutableFactory(REPLY_REMOVEDOCUMENT, new RoutableFactories50.RemoveDocumentReplyFactory(), from50); + putRoutableFactory(REPLY_REMOVELOCATION, new RoutableFactories50.RemoveLocationReplyFactory(), from50); + putRoutableFactory(REPLY_SEARCHRESULT, new RoutableFactories50.SearchResultReplyFactory(), from50); + putRoutableFactory(REPLY_STATBUCKET, new RoutableFactories50.StatBucketReplyFactory(), from50); + putRoutableFactory(REPLY_UPDATEDOCUMENT, new RoutableFactories50.UpdateDocumentReplyFactory(), from50); + putRoutableFactory(REPLY_UPDATEDOCUMENT, new RoutableFactories50.UpdateDocumentReplyFactory(), from50); + putRoutableFactory(REPLY_VISITORINFO, new RoutableFactories50.VisitorInfoReplyFactory(), from50); + putRoutableFactory(REPLY_WRONGDISTRIBUTION, new RoutableFactories50.WrongDistributionReplyFactory(), from50); + + // 5.1 serialization + putRoutableFactory(MESSAGE_CREATEVISITOR, new RoutableFactories51.CreateVisitorMessageFactory(), from51); + putRoutableFactory(MESSAGE_GETDOCUMENT, new RoutableFactories51.GetDocumentMessageFactory(), from51); + putRoutableFactory(REPLY_DOCUMENTIGNORED, new RoutableFactories51.DocumentIgnoredReplyFactory(), from51); + + // 5.2 serialization + putRoutableFactory(MESSAGE_PUTDOCUMENT, new RoutableFactories52.PutDocumentMessageFactory(), from52); + putRoutableFactory(MESSAGE_UPDATEDOCUMENT, new RoutableFactories52.UpdateDocumentMessageFactory(), from52); + putRoutableFactory(MESSAGE_REMOVEDOCUMENT, new RoutableFactories52.RemoveDocumentMessageFactory(), from52); + } + + /** + * Adds a new routable factory to this protocol. This method is thread-safe, and may be invoked on a protocol object + * that is already in use by a message bus instance. Notice that the name you supply for a factory is the + * case-sensitive name that will be referenced by routes. + * + * @param name The name of the factory to add. + * @param factory The factory to add. + * @return This, to allow chaining. + */ + public DocumentProtocol putRoutingPolicyFactory(String name, RoutingPolicyFactory factory) { + routingPolicyRepository.putFactory(name, factory); + return this; + } + + /** + * Adds a new routable factory to this protocol. This method is thread-safe, and may be invoked on a protocol object + * that is already in use by a message bus instance. Notice that you must explicitly register a factory for each + * supported version. You can always bypass this by passing a default version specification object to this function, + * because that object will match any version. + * + * @param type The routable type to assign a factory to. + * @param factory The factory to add. + * @param version The version for which this factory can be used. + * @return This, to allow chaining. + */ + public DocumentProtocol putRoutableFactory(int type, RoutableFactory factory, VersionSpecification version) { + routableRepository.putFactory(version, type, factory); + return this; + } + + /** + * Convenience method to call {@link #putRoutableFactory(int, RoutableFactory, com.yahoo.component.VersionSpecification)} + * for multiple version specifications. + * + * @param type The routable type to assign a factory to. + * @param factory The factory to add. + * @param versions The versions for which this factory can be used. + * @return This, to allow chaining. + */ + public DocumentProtocol putRoutableFactory(int type, RoutableFactory factory, List<VersionSpecification> versions) { + for (VersionSpecification version : versions) { + putRoutableFactory(type, factory, version); + } + return this; + } + + /** + * Returns a string representation of the given error code. + * + * @param code The code whose string symbol to return. + * @return The error string. + */ + public static String getErrorName(int code) { + switch (code) { + case ERROR_MESSAGE_IGNORED: + return "MESSAGE_IGNORED"; + case ERROR_DOCUMENT_NOT_FOUND: + return "DOCUMENT_NOT_FOUND"; + case ERROR_DOCUMENT_EXISTS: + return "DOCUMENT_EXISTS"; + case ERROR_BUCKET_NOT_FOUND: + return "BUCKET_NOT_FOUND"; + case ERROR_BUCKET_DELETED: + return "BUCKET_DELETED"; + case ERROR_NOT_IMPLEMENTED: + return "NOT_IMPLEMENTED"; + case ERROR_ILLEGAL_PARAMETERS: + return "ILLEGAL_PARAMETERS"; + case ERROR_IGNORED: + return "IGNORED"; + case ERROR_UNKNOWN_COMMAND: + return "UNKNOWN_COMMAND"; + case ERROR_UNPARSEABLE: + return "UNPARSEABLE"; + case ERROR_NO_SPACE: + return "NO_SPACE"; + case ERROR_INTERNAL_FAILURE: + return "INTERNAL_FAILURE"; + case ERROR_PROCESSING_FAILURE: + return "PROCESSING_FAILURE"; + case ERROR_TIMESTAMP_EXIST: + return "TIMESTAMP_EXIST"; + case ERROR_NODE_NOT_READY: + return "NODE_NOT_READY"; + case ERROR_WRONG_DISTRIBUTION: + return "WRONG_DISTRIBUTION"; + case ERROR_REJECTED: + return "REJECTED"; + case ERROR_ABORTED: + return "ABORTED"; + case ERROR_BUSY: + return "BUSY"; + case ERROR_NOT_CONNECTED: + return "NOT_CONNECTED"; + case ERROR_DISK_FAILURE: + return "DISK_FAILURE"; + case ERROR_IO_FAILURE: + return "IO_FAILURE"; + case ERROR_SUSPENDED: + return "SUSPENDED"; + case ERROR_TEST_AND_SET_CONDITION_FAILED: + return "TEST_AND_SET_CONDITION_FAILED"; + default: + return ErrorCode.getName(code); + } + } + + /** + * This is a convenient entry to the {@link #merge(RoutingContext,Set)} method by way of a routing context object. + * The replies of all child contexts are merged and stored in the context. + * + * @param ctx The context whose children to merge. + */ + public static void merge(RoutingContext ctx) { + merge(ctx, new HashSet<Integer>(0)); + } + + /** + * This method implements the common way to merge document replies for whatever routing policy. In case of an error + * in any of the replies, it will prepare an EmptyReply() and add all errors to it. If there are no errors, this + * method will use the first reply in the list and transfer whatever feed answers might exist in the replies to it. + * + * @param ctx The context whose children to merge. + * @param mask The indexes of the children to skip. + */ + public static void merge(RoutingContext ctx, Set<Integer> mask) { + List<Reply> replies = new LinkedList<>(); + for (RoutingNodeIterator it = ctx.getChildIterator(); + it.isValid(); it.next()) { + Reply ref = it.getReplyRef(); + replies.add(ref); + } + Tuple2<Integer, Reply> tuple = merge(replies, mask); + if (tuple.first != null) { + ctx.getChildIterator().skip(tuple.first).removeReply(); + } + ctx.setReply(tuple.second); + } + + private static Tuple2<Integer, Reply> merge(List<Reply> replies, Set<Integer> mask) { + ReplyMerger rm = new ReplyMerger(); + for (int i = 0; i < replies.size(); ++i) { + if (mask.contains(i)) { + continue; + } + rm.merge(i, replies.get(i)); + } + return rm.mergedReply(); + } + + /** + * This method implements the common way to merge document replies for whatever routing policy. In case of an error + * in any of the replies, it will prepare an EmptyReply() and add all errors to it. If there are no errors, this + * method will use the first reply in the list and transfer whatever feed answers might exist in the replies to it. + * + * + * @param replies The replies to merge. + * @return The merged Reply. + */ + public static Reply merge(List<Reply> replies) { + return merge(replies, new HashSet<Integer>(0)).second; + } + + /** + * Returns true if the given reply has at least one error, and all errors are of the given type. + * + * @param reply The reply to check for error. + * @param errCode The error code to check for. + * @return Whether or not the reply has only the given error code. + */ + public static boolean hasOnlyErrorsOfType(Reply reply, int errCode) { + if (!reply.hasErrors()) { + return false; + } + for (int i = 0; i < reply.getNumErrors(); ++i) { + if (reply.getError(i).getCode() != errCode) { + return false; + } + } + return true; + } + + public String getName() { + return NAME.toString(); + } + + public RoutingPolicy createPolicy(String name, String param) { + return routingPolicyRepository.createPolicy(name, param); + } + + public byte[] encode(Version version, Routable routable) { + return routableRepository.encode(version, routable); + } + + public Routable decode(Version version, byte[] data) { + try { + return routableRepository.decode(docMan, version, data); + } catch (RuntimeException e) { + e.printStackTrace(); + log.warning(e.getMessage()); + return null; + } + } + + /** + * Returns a list of routable types that support the given version. + * + * @param version The version to return types for. + * @return The list of supported types. + */ + public List<Integer> getRoutableTypes(Version version) { + return routableRepository.getRoutableTypes(version); + } + + public MetricSet getMetrics() { + return metrics; + } + + final public DocumentTypeManager getDocumentTypeManager() { return docMan; } +} diff --git a/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/DocumentProtocolRoutingPolicy.java b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/DocumentProtocolRoutingPolicy.java new file mode 100644 index 00000000000..0f4bc33a944 --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/DocumentProtocolRoutingPolicy.java @@ -0,0 +1,16 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi.messagebus.protocol; + +import com.yahoo.messagebus.metrics.MetricSet; +import com.yahoo.messagebus.routing.RoutingPolicy; + +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +/** + * @author thomasg + */ +public interface DocumentProtocolRoutingPolicy extends RoutingPolicy { + public MetricSet getMetrics(); +} diff --git a/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/DocumentReply.java b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/DocumentReply.java new file mode 100755 index 00000000000..126d85c5703 --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/DocumentReply.java @@ -0,0 +1,53 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi.messagebus.protocol;
+
+import com.yahoo.messagebus.Reply;
+import com.yahoo.text.Utf8String;
+
+/**
+ * This class implements a generic document protocol reply that can be reused by document messages that require no
+ * special reply implementation while still allowing applications to distinguish between types.
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class DocumentReply extends Reply {
+
+ private final int type;
+ private DocumentProtocol.Priority priority = DocumentProtocol.Priority.NORMAL_3;
+
+ /**
+ * Constructs a new reply of given type.
+ *
+ * @param type The type code to assign to this.
+ */
+ public DocumentReply(int type) {
+ this.type = type;
+ }
+
+ /**
+ * Returns the priority tag for this message.
+ * @return The priority.
+ */
+ public DocumentProtocol.Priority getPriority() {
+ return priority;
+ }
+
+ /**
+ * Sets the priority tag for this message.
+ *
+ * @param priority The priority to set.
+ */
+ public void setPriority(DocumentProtocol.Priority priority) {
+ this.priority = priority;
+ }
+
+ @Override
+ public Utf8String getProtocol() {
+ return DocumentProtocol.NAME;
+ }
+
+ @Override
+ public final int getType() {
+ return type;
+ }
+}
diff --git a/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/DocumentRouteSelectorPolicy.java b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/DocumentRouteSelectorPolicy.java new file mode 100755 index 00000000000..276732a494a --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/DocumentRouteSelectorPolicy.java @@ -0,0 +1,179 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi.messagebus.protocol; + +import com.yahoo.config.subscription.ConfigSubscriber; +import com.yahoo.document.Document; +import com.yahoo.document.DocumentPut; +import com.yahoo.document.DocumentType; +import com.yahoo.document.select.DocumentSelector; +import com.yahoo.document.select.Result; +import com.yahoo.log.LogLevel; +import com.yahoo.messagebus.Message; +import com.yahoo.messagebus.metrics.MetricSet; +import com.yahoo.messagebus.routing.Route; +import com.yahoo.messagebus.routing.RoutingContext; + +import java.util.HashMap; +import java.util.Map; +import java.util.logging.Logger; + +/** + * This policy is responsible for selecting among the given recipient routes according to the configured document + * selection properties. To facilitate this the "routing" plugin in the vespa model builds a mapping from the route + * names to a document selector and a feed name of every search cluster. This can very well be extended to include + * storage at a later time. + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class DocumentRouteSelectorPolicy + implements DocumentProtocolRoutingPolicy, ConfigSubscriber.SingleSubscriber<DocumentrouteselectorpolicyConfig> { + + private static Logger log = Logger.getLogger(DocumentRouteSelectorPolicy.class.getName()); + private Map<String, DocumentSelector> config; + private String error = "Not configured."; + private ConfigSubscriber subscriber; + + /** + * This policy is constructed with a configuration identifier that can be subscribed to for the document selector + * config. If the string is either null or empty it will default to the proper one. + * + * @param configId The configuration identifier to subscribe to. + */ + public DocumentRouteSelectorPolicy(String configId) { + subscriber = new ConfigSubscriber(); + subscriber.subscribe(this, DocumentrouteselectorpolicyConfig.class, configId); + } + + /** + * This is a safety mechanism to allow the constructor to fail and signal that it can not be used. + * + * @return The error string, or null if no error. + */ + public synchronized String getError() { + return error; + } + + /** + * This method is called when configuration arrives from the config server. The received config object is traversed + * and a local map is constructed and swapped with the current {@link #config} map. + * + * @param cfg The configuration object given by subscription. + */ + @Override + public void configure(DocumentrouteselectorpolicyConfig cfg) { + String error = null; + Map<String, DocumentSelector> config = new HashMap<>(); + for (int i = 0; i < cfg.route().size(); i++) { + DocumentrouteselectorpolicyConfig.Route route = cfg.route(i); + if (route.selector().isEmpty()) { + continue; + } + DocumentSelector selector; + try { + selector = new DocumentSelector(route.selector()); + log.log(LogLevel.CONFIG, "Selector for route '" + route.name() + "' is '" + selector + "'."); + } catch (com.yahoo.document.select.parser.ParseException e) { + error = "Error parsing selector '" + route.selector() + "' for route '" + route.name() + "; " + + e.getMessage(); + break; + } + config.put(route.name(), selector); + } + synchronized (this) { + this.config = config; + this.error = error; + } + } + + @Override + public void select(RoutingContext context) { + // Require that recipients have been configured. + if (context.getNumRecipients() == 0) { + context.setError(DocumentProtocol.ERROR_POLICY_FAILURE, + "No recipients configured."); + return; + } + + // Invoke private select method for each candidate recipient. + synchronized (this) { + if (error != null) { + context.setError(DocumentProtocol.ERROR_POLICY_FAILURE, error); + return; + } + for (int i = 0; i < context.getNumRecipients(); ++i) { + Route recipient = context.getRecipient(i); + String routeName = recipient.toString(); + if (select(context, routeName)) { + Route route = context.getMessageBus().getRoutingTable(DocumentProtocol.NAME).getRoute(routeName); + context.addChild(route != null ? route : recipient); + } + } + } + context.setSelectOnRetry(false); + + // Notify that no children were selected, this is to differentiate this from the NO_RECIPIENTS_FOR_ROUTE error + // that message bus will generate if there are no recipients and no reply. + if (context.getNumChildren() == 0) { + context.setReply(new DocumentIgnoredReply()); + } + } + + /** + * This method runs the selector associated with the given location on the content of the message. If the selector + * validates the location, this method returns true. + * + * @param context The routing context that contains the necessary data. + * @param routeName The candidate route whose selector to run. + * @return Whether or not to send to the given recipient. + */ + private boolean select(RoutingContext context, String routeName) { + if (config == null) { + return true; + } + DocumentSelector selector = config.get(routeName); + if (selector == null) { + return true; + } + + // Select based on message content. + Message msg = context.getMessage(); + switch (msg.getType()) { + + case DocumentProtocol.MESSAGE_PUTDOCUMENT: + return selector.accepts(((PutDocumentMessage)msg).getDocumentPut()) == Result.TRUE; + + case DocumentProtocol.MESSAGE_UPDATEDOCUMENT: + return selector.accepts(((UpdateDocumentMessage)msg).getDocumentUpdate()) != Result.FALSE; + + + case DocumentProtocol.MESSAGE_BATCHDOCUMENTUPDATE: + BatchDocumentUpdateMessage bdu = (BatchDocumentUpdateMessage)msg; + for (int i = 0; i < bdu.getUpdates().size(); i++) { + if (selector.accepts(bdu.getUpdates().get(i)) == Result.FALSE) { + return false; + } + } + return true; + + default: + return true; + } + } + + @Override + public void merge(RoutingContext context) { + DocumentProtocol.merge(context); + } + + @Override + public void destroy() { + if (subscriber != null) { + subscriber.close(); + } + } + + @Override + public MetricSet getMetrics() { + return null; + } +} diff --git a/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/DocumentState.java b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/DocumentState.java new file mode 100644 index 00000000000..3735296eb4f --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/DocumentState.java @@ -0,0 +1,131 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi.messagebus.protocol; + +import com.yahoo.document.DocumentId; +import com.yahoo.document.GlobalId; +import com.yahoo.text.Utf8; +import com.yahoo.vespa.objects.Deserializer; +import com.yahoo.vespa.objects.Serializer; + +/** + * @author <a href="mailto:einarmr@yahoo-inc.com">Einar M R Rosenvinge</a> +*/ +public class DocumentState implements Comparable { + private DocumentId docId; + private GlobalId gid; + private long timestamp; + private boolean removeEntry; + + public DocumentState(DocumentId docId, long timestamp, boolean removeEntry) { + this.docId = docId; + this.gid = new GlobalId(docId.getGlobalId()); + this.timestamp = timestamp; + this.removeEntry = removeEntry; + } + + public DocumentState(GlobalId gid, long timestamp, boolean removeEntry) { + this.gid = gid; + this.timestamp = timestamp; + this.removeEntry = removeEntry; + } + + public DocumentState(Deserializer buf) { + byte hasDocId = buf.getByte(null); + if (hasDocId == (byte) 1) { + docId = new DocumentId(buf); + } + gid = new GlobalId(buf); + timestamp = buf.getLong(null); + removeEntry = buf.getByte(null)>0; + } + + public DocumentId getDocId() { + return docId; + } + + public GlobalId getGid() { + return gid; + } + + public long getTimestamp() { + return timestamp; + } + + public boolean isRemoveEntry() { + return removeEntry; + } + + public void serialize(Serializer buf) { + if (docId != null) { + buf.putByte(null, (byte) 1); + docId.serialize(buf); + } else { + buf.putByte(null, (byte) 0); + } + gid.serialize(buf); + buf.putLong(null, timestamp); + buf.putByte(null, (byte)(removeEntry ? 1 : 0)); + } + + public int getSerializedSize() { + int size = 0; + if (docId != null) { + size += Utf8.byteCount(docId.toString()) + 1; + } + size += GlobalId.LENGTH; + size += 8; + size += 1; + return size; + } + + public int compareTo(Object o) { + DocumentState state = (DocumentState) o; + int comp = gid.compareTo(state.gid); + if (comp == 0) { + if (docId != null) { + if (state.docId != null) { + return docId.toString().compareTo(state.docId.toString()); + } else { + return 1; + } + } else if (state.docId != null){ + return -1; + } + } + return comp; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof DocumentState)) return false; + + DocumentState that = (DocumentState) o; + + if (removeEntry != that.removeEntry) return false; + if (timestamp != that.timestamp) return false; + if (docId != null ? !docId.equals(that.docId) : that.docId != null) return false; + return gid.equals(that.gid); + } + + @Override + public int hashCode() { + int result; + result = (docId != null ? docId.hashCode() : 0); + result = 31 * result + gid.hashCode(); + result = 31 * result + (int) (timestamp ^ (timestamp >>> 32)); + result = 31 * result + (removeEntry ? 1 : 0); + return result; + } + + @Override + public String toString() { + return "DocumentState{" + + "docId=" + docId + + ", gid=" + gid + + ", timestamp=" + timestamp + + ", removeEntry=" + removeEntry + + '}'; + } + +} diff --git a/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/DocumentSummaryMessage.java b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/DocumentSummaryMessage.java new file mode 100644 index 00000000000..f063d7885e4 --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/DocumentSummaryMessage.java @@ -0,0 +1,27 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi.messagebus.protocol; + +import com.yahoo.vdslib.DocumentSummary; + +public class DocumentSummaryMessage extends VisitorMessage { + + private DocumentSummary documentSummary = null; + + public void setDocumentSummary(DocumentSummary summary) { + documentSummary = summary; + } + + public DocumentSummary getResult() { + return documentSummary; + } + + @Override + public DocumentReply createReply() { + return new VisitorReply(DocumentProtocol.REPLY_DOCUMENTSUMMARY); + } + + @Override + public int getType() { + return DocumentProtocol.MESSAGE_DOCUMENTSUMMARY; + } +} diff --git a/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/EmptyBucketsMessage.java b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/EmptyBucketsMessage.java new file mode 100644 index 00000000000..43514a156bc --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/EmptyBucketsMessage.java @@ -0,0 +1,42 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi.messagebus.protocol; + +import com.yahoo.document.BucketId; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author banino + */ +public class EmptyBucketsMessage extends VisitorMessage { + + private final List<BucketId> bids = new ArrayList<BucketId>(); + + EmptyBucketsMessage() { + // must be deserialized into + } + + public EmptyBucketsMessage(List<BucketId> bids) { + this.bids.addAll(bids); + } + + public List<BucketId> getBucketIds() { + return bids; + } + + public void setBucketIds(List<BucketId> bids) { + this.bids.clear(); + this.bids.addAll(bids); + } + + @Override + public DocumentReply createReply() { + return new VisitorReply(DocumentProtocol.REPLY_EMPTYBUCKETS); + } + + @Override + public int getType() { + return DocumentProtocol.MESSAGE_EMPTYBUCKETS; + } +} diff --git a/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/ErrorPolicy.java b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/ErrorPolicy.java new file mode 100755 index 00000000000..0b7310ad2fb --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/ErrorPolicy.java @@ -0,0 +1,44 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi.messagebus.protocol;
+
+import com.yahoo.messagebus.EmptyReply;
+import com.yahoo.messagebus.metrics.MetricSet;
+import com.yahoo.messagebus.routing.RoutingContext;
+
+/**
+ * This policy assigns an error supplied at constructor time to the routing context when {@link #select(RoutingContext)}
+ * is invoked. This is useful for returning error states to the client instead of those auto-generated by mbus when a
+ * routing policy can not be created.
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class ErrorPolicy implements DocumentProtocolRoutingPolicy {
+
+ private final String msg;
+
+ /**
+ * Creates a new policy that will assign an {@link EmptyReply} with the given error to all routing contexts that
+ * invoke {@link #select(RoutingContext)}.
+ *
+ * @param msg The message of the error to assign.
+ */
+ public ErrorPolicy(String msg) {
+ this.msg = msg;
+ }
+
+ public void select(RoutingContext ctx) {
+ ctx.setError(DocumentProtocol.ERROR_POLICY_FAILURE, msg);
+ }
+
+ public void merge(RoutingContext ctx) {
+ throw new AssertionError("Routing should not pass terminated selection.");
+ }
+
+ public void destroy() {
+ }
+
+
+ public MetricSet getMetrics() {
+ return null;
+ }
+}
diff --git a/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/ExternPolicy.java b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/ExternPolicy.java new file mode 100755 index 00000000000..a843102f466 --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/ExternPolicy.java @@ -0,0 +1,147 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi.messagebus.protocol;
+
+import com.yahoo.jrt.Supervisor;
+import com.yahoo.jrt.Transport;
+import com.yahoo.jrt.slobrok.api.Mirror;
+import com.yahoo.jrt.slobrok.api.SlobrokList;
+import com.yahoo.messagebus.ErrorCode;
+import com.yahoo.messagebus.metrics.MetricSet;
+import com.yahoo.messagebus.routing.Hop;
+import com.yahoo.messagebus.routing.Route;
+import com.yahoo.messagebus.routing.RoutingContext;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * This policy implements the necessary logic to communicate with an external Vespa application and resolve its list of
+ * recipients using that other application's slobrok servers.
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class ExternPolicy implements DocumentProtocolRoutingPolicy {
+
+ private Supervisor orb = null;
+ private Mirror mirror = null;
+ private String pattern = null;
+ private String session = null;
+ private final String error;
+ private int offset = 0;
+ private int generation = 0;
+ private final List<Hop> recipients = new ArrayList<>();
+
+ /**
+ * Constructs a new instance of this policy. The argument given is the connection spec to the slobrok to use for
+ * resolving recipients, as well as the pattern to use when querying. This constructor does _not_ wait for the
+ * mirror to become ready.
+ *
+ * @param arg The slobrok connection spec.
+ */
+ public ExternPolicy(String arg) {
+ if (arg == null || arg.length() == 0) {
+ error = "Expected parameter, got empty string.";
+ return;
+ }
+ String[] args = arg.split(";", 2);
+ if (args.length != 2 || args[0].length() == 0 || args[1].length() == 0) {
+ error = "Expected parameter on the form '<spec>;<pattern>', got '" + arg + "'.";
+ return;
+ }
+ int pos = args[1].lastIndexOf('/');
+ if (pos < 0) {
+ error = "Expected pattern on the form '<service>/<session>', got '" + args[1] + "'.";
+ return;
+ }
+ SlobrokList slobroks = new SlobrokList();
+ slobroks.setup(args[0].split(","));
+ pattern = args[1];
+ session = pattern.substring(pos);
+ orb = new Supervisor(new Transport());
+ mirror = new Mirror(orb, slobroks);
+ error = null;
+ }
+
+ /**
+ * This is a safety mechanism to allow the constructor to fail and signal that it can not be used.
+ *
+ * @return The error string, or null if no error.
+ */
+ public String getError() {
+ return error;
+ }
+
+ /**
+ * Returns the slobrok mirror used by this policy to resolve external recipients.
+ *
+ * @return The external mirror.
+ */
+ public Mirror getMirror() {
+ return mirror;
+ }
+
+ /**
+ * Returns the appropriate recipient hop. This method provides synchronized access to the internal mirror.
+ *
+ * @return The recipient hop to use.
+ */
+ private synchronized Hop getRecipient() {
+ update();
+ if (recipients.isEmpty()) {
+ return null;
+ }
+ int offset = ++this.offset & Integer.MAX_VALUE; // mask signed bit because of modulo
+ return new Hop(recipients.get(offset % recipients.size()));
+ }
+
+ /**
+ * Updates the list of matching recipients by querying the extern slobrok.
+ */
+ private void update() {
+ int upd = mirror.updates();
+ if (generation != upd) {
+ generation = upd;
+ recipients.clear();
+ Mirror.Entry[] arr = mirror.lookup(pattern);
+ for (Mirror.Entry entry : arr) {
+ recipients.add(Hop.parse(entry.getSpec() + session));
+ }
+ }
+ }
+
+ @Override
+ public void finalize() throws Throwable {
+ super.finalize();
+ mirror.shutdown();
+ orb.transport().shutdown().join();
+ }
+
+ public void select(RoutingContext ctx) {
+ if (error != null) {
+ ctx.setError(DocumentProtocol.ERROR_POLICY_FAILURE, error);
+ } else if (mirror.ready()) {
+ Hop hop = getRecipient();
+ if (hop != null) {
+ Route route = new Route(ctx.getRoute());
+ route.setHop(0, hop);
+ ctx.addChild(route);
+ } else {
+ ctx.setError(ErrorCode.NO_ADDRESS_FOR_SERVICE,
+ "Could not resolve any recipients from '" + pattern + "'.");
+ }
+ } else {
+ ctx.setError(ErrorCode.APP_TRANSIENT_ERROR, "Extern slobrok not ready.");
+ }
+ }
+
+ public void merge(RoutingContext ctx) {
+ DocumentProtocol.merge(ctx);
+ }
+
+ public void destroy() {
+ }
+
+ public MetricSet getMetrics() {
+ return null;
+ }
+}
diff --git a/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/ExternalSlobrokPolicy.java b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/ExternalSlobrokPolicy.java new file mode 100644 index 00000000000..d60c8cb7b33 --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/ExternalSlobrokPolicy.java @@ -0,0 +1,121 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi.messagebus.protocol; + +import com.yahoo.config.subscription.ConfigSourceSet; +import com.yahoo.config.subscription.ConfigSubscriber; +import com.yahoo.jrt.Supervisor; +import com.yahoo.jrt.Transport; +import com.yahoo.jrt.slobrok.api.IMirror; +import com.yahoo.jrt.slobrok.api.Mirror; +import com.yahoo.jrt.slobrok.api.SlobrokList; +import com.yahoo.messagebus.routing.RoutingContext; +import com.yahoo.cloud.config.SlobroksConfig; + +import java.util.Map; + +/** + * Abstract class for policies that allow you to specify which slobrok to use for the + * routing. + */ +public abstract class ExternalSlobrokPolicy extends AsyncInitializationPolicy implements ConfigSubscriber.SingleSubscriber<SlobroksConfig> { + String error; + Supervisor orb = null; + Mirror mirror = null; + SlobrokList slobroks = null; + boolean firstTry = true; + private ConfigSubscriber subscriber; + String[] configSources = null; + String slobrokConfigId = "admin/slobrok.0"; + + + public ExternalSlobrokPolicy(Map<String, String> param) { + super(param); + + String conf = param.get("config"); + if (conf != null) { + configSources = conf.split(","); + } + + String slbrk = param.get("slobroks"); + if (slbrk != null) { + slobroks = new SlobrokList(); + slobroks.setup(slbrk.split(",")); + } + + if (slobroks != null || configSources != null) { + needAsynchronousInitialization(); + } + } + + @Override + public void init() { + if (slobroks != null) { + orb = new Supervisor(new Transport()); + mirror = new Mirror(orb, slobroks); + } + + if (configSources != null) { + if (mirror == null) { + orb = new Supervisor(new Transport()); + subscriber = subscribe(slobrokConfigId, new ConfigSourceSet(configSources)); + } + } + } + + private ConfigSubscriber subscribe(String configId, final ConfigSourceSet configSourceSet) { + ConfigSubscriber subscriber = new ConfigSubscriber(configSourceSet); + subscriber.subscribe(this, SlobroksConfig.class, configId); + return subscriber; + } + + public IMirror getMirror() { + return mirror; + } + + public Mirror.Entry[] lookup(RoutingContext context, String pattern) { + IMirror mirror1 = (mirror != null ? mirror : context.getMirror()); + + Mirror.Entry[] arr = mirror1.lookup(pattern); + + if ((arr.length == 0) && firstTry) { + synchronized(this) { + try { + int count = 0; + while (arr.length == 0 && count < 100) { + Thread.sleep(50); + arr = mirror1.lookup(pattern); + count++; + } + } catch (InterruptedException e) { + } + + } + } + + firstTry = false; + return arr; + } + + @Override + public synchronized void configure(SlobroksConfig config) { + String[] slist = new String[config.slobrok().size()]; + + for(int i = 0; i < config.slobrok().size(); i++) { + slist[i] = config.slobrok(i).connectionspec(); + } + if (slobroks == null) { + slobroks = new SlobrokList(); + } + slobroks.setup(slist); + if (mirror == null) { + mirror = new Mirror(orb, slobroks); + } + + } + + @Override + public void destroy() { + if (subscriber!=null) subscriber.close(); + } + +} diff --git a/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/GetBucketListMessage.java b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/GetBucketListMessage.java new file mode 100755 index 00000000000..d38aa2e94f2 --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/GetBucketListMessage.java @@ -0,0 +1,50 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi.messagebus.protocol;
+
+import com.yahoo.document.BucketId;
+
+public class GetBucketListMessage extends DocumentMessage {
+
+ private BucketId bucketId;
+
+ GetBucketListMessage() {
+ // must be deserialized into
+ }
+
+ public GetBucketListMessage(BucketId bucketId) {
+ this.bucketId = bucketId;
+ }
+
+ public BucketId getBucketId() {
+ return bucketId;
+ }
+
+ void setBucketId(BucketId id) {
+ bucketId = id;
+ }
+
+ @Override
+ public DocumentReply createReply() {
+ return new StatBucketReply();
+ }
+
+ @Override
+ public boolean hasSequenceId() {
+ return true;
+ }
+
+ @Override
+ public long getSequenceId() {
+ return bucketId.getRawId();
+ }
+
+ @Override
+ public int getApproxSize() {
+ return super.getApproxSize() + 8;
+ }
+
+ @Override
+ public int getType() {
+ return DocumentProtocol.MESSAGE_GETBUCKETLIST;
+ }
+}
diff --git a/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/GetBucketListReply.java b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/GetBucketListReply.java new file mode 100755 index 00000000000..07013507d91 --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/GetBucketListReply.java @@ -0,0 +1,70 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi.messagebus.protocol;
+
+import com.yahoo.document.BucketId;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class GetBucketListReply extends DocumentReply {
+
+ public static class BucketInfo {
+ BucketId bucket;
+ String bucketInformation;
+
+ BucketInfo() {
+ // must be deserialized into
+ }
+
+ public BucketInfo(BucketId bucket, String bucketInformation) {
+ this.bucket = bucket;
+ this.bucketInformation = bucketInformation;
+ }
+
+ public BucketId getBucketId() {
+ return bucket;
+ }
+
+ public String getBucketInformation() {
+ return bucketInformation;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof BucketInfo)) {
+ return false;
+ }
+ BucketInfo rhs = (BucketInfo)obj;
+ if (bucket == null) {
+ if (rhs.bucket != null) {
+ return false;
+ }
+ } else if (!bucket.equals(rhs.bucket)) {
+ return false;
+ }
+ if (bucketInformation == null) {
+ if (rhs.bucketInformation != null) {
+ return false;
+ }
+ } else if (!bucketInformation.equals(rhs.bucketInformation)) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("BucketInfo(%s: %s)", bucket, bucketInformation);
+ }
+ }
+
+ private final List<BucketInfo> buckets = new ArrayList<BucketInfo>();
+
+ public GetBucketListReply() {
+ super(DocumentProtocol.REPLY_GETBUCKETLIST);
+ }
+
+ public List<BucketInfo> getBuckets() {
+ return buckets;
+ }
+}
diff --git a/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/GetBucketStateMessage.java b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/GetBucketStateMessage.java new file mode 100755 index 00000000000..a6b23647f6c --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/GetBucketStateMessage.java @@ -0,0 +1,69 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi.messagebus.protocol; + +import com.yahoo.document.BucketId; + +/** + * This message is a request to return the state of a given bucket. The corresponding reply is {@link + * GetBucketStateReply}. + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class GetBucketStateMessage extends DocumentMessage { + + private BucketId bucket = null; + + /** + * Constructs a new message for deserialization. + */ + GetBucketStateMessage() { + // empty + } + + /** + * Constructs a new reply with initial content. + * + * @param bucket The bucket whose state to reply with. + */ + public GetBucketStateMessage(BucketId bucket) { + this.bucket = bucket; + } + + /** + * Returns the bucket whose state this contains. + * + * @return The bucket id. + */ + public BucketId getBucketId() { + return bucket; + } + + /** + * Sets the bucket whose state this contains. + * + * @param bucket The bucket id to set. + */ + public void setBucketId(BucketId bucket) { + this.bucket = bucket; + } + + @Override + public DocumentReply createReply() { + return new GetBucketStateReply(); + } + + @Override + public long getSequenceId() { + return bucket.getRawId(); + } + + @Override + public int getApproxSize() { + return super.getApproxSize() + 8; + } + + @Override + public int getType() { + return DocumentProtocol.MESSAGE_GETBUCKETSTATE; + } +} diff --git a/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/GetBucketStateReply.java b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/GetBucketStateReply.java new file mode 100755 index 00000000000..f25543412f6 --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/GetBucketStateReply.java @@ -0,0 +1,51 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi.messagebus.protocol; + +import java.util.ArrayList; +import java.util.List; + +/** + * This is a reply to a {@link GetBucketStateMessage}. It contains the state of the bucket id requested by the message. + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class GetBucketStateReply extends DocumentReply { + + private List<DocumentState> state; + + /** + * Constructs a new reply with no content. + */ + public GetBucketStateReply() { + super(DocumentProtocol.REPLY_GETBUCKETSTATE); + state = new ArrayList<DocumentState>(); + } + + /** + * Constructs a new reply with initial content. + * + * @param state The state to set. + */ + public GetBucketStateReply(List<DocumentState> state) { + super(DocumentProtocol.REPLY_GETBUCKETSTATE); + this.state = state; + } + + /** + * Sets the bucket state of this. + * + * @param state The state to set. + */ + public void setBucketState(List<DocumentState> state) { + this.state = state; + } + + /** + * Returns the bucket state contained in this. + * + * @return The state object. + */ + public List<DocumentState> getBucketState() { + return state; + } +} diff --git a/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/GetDocumentMessage.java b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/GetDocumentMessage.java new file mode 100755 index 00000000000..cf66704d21f --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/GetDocumentMessage.java @@ -0,0 +1,94 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi.messagebus.protocol;
+
+import com.yahoo.document.DocumentId;
+
+import java.util.Arrays;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class GetDocumentMessage extends DocumentMessage {
+
+ final static String DEFAULT_FIELD_SET = "[all]";
+ private DocumentId documentId = null;
+ private String fieldSet = DEFAULT_FIELD_SET;
+
+ /**
+ * Constructs a new message for deserialization.
+ */
+ GetDocumentMessage() {
+ // empty
+ }
+
+ /**
+ * Constructs a new document get message.
+ *
+ * @param documentId The identifier of the document to get.
+ */
+ public GetDocumentMessage(DocumentId documentId) {
+ setDocumentId(documentId);
+ }
+
+ /**
+ * Constructs a new document get message.
+ *
+ * @param documentId The identifier of the document to get.
+ * @param fieldSet Which fields to retrieve from the document
+ */
+ public GetDocumentMessage(DocumentId documentId, String fieldSet) {
+ setDocumentId(documentId);
+ this.fieldSet = fieldSet;
+ }
+
+ /**
+ * Returns the identifier of the document to retrieve.
+ *
+ * @return The document id.
+ */
+ public DocumentId getDocumentId() {
+ return documentId;
+ }
+
+ /**
+ * Sets the identifier of the document to retrieve.
+ *
+ * @param documentId The document id to set.
+ */
+ public void setDocumentId(DocumentId documentId) {
+ if (documentId == null) {
+ throw new IllegalArgumentException("Document id can not be null.");
+ }
+ this.documentId = documentId;
+ }
+
+ public String getFieldSet() {
+ return fieldSet;
+ }
+
+ @Override
+ public DocumentReply createReply() {
+ return new GetDocumentReply();
+ }
+
+ @Override
+ public int getApproxSize() {
+ return super.getApproxSize() + 4 + documentId.toString().length();
+ }
+
+ @Override
+ public boolean hasSequenceId() {
+ return true;
+ }
+
+ @Override
+ public long getSequenceId() {
+ return Arrays.hashCode(documentId.getGlobalId());
+ }
+
+ @Override
+ public int getType() {
+ return DocumentProtocol.MESSAGE_GETDOCUMENT;
+ }
+
+}
diff --git a/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/GetDocumentReply.java b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/GetDocumentReply.java new file mode 100755 index 00000000000..f5f687bb18b --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/GetDocumentReply.java @@ -0,0 +1,108 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi.messagebus.protocol;
+
+import com.yahoo.document.Document;
+import com.yahoo.document.serialization.DocumentDeserializer;
+
+import java.nio.ByteBuffer;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class GetDocumentReply extends DocumentAcceptedReply {
+
+ private DocumentDeserializer buffer = null;
+ private Document document = null;
+ private long lastModified = 0;
+ private LazyDecoder decoder = null;
+
+ /**
+ * Constructs a new reply for deserialization.
+ */
+ GetDocumentReply() {
+ super(DocumentProtocol.REPLY_GETDOCUMENT);
+ }
+
+ /**
+ * Constructs a new reply to lazily deserialize from a byte buffer.
+ * @param decoder The decoder to use for deserialization.
+ * @param buf A byte buffer that contains a serialized reply.
+ */
+ GetDocumentReply(LazyDecoder decoder, DocumentDeserializer buf) {
+ super(DocumentProtocol.REPLY_GETDOCUMENT);
+ this.decoder = decoder;
+ buffer = buf;
+ }
+
+ /**
+ * Constructs a new document get reply.
+ *
+ * @param doc The document requested.
+ */
+ public GetDocumentReply(Document doc) {
+ super(DocumentProtocol.REPLY_GETDOCUMENT);
+ document = doc;
+ }
+
+ /**
+ * This method will make sure that any serialized content is deserialized into proper message content on first
+ * entry. Any subsequent entry into this function will do nothing.
+ */
+ private void deserialize() {
+ if (decoder != null && buffer != null) {
+ decoder.decode(this, buffer);
+ decoder = null;
+ buffer = null;
+ }
+ }
+
+ /**
+ * Returns the document retrieved.
+ *
+ * @return The document.
+ */
+ public Document getDocument() {
+ deserialize();
+ return document;
+ }
+
+ /**
+ * Sets the document of this reply.
+ *
+ * @param doc The document to set.
+ */
+ public void setDocument(Document doc) {
+ buffer = null;
+ decoder = null;
+ document = doc;
+ lastModified = document != null && document.getLastModified() != null ? document.getLastModified() : 0;
+ }
+
+ /**
+ * Returns the date the document was last modified.
+ *
+ * @return The date.
+ */
+ public long getLastModified() {
+ deserialize();
+ return lastModified;
+ }
+
+ /**
+ * Set the date the document was last modified.
+ *
+ * @param modified The date.
+ */
+ void setLastModified(long modified) {
+ lastModified = modified;
+ }
+
+ /**
+ * Returns the internal buffer to deserialize from, may be null.
+ *
+ * @return The buffer.
+ */
+ public ByteBuffer getSerializedBuffer() {
+ return buffer != null ? buffer.getBuf().getByteBuffer() : null;
+ }
+}
diff --git a/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/LazyDecoder.java b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/LazyDecoder.java new file mode 100644 index 00000000000..2ac7f716850 --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/LazyDecoder.java @@ -0,0 +1,11 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi.messagebus.protocol; + +import com.yahoo.document.serialization.DocumentDeserializer; +import com.yahoo.messagebus.Routable; + +public interface LazyDecoder { + + public void decode(Routable obj, DocumentDeserializer buf); + +}
\ No newline at end of file diff --git a/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/LoadBalancer.java b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/LoadBalancer.java new file mode 100644 index 00000000000..79725e25e08 --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/LoadBalancer.java @@ -0,0 +1,151 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi.messagebus.protocol; + +import com.yahoo.jrt.slobrok.api.Mirror; +import com.yahoo.messagebus.metrics.CountMetric; +import com.yahoo.messagebus.metrics.MetricSet; +import com.yahoo.messagebus.metrics.ValueMetric; + +import java.util.ArrayList; +import java.util.List; + +/** + * Load balances over a set of nodes based on statistics gathered from those nodes. + * + * @author thomasg + */ +public class LoadBalancer { + + public static class NodeMetrics extends MetricSet { + public CountMetric sent = new CountMetric("sent", this); + public CountMetric busy = new CountMetric("busy", this); + public ValueMetric<Double> weight = new ValueMetric<Double>("weight", 1.0, this); + + public NodeMetrics(String name, MetricSet owner) { + super(name); + owner.addMetric(this); + } + } + + public static class Metrics extends MetricSet { + MetricSet targets = new MetricSet("nodes"); + + public Metrics(String name) { + super(name); + addMetric(targets); + } + } + + public static class Node { + public Node(Mirror.Entry e, NodeMetrics m) { entry = e; metrics = m; } + + public Mirror.Entry entry; + public NodeMetrics metrics; + } + + /** Statistics on each node we are load balancing over. Populated lazily. */ + private List<NodeMetrics> nodeWeights = new ArrayList<NodeMetrics>(); + + private Metrics metrics; + private String cluster; + private double position = 0.0; + + public LoadBalancer(String cluster, String session, Metrics metrics) { + this.metrics = metrics; + this.cluster = cluster; + } + + public List<NodeMetrics> getNodeWeights() { + return nodeWeights; + } + + /** Returns the index from a node name string */ + public int getIndex(String nodeName) { + try { + String s = nodeName.substring(cluster.length() + 1); + s = s.substring(0, s.indexOf("/")); + s = s.substring(s.lastIndexOf(".") + 1); + return Integer.parseInt(s); + } catch (IndexOutOfBoundsException | NumberFormatException e) { + String err = "Expected recipient on the form '" + cluster + "/x/[y.]number/z', got '" + nodeName + "'."; + throw new IllegalArgumentException(err, e); + } + } + + /** + * The load balancing operation: Returns a node choice from the given choices, + * based on previously gathered statistics on the nodes, and a running "position" + * which is increased by 1 on each call to this. + * + * @param choices the node choices, represented as Slobrok entries + * @return the chosen node, or null only if the given choices were zero + */ + public Node getRecipient(Mirror.Entry[] choices) { + if (choices.length == 0) return null; + + double weightSum = 0.0; + Node selectedNode = null; + for (Mirror.Entry entry : choices) { + NodeMetrics nodeMetrics = getNodeMetrics(entry); + + weightSum += nodeMetrics.weight.get(); + + if (weightSum > position) { + selectedNode = new Node(entry, nodeMetrics); + break; + } + } + if (selectedNode == null) { // Position>sum of all weights: Wrap around (but keep the remainder for some reason) + position -= weightSum; + selectedNode = new Node(choices[0], getNodeMetrics(choices[0])); + } + position += 1.0; + selectedNode.metrics.sent.inc(1); + return selectedNode; + } + + /** + * Returns the node metrics at a given index. + * If there is no entry at the given index it is created by this call. + */ + private NodeMetrics getNodeMetrics(Mirror.Entry entry) { + int index = getIndex(entry.getName()); + // expand node array as needed + while (nodeWeights.size() < (index + 1)) + nodeWeights.add(null); + + NodeMetrics nodeMetrics = nodeWeights.get(index); + if (nodeMetrics == null) { // initialize statistics for this node + nodeMetrics = new NodeMetrics("node_" + index, metrics.targets); + nodeWeights.set(index, nodeMetrics); + } + return nodeMetrics; + } + + /** Scale weights such that ratios are preserved */ + private void increaseWeights() { + for (NodeMetrics n : nodeWeights) { + if (n == null) continue; + double want = n.weight.get() * 1.01010101010101010101; + if (want >= 1.0) { + n.weight.set(want); + } else { + n.weight.set(1.0); + } + } + } + + public void received(Node node, boolean busy) { + if (busy) { + double wantWeight = node.metrics.weight.get() - 0.01; + if (wantWeight < 1.0) { + increaseWeights(); + node.metrics.weight.set(1.0); + } else { + node.metrics.weight.set(wantWeight); + } + node.metrics.busy.inc(1); + } + } + +} diff --git a/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/LoadBalancerPolicy.java b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/LoadBalancerPolicy.java new file mode 100644 index 00000000000..e6123dc3dc2 --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/LoadBalancerPolicy.java @@ -0,0 +1,118 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi.messagebus.protocol; + +import com.yahoo.config.subscription.ConfigSourceSet; +import com.yahoo.config.subscription.ConfigSubscriber; +import com.yahoo.jrt.Supervisor; +import com.yahoo.jrt.Transport; +import com.yahoo.jrt.slobrok.api.IMirror; +import com.yahoo.jrt.slobrok.api.SlobrokList; +import com.yahoo.jrt.slobrok.api.Mirror; +import com.yahoo.messagebus.ErrorCode; +import com.yahoo.messagebus.Reply; +import com.yahoo.messagebus.metrics.MetricSet; +import com.yahoo.messagebus.routing.Hop; +import com.yahoo.messagebus.routing.Route; +import com.yahoo.messagebus.routing.RoutingContext; +import com.yahoo.messagebus.routing.RoutingNodeIterator; +import com.yahoo.cloud.config.SlobroksConfig; + +import java.util.Map; +import java.util.concurrent.atomic.AtomicLong; +import java.util.logging.Logger; + +/** + * Routing policy to load balance between nodes in a randomly distributed cluster, such as a docproc cluster. + * + * pattern=<pattern> (mandatory, determines the pattern of nodes to send to)<br> + * slobroks=<comma-separated connectionspecs> (optional, list of slobroks to use to find the pattern)<br> + * config=<comma-separated list of config servers> (optional, list of config servers to use to find slobrok config) + * + * If both slobroks and config is specified, the list from slobroks is used. + * + * @author <a href="mailto:humbe@yahoo-inc.com">Haakon Humberset</a> + */ +public class LoadBalancerPolicy extends ExternalSlobrokPolicy { + String cluster = null; + String session = null; + private String pattern = null; + private AtomicLong count = new AtomicLong(0); + volatile Mirror.Entry [] lastLookup; + + LoadBalancer.Metrics metrics; + LoadBalancer loadBalancer; + + public LoadBalancerPolicy(String param) { + this(param, parse(param)); + } + + public LoadBalancerPolicy(String param, Map<String, String> params) { + super(params); + + cluster = params.get("cluster"); + session = params.get("session"); + + if (cluster == null) { + error = "Required parameter pattern not set"; + return; + } + + if (session == null) { + error = "Required parameter session not set"; + return; + } + + metrics = new LoadBalancer.Metrics(param); + metrics.setXmlTagName("loadbalancer"); + pattern = cluster + "/*/" + session; + loadBalancer = new LoadBalancer(cluster, session, metrics); + } + + @Override + public void doSelect(RoutingContext context) { + LoadBalancer.Node node = getRecipient(context); + + if (node != null) { + context.setContext(node); + Route route = new Route(context.getRoute()); + route.setHop(0, Hop.parse(node.entry.getSpec() + "/" + session)); + context.addChild(route); + } else { + context.setError(ErrorCode.NO_ADDRESS_FOR_SERVICE, + "Could not resolve any nodes to send to in pattern " + pattern); + } + } + + /** + Finds the TCP address of the target. + + @return Returns a hop representing the TCP address of the target, or null if none could be found. + */ + LoadBalancer.Node getRecipient(RoutingContext context) { + long c = count.getAndIncrement(); + if ((c%1024 == 0) || (lastLookup == null) || (lastLookup.length == 0)) { + lastLookup = lookup(context, pattern); + } + return loadBalancer.getRecipient(lastLookup); + } + + public void merge(RoutingContext context) { + RoutingNodeIterator it = context.getChildIterator(); + Reply reply = it.removeReply(); + LoadBalancer.Node target = (LoadBalancer.Node)context.getContext(); + + boolean busy = false; + for (int i = 0; i < reply.getNumErrors(); i++) { + if (reply.getError(i).getCode() == ErrorCode.SESSION_BUSY) { + busy = true; + } + } + loadBalancer.received(target, busy); + + context.setReply(reply); + } + + public MetricSet getMetrics() { + return metrics; + } +} diff --git a/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/LocalServicePolicy.java b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/LocalServicePolicy.java new file mode 100755 index 00000000000..74ca65df547 --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/LocalServicePolicy.java @@ -0,0 +1,138 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi.messagebus.protocol;
+
+import com.yahoo.jrt.slobrok.api.Mirror;
+import com.yahoo.messagebus.metrics.MetricSet;
+import com.yahoo.messagebus.routing.*;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * This policy implements the logic to prefer local services that matches a slobrok pattern.
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class LocalServicePolicy implements DocumentProtocolRoutingPolicy {
+
+ private final String localAddress;
+ private Map<String, CacheEntry> cache = new HashMap<String, CacheEntry>();
+
+ /**
+ * Constructs a policy that will choose local services that match the slobrok pattern in which this policy occured.
+ * If no local service can be found, this policy simply returns the asterisk to allow the network to choose any.
+ *
+ * @param param The address to use for this, if empty this will resolve to hostname.
+ */
+ public LocalServicePolicy(String param) {
+ localAddress = (param != null && param.length() > 0) ? param : null;
+ }
+
+ // Inherit doc from RoutingPolicy.
+ public void select(RoutingContext ctx) {
+ Route route = new Route(ctx.getRoute());
+ route.setHop(0, getRecipient(ctx));
+ ctx.addChild(route);
+ }
+
+ // Inherit doc from RoutingPolicy.
+ public void merge(RoutingContext ctx) {
+ DocumentProtocol.merge(ctx);
+ }
+
+ /**
+ * Returns the appropriate recipient hop for the given routing context. This method provides synchronized access to
+ * the internal cache.
+ *
+ * @param ctx The routing context.
+ * @return The recipient hop to use.
+ */
+ private synchronized Hop getRecipient(RoutingContext ctx) {
+ CacheEntry entry = update(ctx);
+ if (entry.recipients.isEmpty()) {
+ Hop hop = new Hop(ctx.getRoute().getHop(0));
+ hop.setDirective(ctx.getDirectiveIndex(), new VerbatimDirective("*"));
+ return hop;
+ }
+ if (++entry.offset >= entry.recipients.size()) {
+ entry.offset = 0;
+ }
+ return new Hop(entry.recipients.get(entry.offset));
+ }
+
+ /**
+ * Updates and returns the cache entry for the given routing context. This method assumes that synchronization is
+ * handled outside of it.
+ *
+ * @param ctx The routing context.
+ * @return The updated cache entry.
+ */
+ private CacheEntry update(RoutingContext ctx) {
+ String key = getCacheKey(ctx);
+ CacheEntry entry = cache.get(key);
+ if (entry == null) {
+ entry = new CacheEntry();
+ cache.put(key, entry);
+ }
+ int upd = ctx.getMirror().updates();
+ if (entry.generation != upd) {
+ entry.generation = upd;
+ entry.recipients.clear();
+
+ Mirror.Entry[] arr = ctx.getMirror().lookup(ctx.getHopPrefix() + "*" + ctx.getHopSuffix());
+ String self = localAddress != null ? localAddress : toAddress(ctx.getMessageBus().getConnectionSpec());
+ for (Mirror.Entry item : arr) {
+ if (self.equals(toAddress(item.getSpec()))) {
+ entry.recipients.add(Hop.parse(item.getName()));
+ }
+ }
+ }
+ return entry;
+ }
+
+ /**
+ * Returns a cache key for this instance of the policy. Because behaviour is based on the hop in which the policy
+ * occurs, the cache key is the hop string itself.
+ *
+ * @param ctx The routing context.
+ * @return The cache key.
+ */
+ private String getCacheKey(RoutingContext ctx) {
+ return ctx.getRoute().getHop(0).toString();
+ }
+
+ /**
+ * Defines the necessary cache data.
+ */
+ private class CacheEntry {
+ private final List<Hop> recipients = new ArrayList<Hop>();
+ private int generation = 0;
+ private int offset = 0;
+ }
+
+ /**
+ * Searches the given connection spec for a hostname or IP address. If an address is not found, this method returns
+ * null.
+ *
+ * @param connection The connection spec to search.
+ * @return The address, may be null.
+ */
+ private static String toAddress(String connection) {
+ if (connection.startsWith("tcp/")) {
+ int pos = connection.indexOf(':');
+ if (pos > 4) {
+ return connection.substring(4, pos);
+ }
+ }
+ return null;
+ }
+
+ public void destroy() {
+ }
+
+ public MetricSet getMetrics() {
+ return null;
+ }
+}
diff --git a/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/MapVisitorMessage.java b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/MapVisitorMessage.java new file mode 100644 index 00000000000..97604cd0d27 --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/MapVisitorMessage.java @@ -0,0 +1,47 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi.messagebus.protocol; + +import java.util.Map; +import java.util.TreeMap; + +public class MapVisitorMessage extends VisitorMessage { + + private final Map<String, String> data = new TreeMap<String, String>(); + + MapVisitorMessage() { + // must be deserialized into + } + + public MapVisitorMessage(MapVisitorMessage cmd) { + data.putAll(cmd.data); + } + + public Map<String, String> getData() { + return data; + } + + @Override + public DocumentReply createReply() { + return new VisitorReply(DocumentProtocol.REPLY_MAPVISITOR); + } + + @Override + public int getType() { + return DocumentProtocol.MESSAGE_MAPVISITOR; + } + + @Override + public int getApproxSize() { + int length = super.getApproxSize() + 4; + for (Map.Entry<String, String> pairs : data.entrySet()) { + length += 8; + length += (pairs.getKey()).length() + pairs.getValue().length(); + } + return length; + } + + @Override + public String toString() { + return "MapVisitorMessage(" + data.toString() + ")"; + } +} diff --git a/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/MessageTypePolicy.java b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/MessageTypePolicy.java new file mode 100644 index 00000000000..c72994a9fc7 --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/MessageTypePolicy.java @@ -0,0 +1,61 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi.messagebus.protocol; + +import com.yahoo.config.subscription.ConfigSubscriber; +import com.yahoo.messagebus.metrics.MetricSet; +import com.yahoo.messagebus.routing.Route; +import com.yahoo.messagebus.routing.RoutingContext; +import com.yahoo.vespa.config.content.MessagetyperouteselectorpolicyConfig; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; + +/** + * @author <a href="mailto:balder@yahoo-inc.com">Henning Baldersheim</a> + */ +public class MessageTypePolicy implements DocumentProtocolRoutingPolicy, ConfigSubscriber.SingleSubscriber<MessagetyperouteselectorpolicyConfig> { + + private final AtomicReference<Map<Integer, Route>> configRef = new AtomicReference<Map<Integer, Route>>(); + private ConfigSubscriber subscriber; + private volatile Route defaultRoute; + + public MessageTypePolicy(String configId) { + subscriber = new ConfigSubscriber(); + subscriber.subscribe(this, MessagetyperouteselectorpolicyConfig.class, configId); + } + + @Override + public void select(RoutingContext context) { + int messageType = context.getMessage().getType(); + Route route = configRef.get().get(messageType); + if (route == null) { + route = defaultRoute; + } + context.addChild(route); + } + + @Override + public void merge(RoutingContext context) { + DocumentProtocol.merge(context); + } + + @Override + public void destroy() { + if (subscriber!=null) subscriber.close(); + } + + @Override + public MetricSet getMetrics() { + return null; + } + + @Override + public void configure(MessagetyperouteselectorpolicyConfig cfg) { + Map<Integer, Route> h = new HashMap<Integer, Route>(); + for (MessagetyperouteselectorpolicyConfig.Route selector : cfg.route()) { + h.put(selector.messagetype(), Route.parse(selector.name())); + } + configRef.set(h); + defaultRoute = Route.parse(cfg.defaultroute()); + } +} diff --git a/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/PutDocumentMessage.java b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/PutDocumentMessage.java new file mode 100755 index 00000000000..9ff49e08365 --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/PutDocumentMessage.java @@ -0,0 +1,156 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi.messagebus.protocol; + +import com.yahoo.document.Document; +import com.yahoo.document.DocumentPut; +import com.yahoo.document.TestAndSetCondition; +import com.yahoo.document.serialization.DocumentDeserializer; + +import java.nio.ByteBuffer; +import java.util.Arrays; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class PutDocumentMessage extends TestAndSetMessage { + + private DocumentDeserializer buffer = null; + private DocumentPut put = null; + private long time = 0; + private LazyDecoder decoder = null; + + /** + * Constructs a new message for deserialization. + */ + PutDocumentMessage() { + // empty + } + + /** + * Constructs a new message from a byte buffer. + * @param decoder The decoder to use for deserialization. + * @param buffer A byte buffer that contains a serialized message. + */ + public PutDocumentMessage(LazyDecoder decoder, DocumentDeserializer buffer) { + this.decoder = decoder; + this.buffer = buffer; + } + + /** + * Constructs a new document put message. + * + * @param put Document put operation + */ + public PutDocumentMessage(DocumentPut put) { + this.put = put; + } + + /** + * Creates an empty PutDocumentMessage + */ + public static PutDocumentMessage createEmpty() { + return new PutDocumentMessage(null); + } + + /** + * This method will make sure that any serialized content is deserialized into proper message content on first + * entry. Any subsequent entry into this function will do nothing. + */ + private void deserialize() { + if (decoder != null && buffer != null) { + decoder.decode(this, buffer); + decoder = null; + buffer = null; + } + } + + /** + * Returns the document put operation + */ + public DocumentPut getDocumentPut() { + deserialize(); + return put; + } + + /** + * Sets the document to put. + * + * @param put Put document operation + */ + public void setDocumentPut(DocumentPut put) { + buffer = null; + decoder = null; + this.put = put; + } + + /** + * Returns the timestamp of the document to put. + * + * @return The document timestamp. + */ + public long getTimestamp() { + deserialize(); + return time; + } + + /** + * Sets the timestamp of the document to put. + * + * @param time The timestamp to set. + */ + public void setTimestamp(long time) { + buffer = null; + decoder = null; + this.time = time; + } + + /** + * Returns the raw serialized buffer. This buffer is stored as the message is received from accross the network, and + * deserialized from as soon as a member is requested. This method will return null if the buffer has been decoded. + * + * @return The buffer containing the serialized data for this message, or null. + */ + ByteBuffer getSerializedBuffer() { + return buffer != null ? buffer.getBuf().getByteBuffer() : null; // TODO: very dirty. Must make interface. + } + + @Override + public DocumentReply createReply() { + return new WriteDocumentReply(DocumentProtocol.REPLY_PUTDOCUMENT); + } + + @Override + public int getApproxSize() { + if (buffer != null) { + return buffer.getBuf().remaining(); + } + return put.getDocument().getApproxSize(); + } + + @Override + public boolean hasSequenceId() { + return true; + } + + @Override + public long getSequenceId() { + deserialize(); + return Arrays.hashCode(put.getId().getGlobalId()); + } + + @Override + public int getType() { + return DocumentProtocol.MESSAGE_PUTDOCUMENT; + } + + @Override + public TestAndSetCondition getCondition() { + deserialize(); + return put.getCondition(); + } + + @Override + public void setCondition(TestAndSetCondition condition) { + put.setCondition(condition); + } +} diff --git a/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/QueryResultMessage.java b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/QueryResultMessage.java new file mode 100644 index 00000000000..d2e7f4013e6 --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/QueryResultMessage.java @@ -0,0 +1,39 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi.messagebus.protocol; + +import com.yahoo.vdslib.SearchResult; +import com.yahoo.vdslib.DocumentSummary; + +/** + */ +public class QueryResultMessage extends VisitorMessage { + + private SearchResult searchResult = null; + private DocumentSummary summary = null; + + public SearchResult getResult() { + return searchResult; + } + + public DocumentSummary getSummary() { + return summary; + } + + public void setSearchResult(SearchResult result) { + searchResult = result; + } + + public void setSummary(DocumentSummary summary) { + this.summary = summary; + } + + @Override + public DocumentReply createReply() { + return new VisitorReply(DocumentProtocol.REPLY_QUERYRESULT); + } + + @Override + public int getType() { + return DocumentProtocol.MESSAGE_QUERYRESULT; + } +} diff --git a/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/RemoveDocumentMessage.java b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/RemoveDocumentMessage.java new file mode 100755 index 00000000000..f6fcb5965ad --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/RemoveDocumentMessage.java @@ -0,0 +1,101 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi.messagebus.protocol;
+
+import com.yahoo.document.DocumentId;
+import com.yahoo.document.DocumentRemove;
+import com.yahoo.document.TestAndSetCondition;
+
+import java.util.Arrays;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class RemoveDocumentMessage extends TestAndSetMessage {
+ private DocumentRemove remove = null;
+
+ /**
+ * Constructs a new message for deserialization.
+ */
+ RemoveDocumentMessage() {
+ // empty
+ }
+
+ /**
+ * Constructs a new document remove message.
+ *
+ * @param documentId The identifier of the document to remove.
+ */
+ public RemoveDocumentMessage(DocumentId documentId) {
+ remove = new DocumentRemove(documentId);
+ }
+
+ /**
+ * Constructs a new document remove message.
+ *
+ * @param remove The DocumentRemove operation to perform
+ */
+ public RemoveDocumentMessage(DocumentRemove remove) {
+ this.remove = remove;
+ }
+
+ /**
+ * Returns the identifier of the document to remove.
+ *
+ * @return The document id.
+ */
+ public DocumentId getDocumentId() {
+ return remove.getId();
+ }
+
+ /**
+ * Sets the identifier of the document to remove.
+ *
+ * @param documentId The document id to set.
+ */
+ public void setDocumentId(DocumentId documentId) {
+ if (documentId == null) {
+ throw new IllegalArgumentException("Document id can not be null.");
+ }
+
+ remove = new DocumentRemove(documentId);
+ }
+
+ @Override
+ public DocumentReply createReply() {
+ return new RemoveDocumentReply();
+ }
+
+ @Override
+ public int getApproxSize() {
+ return super.getApproxSize() + 4 + remove.getId().toString().length();
+ }
+
+ @Override
+ public boolean hasSequenceId() {
+ return true;
+ }
+
+ @Override
+ public long getSequenceId() {
+ return Arrays.hashCode(remove.getId().getGlobalId());
+ }
+
+ @Override
+ public int getType() {
+ return DocumentProtocol.MESSAGE_REMOVEDOCUMENT;
+ }
+
+ @Override
+ public void setCondition(TestAndSetCondition condition) {
+ remove.setCondition(condition);
+ }
+
+ @Override
+ public TestAndSetCondition getCondition() {
+ return remove.getCondition();
+ }
+
+ public DocumentRemove getDocumentRemove() {
+ return remove;
+ }
+}
diff --git a/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/RemoveDocumentReply.java b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/RemoveDocumentReply.java new file mode 100755 index 00000000000..c259aaa5731 --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/RemoveDocumentReply.java @@ -0,0 +1,35 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi.messagebus.protocol;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class RemoveDocumentReply extends WriteDocumentReply {
+
+ private boolean found = true;
+
+ /**
+ * Constructs a new reply with no content.
+ */
+ public RemoveDocumentReply() {
+ super(DocumentProtocol.REPLY_REMOVEDOCUMENT);
+ }
+
+ /**
+ * Returns whether or not the document was found and removed.
+ *
+ * @return True if document was found.
+ */
+ public boolean wasFound() {
+ return found;
+ }
+
+ /**
+ * Set whether or not the document was found and removed.
+ *
+ * @param found True if the document was found.
+ */
+ public void setWasFound(boolean found) {
+ this.found = found;
+ }
+}
diff --git a/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/RemoveLocationMessage.java b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/RemoveLocationMessage.java new file mode 100755 index 00000000000..4a7287b59d8 --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/RemoveLocationMessage.java @@ -0,0 +1,52 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi.messagebus.protocol; + +import com.yahoo.document.*; +import com.yahoo.document.select.BucketSelector; +import java.util.Set; + +/** + * Message (VDS only) to remove an entire location for users using userdoc or groupdoc schemes. + * We use a document selection so the user can specify a subset of those documents to be deleted + * if they wish. + */ +public class RemoveLocationMessage extends DocumentMessage { + String documentSelection; + BucketId bucketId; + + public RemoveLocationMessage(String documentSelection) { + try { + this.documentSelection = documentSelection; + BucketSelector bucketSel = new BucketSelector(new BucketIdFactory()); + Set<BucketId> rawBuckets = bucketSel.getBucketList(documentSelection); + if (rawBuckets == null || rawBuckets.size() != 1) { + throw new IllegalArgumentException("Document selection for remove location must map to a single location (user or group)"); + } else { + // There can only be one. + for (BucketId id : rawBuckets) { + bucketId = id; + } + } + } catch (com.yahoo.document.select.parser.ParseException p) { + throw new IllegalArgumentException(p); + } + } + + public String getDocumentSelection() { + return documentSelection; + } + + @Override + public DocumentReply createReply() { + return new DocumentReply(DocumentProtocol.REPLY_REMOVELOCATION); + } + + @Override + public int getType() { + return DocumentProtocol.MESSAGE_REMOVELOCATION; + } + + public BucketId getBucketId() { + return bucketId; + } +} diff --git a/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/ReplyMerger.java b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/ReplyMerger.java new file mode 100644 index 00000000000..2dad8312fc9 --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/ReplyMerger.java @@ -0,0 +1,109 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi.messagebus.protocol;
+
+import com.yahoo.collections.Tuple2;
+import com.yahoo.messagebus.EmptyReply;
+import com.yahoo.messagebus.Reply;
+
+/**
+ * Encapsulated logic for merging replies from 1-n related DocumentProtocol messages.
+ * For internal use only. Not multithread safe.
+ */
+final class ReplyMerger {
+
+ private Reply successReply = null;
+ private int successIndex = -1;
+ private Reply error = null;
+ private Reply ignore = null;
+
+ public void merge(int i, Reply r) {
+ if (r.hasErrors()) {
+ mergeAllReplyErrors(r);
+ } else {
+ updateStateWithSuccessfulReply(i, r);
+ }
+ }
+
+ private boolean resourceWasFound(Reply r) {
+ if (r instanceof RemoveDocumentReply) {
+ return ((RemoveDocumentReply) r).wasFound();
+ }
+ if (r instanceof UpdateDocumentReply) {
+ return ((UpdateDocumentReply) r).wasFound();
+ }
+ if (r instanceof GetDocumentReply) {
+ return ((GetDocumentReply) r).getLastModified() > 0;
+ }
+ return false;
+ }
+
+ private boolean replyIsBetterThanCurrent(Reply r) {
+ return resourceWasFound(r) && !resourceWasFound(successReply);
+ }
+
+ private void updateStateWithSuccessfulReply(int i, Reply r) {
+ if (successReply == null || replyIsBetterThanCurrent(r)) {
+ setCurrentBestReply(i, r);
+ }
+ }
+
+ private void setCurrentBestReply(int i, Reply r) {
+ successReply = r;
+ successIndex = i;
+ }
+
+ private void mergeAllReplyErrors(Reply r) {
+ if (handleReplyWithOnlyIgnoredErrors(r)) {
+ return;
+ }
+ if (error == null) {
+ error = new EmptyReply();
+ }
+ for (int j = 0; j < r.getNumErrors(); ++j) {
+ error.addError(r.getError(j));
+ }
+ }
+
+ private boolean handleReplyWithOnlyIgnoredErrors(Reply r) {
+ if (DocumentProtocol.hasOnlyErrorsOfType(r, DocumentProtocol.ERROR_MESSAGE_IGNORED)) {
+ if (ignore == null) {
+ ignore = new EmptyReply();
+ }
+ ignore.addError(r.getError(0));
+ return true;
+ }
+ return false;
+ }
+
+ private boolean shouldReturnErrorReply() {
+ return (error != null || (ignore != null && successReply == null));
+ }
+
+ private Tuple2<Integer, Reply> createMergedErrorReplyResult() {
+ if (error != null) {
+ return new Tuple2<>(null, error);
+ }
+ if (ignore != null && successReply == null) {
+ return new Tuple2<>(null, ignore);
+ }
+ throw new IllegalStateException("createMergedErrorReplyResult called without error");
+ }
+
+ private boolean successfullyMergedAtLeastOneReply() {
+ return successReply != null;
+ }
+
+ private Tuple2<Integer, Reply> createEmptyReplyResult() {
+ return new Tuple2<>(null, (Reply)new EmptyReply());
+ }
+
+ public Tuple2<Integer, Reply> mergedReply() {
+ if (shouldReturnErrorReply()) {
+ return createMergedErrorReplyResult();
+ } else if (!successfullyMergedAtLeastOneReply()) {
+ return createEmptyReplyResult();
+ }
+ return new Tuple2<>(successIndex, successReply);
+ }
+
+}
diff --git a/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/RoundRobinPolicy.java b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/RoundRobinPolicy.java new file mode 100755 index 00000000000..f0e49146851 --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/RoundRobinPolicy.java @@ -0,0 +1,125 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi.messagebus.protocol;
+
+import com.yahoo.jrt.slobrok.api.Mirror;
+import com.yahoo.messagebus.EmptyReply;
+import com.yahoo.messagebus.Error;
+import com.yahoo.messagebus.ErrorCode;
+import com.yahoo.messagebus.Reply;
+import com.yahoo.messagebus.metrics.MetricSet;
+import com.yahoo.messagebus.routing.Hop;
+import com.yahoo.messagebus.routing.Route;
+import com.yahoo.messagebus.routing.RoutingContext;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * This policy implements round-robin selection of the configured recipients that are currently registered in slobrok.
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class RoundRobinPolicy implements DocumentProtocolRoutingPolicy {
+
+ private final Map<String, CacheEntry> cache = new HashMap<String, CacheEntry>();
+
+ // Inherit doc from RoutingPolicy.
+ public void select(RoutingContext ctx) {
+ Hop hop = getRecipient(ctx);
+ if (hop != null) {
+ Route route = new Route(ctx.getRoute());
+ route.setHop(0, hop);
+ ctx.addChild(route);
+ } else {
+ Reply reply = new EmptyReply();
+ reply.addError(new Error(ErrorCode.NO_ADDRESS_FOR_SERVICE,
+ "None of the configured recipients are currently available."));
+ ctx.setReply(reply);
+ }
+ }
+
+ // Inherit doc from RoutingPolicy.
+ public void merge(RoutingContext ctx) {
+ DocumentProtocol.merge(ctx);
+ }
+
+ /**
+ * Returns the appropriate recipient hop for the given routing context. This method provides synchronized access to
+ * the internal cache.
+ *
+ * @param ctx The routing context.
+ * @return The recipient hop to use.
+ */
+ private synchronized Hop getRecipient(RoutingContext ctx) {
+ CacheEntry entry = update(ctx);
+ if (entry.recipients.isEmpty()) {
+ return null;
+ }
+ if (++entry.offset >= entry.recipients.size()) {
+ entry.offset = 0;
+ }
+ return new Hop(entry.recipients.get(entry.offset));
+ }
+
+ /**
+ * Updates and returns the cache entry for the given routing context. This method assumes that synchronization is
+ * handled outside of it.
+ *
+ * @param ctx The routing context.
+ * @return The updated cache entry.
+ */
+ private CacheEntry update(RoutingContext ctx) {
+ String key = getCacheKey(ctx);
+ CacheEntry entry = cache.get(key);
+ if (entry == null) {
+ entry = new CacheEntry();
+ cache.put(key, entry);
+ }
+
+ int upd = ctx.getMirror().updates();
+ if (entry.generation != upd) {
+ entry.generation = upd;
+ entry.recipients.clear();
+ for (int i = 0; i < ctx.getNumRecipients(); ++i) {
+ Mirror.Entry[] arr = ctx.getMirror().lookup(ctx.getRecipient(i).getHop(0).toString());
+ for (Mirror.Entry item : arr) {
+ entry.recipients.add(Hop.parse(item.getName()));
+ }
+ }
+ }
+ return entry;
+ }
+
+ /**
+ * Returns a cache key for this instance of the policy. Because behaviour is based on the recipient list of this
+ * policy, the cache key is the concatenated string of recipient routes.
+ *
+ * @param ctx The routing context.
+ * @return The cache key.
+ */
+ private String getCacheKey(RoutingContext ctx) {
+ StringBuilder ret = new StringBuilder();
+ for (int i = 0; i < ctx.getNumRecipients(); ++i) {
+ ret.append(ctx.getRecipient(i).getHop(0).toString()).append(" ");
+ }
+ return ret.toString();
+ }
+
+ /**
+ * Defines the necessary cache data.
+ */
+ private class CacheEntry {
+ private final List<Hop> recipients = new ArrayList<Hop>();
+ private int generation = 0;
+ private int offset = 0;
+ }
+
+ public void destroy() {
+ }
+
+ public MetricSet getMetrics() {
+ return null;
+ }
+}
diff --git a/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/RoutableFactories50.java b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/RoutableFactories50.java new file mode 100755 index 00000000000..9ba2aee9227 --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/RoutableFactories50.java @@ -0,0 +1,984 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi.messagebus.protocol; + +import com.yahoo.document.BucketId; +import com.yahoo.document.Document; +import com.yahoo.document.DocumentId; +import com.yahoo.document.DocumentPut; +import com.yahoo.document.DocumentTypeManager; +import com.yahoo.document.DocumentUpdate; +import com.yahoo.document.serialization.DocumentDeserializer; +import com.yahoo.document.serialization.DocumentSerializer; +import com.yahoo.document.serialization.DocumentSerializerFactory; +import com.yahoo.documentapi.messagebus.loadtypes.LoadTypeSet; +import com.yahoo.log.LogLevel; +import com.yahoo.messagebus.Routable; +import com.yahoo.vdslib.DocumentList; +import com.yahoo.vdslib.DocumentSummary; +import com.yahoo.vdslib.SearchResult; +import com.yahoo.vdslib.VisitorStatistics; +import com.yahoo.vespa.objects.Deserializer; +import com.yahoo.vespa.objects.Serializer; + +import java.util.Map; +import java.util.logging.Logger; + + +/** + * This class encapsulates all the {@link RoutableFactory} classes needed to implement serialization for the document + * protocol. When adding new factories to this class, please KEEP THE THEM ORDERED alphabetically like they are now. + */ +public abstract class RoutableFactories50 { + + /** + * Implements the shared factory logic required for {@link DocumentMessage} objects, and it offers a more convenient + * interface for implementing {@link RoutableFactory}. + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ + public static abstract class DocumentMessageFactory extends AbstractRoutableFactory { + + /** + * This method encodes the given message using the given serializer. You are guaranteed to only receive messages + * of the type that this factory was registered for. + * <p> + * This method is NOT exception safe. Return false to + * signal failure. + * + * @param msg The message to encode. + * @param serializer The serializer to use for encoding. + * @return True if the message was encoded. + */ + protected abstract boolean doEncode(DocumentMessage msg, DocumentSerializer serializer); + + /** + * This method decodes a message from the given deserializer. You are guaranteed to only receive byte buffers + * generated by a previous call to {@link #doEncode(DocumentMessage, DocumentSerializer)}. + * <p> + * This method is NOT exception safe. Return null to signal failure. + * + * @param deserializer The deserializer to use for decoding. + * @return The decoded message. + */ + protected abstract DocumentMessage doDecode(DocumentDeserializer deserializer); + + public boolean encode(Routable obj, DocumentSerializer out) { + if (!(obj instanceof DocumentMessage)) { + throw new AssertionError( + "Document message factory (" + getClass().getName() + ") registered for incompatible " + + "routable type " + obj.getType() + "(" + obj.getClass().getName() + ")."); + } + DocumentMessage msg = (DocumentMessage)obj; + out.putByte(null, (byte)(msg.getPriority().getValue())); + out.putInt(null, msg.getLoadType().getId()); + return doEncode(msg, out); + } + + public Routable decode(DocumentDeserializer in, LoadTypeSet loadTypes) { + byte pri = in.getByte(null); + int loadType = in.getInt(null); + DocumentMessage msg = doDecode(in); + if (msg != null) { + msg.setPriority(DocumentProtocol.getPriority(pri)); + msg.setLoadType(loadTypes.getIdMap().get(loadType)); + } + return msg; + } + } + + /** + * Implements the shared factory logic required for {@link DocumentReply} objects, and it offers a more convenient + * interface for implementing {@link RoutableFactory}. + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ + public static abstract class DocumentReplyFactory extends AbstractRoutableFactory { + + /** + * This method encodes the given reply into the given byte buffer. You are guaranteed to only receive replies of + * the type that this factory was registered for. + * <p> + * This method is NOT exception safe. Return false to signal + * failure. + * + * @param reply The reply to encode. + * @param buf The byte buffer to write to. + * @return True if the message was encoded. + */ + protected abstract boolean doEncode(DocumentReply reply, DocumentSerializer buf); + + /** + * This method decodes a reply from the given byte buffer. You are guaranteed to only receive byte buffers + * generated by a previous call to {@link #doEncode(DocumentReply, com.yahoo.document.serialization.DocumentSerializer)}. + * + * <p> + * This method is NOT exception safe. Return null to signal failure. + * + * @param buf The byte buffer to read from. + * @return The decoded reply. + */ + protected abstract DocumentReply doDecode(DocumentDeserializer buf); + + public boolean encode(Routable obj, DocumentSerializer out) { + if (!(obj instanceof DocumentReply)) { + throw new AssertionError( + "Document reply factory (" + getClass().getName() + ") registered for incompatible " + + "routable type " + obj.getType() + "(" + obj.getClass().getName() + ")."); + } + DocumentReply reply = (DocumentReply)obj; + out.putByte(null, (byte)(reply.getPriority().getValue())); + return doEncode(reply, out); + } + + public Routable decode(DocumentDeserializer in, LoadTypeSet loadTypes) { + byte pri = in.getByte(null); + DocumentReply reply = doDecode(in); + if (reply != null) { + reply.setPriority(DocumentProtocol.getPriority(pri)); + } + return reply; + } + } + + public static class CreateVisitorMessageFactory extends DocumentMessageFactory { + + @Override + @SuppressWarnings("deprecation") + protected DocumentMessage doDecode(DocumentDeserializer buf) { + CreateVisitorMessage msg = new CreateVisitorMessage(); + msg.setLibraryName(decodeString(buf)); + msg.setInstanceId(decodeString(buf)); + msg.setControlDestination(decodeString(buf)); + msg.setDataDestination(decodeString(buf)); + msg.setDocumentSelection(decodeString(buf)); + msg.setMaxPendingReplyCount(buf.getInt(null)); + + int size = buf.getInt(null); + for (int i = 0; i < size; i++) { + long reversed = buf.getLong(null); + long rawid = ((reversed >>> 56) & 0x00000000000000FFl) | ((reversed >>> 40) & 0x000000000000FF00l) | + ((reversed >>> 24) & 0x0000000000FF0000l) | ((reversed >>> 8) & 0x00000000FF000000l) | + ((reversed << 8) & 0x000000FF00000000l) | ((reversed << 24) & 0x0000FF0000000000l) | + ((reversed << 40) & 0x00FF000000000000l) | ((reversed << 56) & 0xFF00000000000000l); + msg.getBuckets().add(new BucketId(rawid)); + } + + msg.setFromTimestamp(buf.getLong(null)); + msg.setToTimestamp(buf.getLong(null)); + msg.setVisitRemoves(buf.getByte(null) == (byte)1); + buf.getByte(null); // removed feature "visitHeadersOnly" + msg.setVisitInconsistentBuckets(buf.getByte(null) == (byte)1); + + size = buf.getInt(null); + for (int i = 0; i < size; i++) { + String key = decodeString(buf); + int sz = buf.getInt(null); + msg.getParameters().put(key, buf.getBytes(null, sz)); + } + + msg.setVisitorOrdering(buf.getInt(null)); + msg.setMaxBucketsPerVisitor(buf.getInt(null)); + msg.setVisitorDispatcherVersion(50); + return msg; + } + + @Override + @SuppressWarnings("deprecation") + protected boolean doEncode(DocumentMessage obj, DocumentSerializer buf) { + CreateVisitorMessage msg = (CreateVisitorMessage)obj; + encodeString(msg.getLibraryName(), buf); + encodeString(msg.getInstanceId(), buf); + encodeString(msg.getControlDestination(), buf); + encodeString(msg.getDataDestination(), buf); + encodeString(msg.getDocumentSelection(), buf); + buf.putInt(null, msg.getMaxPendingReplyCount()); + + buf.putInt(null, msg.getBuckets().size()); + for (BucketId id : msg.getBuckets()) { + long rawid = id.getRawId(); + long reversed = ((rawid >>> 56) & 0x00000000000000FFl) | ((rawid >>> 40) & 0x000000000000FF00l) | + ((rawid >>> 24) & 0x0000000000FF0000l) | ((rawid >>> 8) & 0x00000000FF000000l) | + ((rawid << 8) & 0x000000FF00000000l) | ((rawid << 24) & 0x0000FF0000000000l) | + ((rawid << 40) & 0x00FF000000000000l) | ((rawid << 56) & 0xFF00000000000000l); + buf.putLong(null, reversed); + } + + buf.putLong(null, msg.getFromTimestamp()); + buf.putLong(null, msg.getToTimestamp()); + buf.putByte(null, msg.getVisitRemoves() ? (byte)1 : (byte)0); + buf.putByte(null, (byte)0); // removed feature "visitHeadersOnly" + buf.putByte(null, msg.getVisitInconsistentBuckets() ? (byte)1 : (byte)0); + + buf.putInt(null, msg.getParameters().size()); + for (Map.Entry<String, byte[]> pairs : msg.getParameters().entrySet()) { + encodeString(pairs.getKey(), buf); + byte[] b = pairs.getValue(); + buf.putInt(null, b.length); + buf.put(null, b); + } + + buf.putInt(null, msg.getVisitorOrdering()); + buf.putInt(null, msg.getMaxBucketsPerVisitor()); + return true; + } + } + + public static class CreateVisitorReplyFactory extends DocumentReplyFactory { + + @Override + protected DocumentReply doDecode(DocumentDeserializer buf) { + CreateVisitorReply reply = new CreateVisitorReply(DocumentProtocol.REPLY_CREATEVISITOR); + reply.setLastBucket(new BucketId(buf.getLong(null))); + + VisitorStatistics vs = new VisitorStatistics(); + vs.setBucketsVisited(buf.getInt(null)); + vs.setDocumentsVisited(buf.getLong(null)); + vs.setBytesVisited(buf.getLong(null)); + vs.setDocumentsReturned(buf.getLong(null)); + vs.setBytesReturned(buf.getLong(null)); + vs.setSecondPassDocumentsReturned(buf.getLong(null)); + vs.setSecondPassBytesReturned(buf.getLong(null)); + reply.setVisitorStatistics(vs); + return reply; + } + + @Override + protected boolean doEncode(DocumentReply obj, DocumentSerializer buf) { + CreateVisitorReply reply = (CreateVisitorReply)obj; + buf.putLong(null, reply.getLastBucket().getRawId()); + buf.putInt(null, reply.getVisitorStatistics().getBucketsVisited()); + buf.putLong(null, reply.getVisitorStatistics().getDocumentsVisited()); + buf.putLong(null, reply.getVisitorStatistics().getBytesVisited()); + buf.putLong(null, reply.getVisitorStatistics().getDocumentsReturned()); + buf.putLong(null, reply.getVisitorStatistics().getBytesReturned()); + buf.putLong(null, reply.getVisitorStatistics().getSecondPassDocumentsReturned()); + buf.putLong(null, reply.getVisitorStatistics().getSecondPassBytesReturned()); + return true; + } + } + + public static class DestroyVisitorMessageFactory extends DocumentMessageFactory { + + @Override + protected DocumentMessage doDecode(DocumentDeserializer buf) { + DestroyVisitorMessage msg = new DestroyVisitorMessage(); + msg.setInstanceId(decodeString(buf)); + return msg; + } + + @Override + protected boolean doEncode(DocumentMessage obj, DocumentSerializer buf) { + DestroyVisitorMessage msg = (DestroyVisitorMessage)obj; + encodeString(msg.getInstanceId(), buf); + return true; + } + } + + public static class DestroyVisitorReplyFactory extends DocumentReplyFactory { + + @Override + protected DocumentReply doDecode(DocumentDeserializer buf) { + return new VisitorReply(DocumentProtocol.REPLY_DESTROYVISITOR); + } + + @Override + protected boolean doEncode(DocumentReply obj, DocumentSerializer buf) { + return true; + } + } + + public static class DocumentListMessageFactory extends DocumentMessageFactory { + + @Override + protected DocumentMessage doDecode(DocumentDeserializer buf) { + DocumentListMessage msg = new DocumentListMessage(); + msg.setBucketId(new BucketId(buf.getLong(null))); + int len = buf.getInt(null); + for (int i = 0; i < len; i++) { + msg.getDocuments().add(new DocumentListEntry(buf)); + } + return msg; + } + + @Override + protected boolean doEncode(DocumentMessage obj, DocumentSerializer buf) { + DocumentListMessage msg = (DocumentListMessage)obj; + buf.putLong(null, msg.getBucketId().getRawId()); + buf.putInt(null, msg.getDocuments().size()); + + for (int i = 0; i < msg.getDocuments().size(); i++) { + msg.getDocuments().get(i).serialize(buf); + } + return true; + } + } + + public static class DocumentListReplyFactory extends DocumentReplyFactory { + + @Override + protected DocumentReply doDecode(DocumentDeserializer buf) { + return new VisitorReply(DocumentProtocol.REPLY_DOCUMENTLIST); + } + + @Override + protected boolean doEncode(DocumentReply obj, DocumentSerializer buf) { + return true; + } + } + + public static class DocumentSummaryMessageFactory extends DocumentMessageFactory { + + @Override + protected DocumentMessage doDecode(DocumentDeserializer buf) { + DocumentSummaryMessage msg = new DocumentSummaryMessage(); + msg.setDocumentSummary(new DocumentSummary(buf)); + return msg; + } + + @Override + protected boolean doEncode(DocumentMessage obj, DocumentSerializer buf) { + return false; // not supported + } + } + + public static class DocumentSummaryReplyFactory extends DocumentReplyFactory { + + @Override + protected DocumentReply doDecode(DocumentDeserializer buf) { + return new VisitorReply(DocumentProtocol.REPLY_DOCUMENTSUMMARY); + } + + @Override + protected boolean doEncode(DocumentReply obj, DocumentSerializer buf) { + return true; + } + } + + public static class EmptyBucketsMessageFactory extends DocumentMessageFactory { + + @Override + protected DocumentMessage doDecode(DocumentDeserializer buf) { + EmptyBucketsMessage msg = new EmptyBucketsMessage(); + int size = buf.getInt(null); + for (int i = 0; i < size; ++i) { + msg.getBucketIds().add(new BucketId(buf.getLong(null))); + } + return msg; + } + + @Override + protected boolean doEncode(DocumentMessage obj, DocumentSerializer buf) { + EmptyBucketsMessage msg = (EmptyBucketsMessage)obj; + buf.putInt(null, msg.getBucketIds().size()); + for (BucketId bid : msg.getBucketIds()) { + buf.putLong(null, bid.getRawId()); + } + return true; + } + } + + public static class EmptyBucketsReplyFactory extends DocumentReplyFactory { + + @Override + protected DocumentReply doDecode(DocumentDeserializer buf) { + return new VisitorReply(DocumentProtocol.REPLY_EMPTYBUCKETS); + } + + @Override + protected boolean doEncode(DocumentReply obj, DocumentSerializer buf) { + return true; + } + } + + public static class GetBucketListMessageFactory extends DocumentMessageFactory { + + @Override + protected DocumentMessage doDecode(DocumentDeserializer buf) { + GetBucketListMessage msg = new GetBucketListMessage(); + msg.setBucketId(new BucketId(buf.getLong(null))); + return msg; + } + + @Override + protected boolean doEncode(DocumentMessage obj, DocumentSerializer buf) { + GetBucketListMessage msg = (GetBucketListMessage)obj; + buf.putLong(null, msg.getBucketId().getRawId()); + return true; + } + } + + public static class GetBucketListReplyFactory extends DocumentReplyFactory { + + @Override + protected DocumentReply doDecode(DocumentDeserializer buf) { + GetBucketListReply reply = new GetBucketListReply(); + int len = buf.getInt(null); + for (int i = 0; i < len; i++) { + GetBucketListReply.BucketInfo info = new GetBucketListReply.BucketInfo(); + info.bucket = new BucketId(buf.getLong(null)); + info.bucketInformation = decodeString(buf); + reply.getBuckets().add(info); + } + return reply; + } + + @Override + protected boolean doEncode(DocumentReply obj, DocumentSerializer buf) { + GetBucketListReply reply = (GetBucketListReply)obj; + buf.putInt(null, reply.getBuckets().size()); + for (GetBucketListReply.BucketInfo info : reply.getBuckets()) { + buf.putLong(null, info.bucket.getRawId()); + encodeString(info.bucketInformation, buf); + } + return true; + } + } + + public static class GetBucketStateMessageFactory extends DocumentMessageFactory { + + @Override + protected DocumentMessage doDecode(DocumentDeserializer buf) { + GetBucketStateMessage msg = new GetBucketStateMessage(); + msg.setBucketId(new BucketId(buf.getLong(null))); + return msg; + } + + @Override + protected boolean doEncode(DocumentMessage obj, DocumentSerializer buf) { + GetBucketStateMessage msg = (GetBucketStateMessage)obj; + buf.putLong(null, msg.getBucketId().getRawId()); + return true; + } + } + + public static class GetBucketStateReplyFactory extends DocumentReplyFactory { + + @Override + protected DocumentReply doDecode(DocumentDeserializer buf) { + GetBucketStateReply reply = new GetBucketStateReply(); + int size = buf.getInt(null); + for (int i = 0; i < size; i++) { + reply.getBucketState().add(new DocumentState(buf)); + } + return reply; + } + + @Override + protected boolean doEncode(DocumentReply obj, DocumentSerializer buf) { + GetBucketStateReply reply = (GetBucketStateReply)obj; + buf.putInt(null, reply.getBucketState().size()); + for (DocumentState stat : reply.getBucketState()) { + stat.serialize(buf); + } + return true; + } + } + + public static class GetDocumentMessageFactory extends DocumentMessageFactory { + + @Override + @SuppressWarnings("deprecation") + protected DocumentMessage doDecode(DocumentDeserializer buf) { + GetDocumentMessage msg = new GetDocumentMessage(); + msg.setDocumentId(new DocumentId(buf)); + buf.getInt(null); // removed feature "flags"; ignore + return msg; + } + + @Override + @SuppressWarnings("deprecation") + protected boolean doEncode(DocumentMessage obj, DocumentSerializer buf) { + GetDocumentMessage msg = (GetDocumentMessage)obj; + msg.getDocumentId().serialize(buf); + buf.putInt(null, 0); // removed feature "flags" + return true; + } + } + + public static class GetDocumentReplyFactory extends DocumentReplyFactory { + + private final LazyDecoder decoder = new LazyDecoder() { + + public void decode(Routable obj, DocumentDeserializer buf) { + GetDocumentReply reply = (GetDocumentReply)obj; + + Document doc = null; + byte flag = buf.getByte(null); + if (flag != 0) { + doc = Document.createDocument(buf); + reply.setDocument(doc); + } + long lastModified = buf.getLong(null); + reply.setLastModified(lastModified); + if (doc != null) { + doc.setLastModified(lastModified); + } + } + }; + + @Override + protected DocumentReply doDecode(DocumentDeserializer buf) { + GetDocumentReply reply = new GetDocumentReply(decoder, buf); + + return reply; + } + + @Override + protected boolean doEncode(DocumentReply obj, DocumentSerializer buf) { + GetDocumentReply reply = (GetDocumentReply)obj; + if (reply.getSerializedBuffer() != null) { + buf.put(null, reply.getSerializedBuffer()); + } else { + Document document = reply.getDocument(); + buf.putByte(null, (byte)(document == null ? 0 : 1)); + if (document != null) { + document.serialize(buf); + } + buf.putLong(null, reply.getLastModified()); + } + return true; + } + } + + public static class MapVisitorMessageFactory extends DocumentMessageFactory { + + @Override + protected DocumentMessage doDecode(DocumentDeserializer buf) { + MapVisitorMessage msg = new MapVisitorMessage(); + int size = buf.getInt(null); + for (int i = 0; i < size; i++) { + String key = decodeString(buf); + String value = decodeString(buf); + msg.getData().put(key, value); + } + return msg; + } + + @Override + protected boolean doEncode(DocumentMessage obj, DocumentSerializer buf) { + MapVisitorMessage msg = (MapVisitorMessage)obj; + buf.putInt(null, msg.getData().size()); + for (Map.Entry<String, String> pairs : msg.getData().entrySet()) { + encodeString(pairs.getKey(), buf); + encodeString(pairs.getValue(), buf); + } + return true; + } + } + + public static class MapVisitorReplyFactory extends DocumentReplyFactory { + + @Override + protected DocumentReply doDecode(DocumentDeserializer buf) { + return new VisitorReply(DocumentProtocol.REPLY_MAPVISITOR); + } + + @Override + protected boolean doEncode(DocumentReply obj, DocumentSerializer buf) { + return true; + } + } + + public static class BatchDocumentUpdateMessageFactory extends DocumentMessageFactory { + + private final LazyDecoder decoder = new LazyDecoder() { + + public void decode(Routable obj, DocumentDeserializer buf) { + BatchDocumentUpdateMessage msg = (BatchDocumentUpdateMessage)obj; + int size = buf.getInt(null); + for (int i = 0; i < size; i++) { + msg.addUpdate(new DocumentUpdate(buf)); + } + } + }; + + @Override + protected DocumentMessage doDecode(DocumentDeserializer buf) { + long userId = buf.getLong(null); + String group = decodeString(buf); + + if (group.length() > 0) { + return new BatchDocumentUpdateMessage(group, decoder, buf); + } else { + return new BatchDocumentUpdateMessage(userId, decoder, buf); + } + } + + @Override + protected boolean doEncode(DocumentMessage obj, DocumentSerializer buf) { + BatchDocumentUpdateMessage msg = (BatchDocumentUpdateMessage)obj; + + if (msg.getSerializedBuffer() != null) { + buf.put(null, msg.getSerializedBuffer()); + } else { + if (msg.getUserId() != null) { + buf.putLong(null, msg.getUserId()); + } else { + buf.putLong(null, 0); + } + + if (msg.getGroup() != null) { + encodeString(msg.getGroup(), buf); + } else { + encodeString("", buf); + } + + buf.putInt(null, msg.getUpdates().size()); + for (int i = 0; i < msg.getUpdates().size(); i++) { + msg.getUpdates().get(i).serialize(buf); + } + } + + return true; + } + } + + public static class BatchDocumentUpdateReplyFactory extends DocumentReplyFactory { + + @Override + protected DocumentReply doDecode(DocumentDeserializer buf) { + BatchDocumentUpdateReply rep = new BatchDocumentUpdateReply(); + rep.setHighestModificationTimestamp(buf.getLong(null)); + int size = buf.getInt(null); + rep.getDocumentsNotFound().ensureCapacity(size); + for (int i = 0; i < size; ++i) { + rep.getDocumentsNotFound().add(buf.getByte(null) == 1); + } + return rep; + } + + @Override + protected boolean doEncode(DocumentReply obj, DocumentSerializer buf) { + BatchDocumentUpdateReply rep = (BatchDocumentUpdateReply)obj; + buf.putLong(null, rep.getHighestModificationTimestamp()); + buf.putInt(null, rep.getDocumentsNotFound().size()); + for (int i = 0; i < rep.getDocumentsNotFound().size(); ++i) { + buf.putByte(null, (byte)(rep.getDocumentsNotFound().get(i) ? 1 : 0)); + } + return true; + } + } + + public static class PutDocumentMessageFactory extends DocumentMessageFactory { + protected void decodeInto(PutDocumentMessage msg, DocumentDeserializer buf) { + msg.setDocumentPut(new DocumentPut(Document.createDocument(buf))); + msg.setTimestamp(buf.getLong(null)); + } + + @Override + protected DocumentMessage doDecode(DocumentDeserializer buffer) { + final LazyDecoder decoder = (obj, buf) -> { + decodeInto((PutDocumentMessage) obj, buf); + }; + + return new PutDocumentMessage(decoder, buffer); + } + + @Override + protected boolean doEncode(DocumentMessage obj, DocumentSerializer buf) { + PutDocumentMessage msg = (PutDocumentMessage)obj; + if (msg.getSerializedBuffer() != null) { + buf.put(null, msg.getSerializedBuffer()); + } else { + msg.getDocumentPut().getDocument().serialize(buf); + buf.putLong(null, msg.getTimestamp()); + } + return true; + } + } + + public static class PutDocumentReplyFactory extends DocumentReplyFactory { + + @Override + protected DocumentReply doDecode(DocumentDeserializer buf) { + WriteDocumentReply rep = new WriteDocumentReply(DocumentProtocol.REPLY_PUTDOCUMENT); + rep.setHighestModificationTimestamp(buf.getLong(null)); + return rep; + } + + @Override + protected boolean doEncode(DocumentReply obj, DocumentSerializer buf) { + WriteDocumentReply rep = (WriteDocumentReply)obj; + buf.putLong(null, rep.getHighestModificationTimestamp()); + return true; + } + } + + public static class RemoveDocumentMessageFactory extends DocumentMessageFactory { + protected void decodeInto(RemoveDocumentMessage msg, DocumentDeserializer buf) { + msg.setDocumentId(new DocumentId(buf)); + } + + @Override + protected DocumentMessage doDecode(DocumentDeserializer buf) { + RemoveDocumentMessage msg = new RemoveDocumentMessage(); + decodeInto(msg, buf); + return msg; + } + + @Override + protected boolean doEncode(DocumentMessage obj, DocumentSerializer buf) { + RemoveDocumentMessage msg = (RemoveDocumentMessage)obj; + msg.getDocumentId().serialize(buf); + return true; + } + } + + public static class RemoveDocumentReplyFactory extends DocumentReplyFactory { + + @Override + protected DocumentReply doDecode(DocumentDeserializer buf) { + RemoveDocumentReply reply = new RemoveDocumentReply(); + byte flag = buf.getByte(null); + reply.setWasFound(flag != 0); + reply.setHighestModificationTimestamp(buf.getLong(null)); + return reply; + } + + @Override + protected boolean doEncode(DocumentReply obj, DocumentSerializer buf) { + RemoveDocumentReply reply = (RemoveDocumentReply)obj; + buf.putByte(null, (byte)(reply.wasFound() ? 1 : 0)); + buf.putLong(null, reply.getHighestModificationTimestamp()); + return true; + } + } + + public static class RemoveLocationMessageFactory extends DocumentMessageFactory { + + @Override + protected DocumentMessage doDecode(DocumentDeserializer buf) { + return new RemoveLocationMessage(decodeString(buf)); + } + + @Override + protected boolean doEncode(DocumentMessage obj, DocumentSerializer buf) { + RemoveLocationMessage msg = (RemoveLocationMessage)obj; + encodeString(msg.getDocumentSelection(), buf); + return true; + } + } + + public static class RemoveLocationReplyFactory extends DocumentReplyFactory { + + @Override + protected DocumentReply doDecode(DocumentDeserializer buf) { + return new DocumentReply(DocumentProtocol.REPLY_REMOVELOCATION); + } + + @Override + protected boolean doEncode(DocumentReply obj, DocumentSerializer buf) { + return true; + } + } + + public static class SearchResultMessageFactory extends DocumentMessageFactory { + + @Override + protected DocumentMessage doDecode(DocumentDeserializer buf) { + SearchResultMessage msg = new SearchResultMessage(); + msg.setSearchResult(new SearchResult(buf)); + return msg; + } + + @Override + protected boolean doEncode(DocumentMessage obj, DocumentSerializer buf) { + return false; // not supported + } + } + + public static class QueryResultMessageFactory extends DocumentMessageFactory { + + @Override + protected DocumentMessage doDecode(DocumentDeserializer buf) { + QueryResultMessage msg = new QueryResultMessage(); + msg.setSearchResult(new SearchResult(buf)); + msg.setSummary(new DocumentSummary(buf)); + return msg; + } + + @Override + protected boolean doEncode(DocumentMessage obj, DocumentSerializer buf) { + return false; // not supported + } + } + + public static class SearchResultReplyFactory extends DocumentReplyFactory { + + @Override + protected DocumentReply doDecode(DocumentDeserializer buf) { + return new VisitorReply(DocumentProtocol.REPLY_SEARCHRESULT); + } + + @Override + protected boolean doEncode(DocumentReply obj, DocumentSerializer buf) { + return true; + } + } + + public static class QueryResultReplyFactory extends DocumentReplyFactory { + + @Override + protected DocumentReply doDecode(DocumentDeserializer buf) { + return new VisitorReply(DocumentProtocol.REPLY_QUERYRESULT); + } + + @Override + protected boolean doEncode(DocumentReply obj, DocumentSerializer buf) { + return true; + } + } + + public static class StatBucketMessageFactory extends DocumentMessageFactory { + + @Override + protected DocumentMessage doDecode(DocumentDeserializer buf) { + StatBucketMessage msg = new StatBucketMessage(); + msg.setBucketId(new BucketId(buf.getLong(null))); + msg.setDocumentSelection(decodeString(buf)); + return msg; + } + + @Override + protected boolean doEncode(DocumentMessage obj, DocumentSerializer buf) { + StatBucketMessage msg = (StatBucketMessage)obj; + buf.putLong(null, msg.getBucketId().getRawId()); + encodeString(msg.getDocumentSelection(), buf); + return true; + } + } + + public static class StatBucketReplyFactory extends DocumentReplyFactory { + + @Override + protected DocumentReply doDecode(DocumentDeserializer buf) { + StatBucketReply reply = new StatBucketReply(); + reply.setResults(decodeString(buf)); + return reply; + } + + @Override + protected boolean doEncode(DocumentReply obj, DocumentSerializer buf) { + StatBucketReply reply = (StatBucketReply)obj; + encodeString(reply.getResults(), buf); + return true; + } + } + + public static class UpdateDocumentMessageFactory extends DocumentMessageFactory { + protected void decodeInto(UpdateDocumentMessage msg, DocumentDeserializer buf) { + msg.setDocumentUpdate(new DocumentUpdate(buf)); + msg.setOldTimestamp(buf.getLong(null)); + msg.setNewTimestamp(buf.getLong(null)); + } + + @Override + protected DocumentMessage doDecode(DocumentDeserializer buffer) { + final LazyDecoder decoder = (obj, buf) -> { + decodeInto((UpdateDocumentMessage) obj, buf); + }; + + return new UpdateDocumentMessage(decoder, buffer); + } + + @Override + protected boolean doEncode(DocumentMessage obj, DocumentSerializer buf) { + UpdateDocumentMessage msg = (UpdateDocumentMessage)obj; + if (msg.getSerializedBuffer() != null) { + buf.put(null, msg.getSerializedBuffer()); + } else { + msg.getDocumentUpdate().serialize(buf); + buf.putLong(null, msg.getOldTimestamp()); + buf.putLong(null, msg.getNewTimestamp()); + } + return true; + } + } + + public static class UpdateDocumentReplyFactory extends DocumentReplyFactory { + + @Override + protected DocumentReply doDecode(DocumentDeserializer buf) { + UpdateDocumentReply rep = new UpdateDocumentReply(); + byte flag = buf.getByte(null); + rep.setWasFound(flag != 0); + rep.setHighestModificationTimestamp(buf.getLong(null)); + return rep; + } + + @Override + protected boolean doEncode(DocumentReply obj, DocumentSerializer buf) { + UpdateDocumentReply rep = (UpdateDocumentReply)obj; + buf.putByte(null, (byte)(rep.wasFound() ? 1 : 0)); + buf.putLong(null, rep.getHighestModificationTimestamp()); + return true; + } + } + + public static class VisitorInfoMessageFactory extends DocumentMessageFactory { + + @Override + protected DocumentMessage doDecode(DocumentDeserializer buf) { + VisitorInfoMessage msg = new VisitorInfoMessage(); + int size = buf.getInt(null); + for (int i = 0; i < size; i++) { + long reversed = buf.getLong(null); + long rawid = ((reversed >>> 56) & 0x00000000000000FFl) | ((reversed >>> 40) & 0x000000000000FF00l) | + ((reversed >>> 24) & 0x0000000000FF0000l) | ((reversed >>> 8) & 0x00000000FF000000l) | + ((reversed << 8) & 0x000000FF00000000l) | ((reversed << 24) & 0x0000FF0000000000l) | + ((reversed << 40) & 0x00FF000000000000l) | ((reversed << 56) & 0xFF00000000000000l); + msg.getFinishedBuckets().add(new BucketId(rawid)); + } + + msg.setErrorMessage(decodeString(buf)); + return msg; + } + + @Override + protected boolean doEncode(DocumentMessage obj, DocumentSerializer buf) { + VisitorInfoMessage msg = (VisitorInfoMessage)obj; + buf.putInt(null, msg.getFinishedBuckets().size()); + for (BucketId id : msg.getFinishedBuckets()) { + long rawid = id.getRawId(); + long reversed = ((rawid >>> 56) & 0x00000000000000FFl) | ((rawid >>> 40) & 0x000000000000FF00l) | + ((rawid >>> 24) & 0x0000000000FF0000l) | ((rawid >>> 8) & 0x00000000FF000000l) | + ((rawid << 8) & 0x000000FF00000000l) | ((rawid << 24) & 0x0000FF0000000000l) | + ((rawid << 40) & 0x00FF000000000000l) | ((rawid << 56) & 0xFF00000000000000l); + buf.putLong(null, reversed); + } + encodeString(msg.getErrorMessage(), buf); + return true; + } + } + + public static class VisitorInfoReplyFactory extends DocumentReplyFactory { + + @Override + protected DocumentReply doDecode(DocumentDeserializer buf) { + return new VisitorReply(DocumentProtocol.REPLY_VISITORINFO); + } + + @Override + protected boolean doEncode(DocumentReply obj, DocumentSerializer buf) { + return true; + } + } + + public static class WrongDistributionReplyFactory extends DocumentReplyFactory { + + @Override + protected DocumentReply doDecode(DocumentDeserializer buf) { + WrongDistributionReply reply = new WrongDistributionReply(); + reply.setSystemState(decodeString(buf)); + return reply; + } + + @Override + protected boolean doEncode(DocumentReply obj, DocumentSerializer buf) { + WrongDistributionReply reply = (WrongDistributionReply)obj; + encodeString(reply.getSystemState(), buf); + return true; + } + } + +} diff --git a/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/RoutableFactories51.java b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/RoutableFactories51.java new file mode 100755 index 00000000000..0005dc8d296 --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/RoutableFactories51.java @@ -0,0 +1,126 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi.messagebus.protocol; + +import com.yahoo.document.BucketId; +import com.yahoo.document.DocumentId; +import com.yahoo.document.serialization.DocumentDeserializer; +import com.yahoo.document.serialization.DocumentSerializer; + +import java.util.Map; + +/** + * This class encapsulates all the {@link RoutableFactory} classes needed to implement serialization for the document + * protocol. When adding new factories to this class, please KEEP THE THEM ORDERED alphabetically like they are now. + * + */ +public abstract class RoutableFactories51 extends RoutableFactories50 { + + public static class CreateVisitorMessageFactory extends DocumentMessageFactory { + + @Override + protected DocumentMessage doDecode(DocumentDeserializer buf) { + CreateVisitorMessage msg = new CreateVisitorMessage(); + msg.setLibraryName(decodeString(buf)); + msg.setInstanceId(decodeString(buf)); + msg.setControlDestination(decodeString(buf)); + msg.setDataDestination(decodeString(buf)); + msg.setDocumentSelection(decodeString(buf)); + msg.setMaxPendingReplyCount(buf.getInt(null)); + + int size = buf.getInt(null); + for (int i = 0; i < size; i++) { + long reversed = buf.getLong(null); + long rawid = ((reversed >>> 56) & 0x00000000000000FFl) | ((reversed >>> 40) & 0x000000000000FF00l) | + ((reversed >>> 24) & 0x0000000000FF0000l) | ((reversed >>> 8) & 0x00000000FF000000l) | + ((reversed << 8) & 0x000000FF00000000l) | ((reversed << 24) & 0x0000FF0000000000l) | + ((reversed << 40) & 0x00FF000000000000l) | ((reversed << 56) & 0xFF00000000000000l); + msg.getBuckets().add(new BucketId(rawid)); + } + + msg.setFromTimestamp(buf.getLong(null)); + msg.setToTimestamp(buf.getLong(null)); + msg.setVisitRemoves(buf.getByte(null) == (byte)1); + msg.setFieldSet(decodeString(buf)); + msg.setVisitInconsistentBuckets(buf.getByte(null) == (byte)1); + + size = buf.getInt(null); + for (int i = 0; i < size; i++) { + String key = decodeString(buf); + int sz = buf.getInt(null); + msg.getParameters().put(key, buf.getBytes(null, sz)); + } + + msg.setVisitorOrdering(buf.getInt(null)); + msg.setMaxBucketsPerVisitor(buf.getInt(null)); + msg.setVisitorDispatcherVersion(50); + return msg; + } + + @Override + protected boolean doEncode(DocumentMessage obj, DocumentSerializer buf) { + CreateVisitorMessage msg = (CreateVisitorMessage)obj; + encodeString(msg.getLibraryName(), buf); + encodeString(msg.getInstanceId(), buf); + encodeString(msg.getControlDestination(), buf); + encodeString(msg.getDataDestination(), buf); + encodeString(msg.getDocumentSelection(), buf); + buf.putInt(null, msg.getMaxPendingReplyCount()); + + buf.putInt(null, msg.getBuckets().size()); + for (BucketId id : msg.getBuckets()) { + long rawid = id.getRawId(); + long reversed = ((rawid >>> 56) & 0x00000000000000FFl) | ((rawid >>> 40) & 0x000000000000FF00l) | + ((rawid >>> 24) & 0x0000000000FF0000l) | ((rawid >>> 8) & 0x00000000FF000000l) | + ((rawid << 8) & 0x000000FF00000000l) | ((rawid << 24) & 0x0000FF0000000000l) | + ((rawid << 40) & 0x00FF000000000000l) | ((rawid << 56) & 0xFF00000000000000l); + buf.putLong(null, reversed); + } + + buf.putLong(null, msg.getFromTimestamp()); + buf.putLong(null, msg.getToTimestamp()); + buf.putByte(null, msg.getVisitRemoves() ? (byte)1 : (byte)0); + encodeString(msg.getFieldSet(), buf); + buf.putByte(null, msg.getVisitInconsistentBuckets() ? (byte)1 : (byte)0); + + buf.putInt(null, msg.getParameters().size()); + for (Map.Entry<String, byte[]> pairs : msg.getParameters().entrySet()) { + encodeString(pairs.getKey(), buf); + byte[] b = pairs.getValue(); + buf.putInt(null, b.length); + buf.put(null, b); + } + + buf.putInt(null, msg.getVisitorOrdering()); + buf.putInt(null, msg.getMaxBucketsPerVisitor()); + return true; + } + } + + public static class GetDocumentMessageFactory extends DocumentMessageFactory { + + @Override + protected DocumentMessage doDecode(DocumentDeserializer buf) { + return new GetDocumentMessage(new DocumentId(buf), decodeString(buf)); + } + + @Override + protected boolean doEncode(DocumentMessage obj, DocumentSerializer buf) { + GetDocumentMessage msg = (GetDocumentMessage)obj; + msg.getDocumentId().serialize(buf); + encodeString(msg.getFieldSet(), buf); + return true; + } + } + + public static class DocumentIgnoredReplyFactory extends DocumentReplyFactory { + @Override + protected DocumentReply doDecode(DocumentDeserializer buf) { + return new DocumentIgnoredReply(); + } + + @Override + protected boolean doEncode(DocumentReply obj, DocumentSerializer buf) { + return true; + } + } +} diff --git a/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/RoutableFactories52.java b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/RoutableFactories52.java new file mode 100644 index 00000000000..035309f373f --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/RoutableFactories52.java @@ -0,0 +1,87 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi.messagebus.protocol; + +import com.google.common.annotations.Beta; +import com.yahoo.document.TestAndSetCondition; +import com.yahoo.document.serialization.DocumentDeserializer; +import com.yahoo.document.serialization.DocumentSerializer; + +import static com.yahoo.documentapi.messagebus.protocol.AbstractRoutableFactory.decodeString; +import static com.yahoo.documentapi.messagebus.protocol.AbstractRoutableFactory.encodeString; + +/** + * @author Vegard Sjonfjell + */ + +@Beta +public abstract class RoutableFactories52 extends RoutableFactories51 { + protected static class PutDocumentMessageFactory extends RoutableFactories51.PutDocumentMessageFactory { + @Override + protected void decodeInto(PutDocumentMessage msg, DocumentDeserializer buf) { + super.decodeInto(msg, buf); + decodeTasCondition(msg, buf); + } + + @Override + protected boolean doEncode(DocumentMessage obj, DocumentSerializer buf) { + if (!super.doEncode(obj, buf)) { + return false; + } + + // If the serialized buffer exists, the test and set condition has already been encoded + if (((PutDocumentMessage)obj).getSerializedBuffer() == null) { + encodeTasCondition(buf, (TestAndSetMessage) obj); + } + + return true; + } + } + + protected static class RemoveDocumentMessageFactory extends RoutableFactories51.RemoveDocumentMessageFactory { + @Override + protected void decodeInto(RemoveDocumentMessage msg, DocumentDeserializer buf) { + super.decodeInto(msg, buf); + decodeTasCondition(msg, buf); + } + + @Override + protected boolean doEncode(DocumentMessage obj, DocumentSerializer buf) { + if (!super.doEncode(obj, buf)) { + return false; + } + + encodeTasCondition(buf, (TestAndSetMessage) obj); + return true; + } + } + + protected static class UpdateDocumentMessageFactory extends RoutableFactories51.UpdateDocumentMessageFactory { + @Override + protected void decodeInto(UpdateDocumentMessage msg, DocumentDeserializer buf) { + super.decodeInto(msg, buf); + decodeTasCondition(msg, buf); + } + + @Override + protected boolean doEncode(DocumentMessage obj, DocumentSerializer buf) { + if (!super.doEncode(obj, buf)) { + return false; + } + + // If the serialized buffer exists, the test and set condition has already been encoded + if (((UpdateDocumentMessage)obj).getSerializedBuffer() == null) { + encodeTasCondition(buf, (TestAndSetMessage) obj); + } + + return true; + } + } + + static void decodeTasCondition(TestAndSetMessage msg, DocumentDeserializer buf) { + msg.setCondition(new TestAndSetCondition(decodeString(buf))); + } + + static void encodeTasCondition(DocumentSerializer buf, TestAndSetMessage msg) { + encodeString(msg.getCondition().getSelection(), buf); + } +} diff --git a/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/RoutableFactory.java b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/RoutableFactory.java new file mode 100755 index 00000000000..d5c30101baf --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/RoutableFactory.java @@ -0,0 +1,44 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi.messagebus.protocol; + +import com.yahoo.document.serialization.DocumentDeserializer; +import com.yahoo.document.serialization.DocumentSerializer; +import com.yahoo.documentapi.messagebus.loadtypes.LoadTypeSet; +import com.yahoo.messagebus.Routable; + +/** + * <p>This interface defines the necessary methods of a routable factory that can be plugged into a {@link + * DocumentProtocol} using the {@link DocumentProtocol#putRoutableFactory(int, RoutableFactory, + * com.yahoo.component.VersionSpecification)} method. </p> + * + * <p>Notice that no routable type is passed to the + * {@link #decode(DocumentDeserializer, com.yahoo.documentapi.messagebus.loadtypes.LoadTypeSet)} method, so + * you may NOT share a factory across multiple routable types. To share serialization logic between factory use a common + * superclass or composition with a common serialization utility.</p> + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public interface RoutableFactory { + + /** + * <p>This method encodes the content of the given routable into a byte buffer that can later be decoded using the + * {@link #decode(DocumentDeserializer, com.yahoo.documentapi.messagebus.loadtypes.LoadTypeSet)} method.</p> <p>Return false to signal failure.</p> + * <p>This method is NOT exception safe.</p> + * + * @param obj The routable to encode. + * @param out The buffer to write into. + * @return True if the routable could be encoded. + */ + boolean encode(Routable obj, DocumentSerializer out); + + /** + * <p>This method decodes the given byte bufer to a routable.</p> <p>Return false to signal failure.</p> <p>This + * method is NOT exception safe.</p> + * + * @param in The buffer to read from. + * @param loadTypes The LoadTypeSet to inject into the Routable. + * @return The decoded routable. + */ + Routable decode(DocumentDeserializer in, LoadTypeSet loadTypes); + +} diff --git a/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/RoutableRepository.java b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/RoutableRepository.java new file mode 100755 index 00000000000..6f044a1951f --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/RoutableRepository.java @@ -0,0 +1,237 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi.messagebus.protocol;
+
+import com.yahoo.component.Version;
+import com.yahoo.component.VersionSpecification;
+import com.yahoo.concurrent.CopyOnWriteHashMap;
+import com.yahoo.document.DocumentTypeManager;
+import com.yahoo.document.serialization.*;
+import com.yahoo.documentapi.messagebus.loadtypes.LoadTypeSet;
+import com.yahoo.io.GrowableByteBuffer;
+import com.yahoo.log.LogLevel;
+import com.yahoo.messagebus.Routable;
+
+import java.util.*;
+import java.util.logging.Logger;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * This class encapsulates the logic required to map routable type and version to a corresponding {@link
+ * RoutableFactory}. It is owned and accessed through a {@link DocumentProtocol} instance. This class uses a factory
+ * cache to reduce the latency of matching version specifications to actual versions when resolving factories.
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+final class RoutableRepository {
+
+ private static final Logger log = Logger.getLogger(RoutableRepository.class.getName());
+ private final CopyOnWriteHashMap<Integer, VersionMap> factoryTypes = new CopyOnWriteHashMap<>();
+ private final CopyOnWriteHashMap<CacheKey, RoutableFactory> cache = new CopyOnWriteHashMap<>();
+ private LoadTypeSet loadTypes;
+
+ public RoutableRepository(LoadTypeSet set) {
+ loadTypes = set;
+ }
+
+ /**
+ * Decodes a {@link Routable} from the given byte array. This uses the content of the byte array to dispatch the
+ * decode request to the appropriate {@link RoutableFactory} that was previously registered.
+ *
+ * If a routable can not be decoded, this method returns null.
+ *
+ * @param version The version of the encoded routable.
+ * @param data The byte array containing the encoded routable.
+ * @return The decoded routable.
+ */
+ Routable decode(DocumentTypeManager docMan, Version version, byte[] data) {
+ if (data == null || data.length == 0) {
+ log.log(LogLevel.ERROR, "Received empty byte array for deserialization.");
+ return null;
+ }
+ DocumentDeserializer in;
+
+ if (version.getMajor() >= 5) {
+ in = DocumentDeserializerFactory.createHead(docMan, GrowableByteBuffer.wrap(data));
+ } else {
+ in = DocumentDeserializerFactory.create42(docMan, GrowableByteBuffer.wrap(data));
+ }
+
+ int type = in.getInt(null);
+ RoutableFactory factory = getFactory(version, type);
+ if (factory == null) {
+ log.log(LogLevel.ERROR, "No routable factory found for routable type " + type +
+ " (version " + version + ").");
+ return null;
+ }
+ Routable ret = factory.decode(in, loadTypes);
+ if (ret == null) {
+ log.log(LogLevel.ERROR, "Routable factory " + factory.getClass().getName() + " failed to deserialize " +
+ "routable of type " + type + " (version " + version + ").");
+ log.log(LogLevel.ERROR, Arrays.toString(data));
+ return null;
+ }
+ return ret;
+ }
+
+ /**
+ * Encodes a {@link Routable} into a byte array. This dispatches the encode request to the appropriate {@link
+ * RoutableFactory} that was previously registered.
+ *
+ * If a routable can not be encoded, this method returns an empty byte array.
+ *
+ * @param version The version to encode the routable as.
+ * @param obj The routable to encode.
+ * @return The byte array containing the encoded routable.
+ */
+ byte[] encode(Version version, Routable obj) {
+ int type = obj.getType();
+ RoutableFactory factory = getFactory(version, type);
+ if (factory == null) {
+ log.log(LogLevel.ERROR, "No routable factory found for routable type " + type +
+ " (version " + version + ").");
+ return new byte[0];
+ }
+ DocumentSerializer out;
+
+ if (version.getMajor() >= 5) {
+ out = DocumentSerializerFactory.createHead(new GrowableByteBuffer(8192));
+ } else {
+ out = DocumentSerializerFactory.create42(new GrowableByteBuffer(8192));
+ }
+
+ out.putInt(null, type);
+ if (!factory.encode(obj, out)) {
+ log.log(LogLevel.ERROR, "Routable factory " + factory.getClass().getName() + " failed to serialize " +
+ "routable of type " + type + " (version " + version + ").");
+ return new byte[0];
+ }
+ byte[] ret = new byte[out.getBuf().position()];
+ out.getBuf().rewind();
+ out.getBuf().get(ret);
+ return ret;
+ }
+
+ /**
+ * Registers a routable factory for a given version and routable type.
+ *
+ * @param version The version specification that the given factory supports.
+ * @param type The routable type that the given factory supports.
+ * @param factory The routable factory to register.
+ */
+ void putFactory(VersionSpecification version, int type, RoutableFactory factory) {
+ VersionMap versionMap = factoryTypes.get(type);
+ if (versionMap == null) {
+ versionMap = new VersionMap();
+
+ factoryTypes.put(type, versionMap);
+ }
+ if (versionMap.putFactory(version, factory)) {
+ cache.clear();
+ }
+ }
+
+ /**
+ * Returns the routable factory for a given version and routable type.
+ *
+ * @param version The version that the factory must support.
+ * @param type The routable type that the factory must support.
+ * @return The routable factory matching the criteria, or null.
+ */
+ RoutableFactory getFactory(Version version, int type) {
+ CacheKey cacheKey = new CacheKey(version, type);
+ RoutableFactory factory = cache.get(cacheKey);
+ if (factory != null) {
+ return factory;
+ }
+ VersionMap versionMap = factoryTypes.get(type);
+ if (versionMap == null) {
+ return null;
+ }
+ factory = versionMap.getFactory(version);
+ if (factory == null) {
+ return null;
+ }
+ cache.put(cacheKey, factory);
+ return factory;
+ }
+
+ /**
+ * Returns a list of routable types that support the given version.
+ *
+ * @param version The version to return types for.
+ * @return The list of supported types.
+ */
+ List<Integer> getRoutableTypes(Version version) {
+ List<Integer> ret = new ArrayList<>();
+ for (Map.Entry<Integer, VersionMap> entry : factoryTypes.entrySet()) {
+ if (entry.getValue().getFactory(version) != null) {
+ ret.add(entry.getKey());
+ }
+ }
+ return ret;
+ }
+
+ /**
+ * Internal helper class that implements a map from {@link VersionSpecification} to {@link RoutableFactory}.
+ */
+ private static class VersionMap {
+
+ final Map<VersionSpecification, RoutableFactory> factoryVersions = new HashMap<>();
+
+ boolean putFactory(VersionSpecification version, RoutableFactory factory) {
+ return factoryVersions.put(version, factory) == null;
+ }
+
+ RoutableFactory getFactory(Version version) {
+ VersionSpecification versionSpec = version.toSpecification();
+
+ // Retrieve the factory with the highest version lower than or equal to actual version
+ return factoryVersions.entrySet().stream()
+ // Drop factories that have a higher version than actual version
+ .filter(entry -> entry.getKey().compareTo(versionSpec) <= 0)
+
+ // Get the factory with the highest version
+ .max((entry1, entry2) -> entry1.getKey().compareTo(entry2.getKey()))
+ .map(Map.Entry::getValue)
+
+ // Return factory or null if no suitable factory found
+ .orElse(null);
+ }
+ }
+
+ /**
+ * Internal helper class that implements a cache key for mapping a {@link Version} and routable type to a {@link
+ * RoutableFactory}.
+ */
+ private static class CacheKey {
+
+ final Version version;
+ final int type;
+
+ public CacheKey(Version version, int type) {
+ this.version = version;
+ this.type = type;
+ }
+
+ @Override
+ public int hashCode() {
+ return version.hashCode() + type;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof CacheKey)) {
+ return false;
+ }
+ CacheKey rhs = (CacheKey)obj;
+ if (!version.equals(rhs.version)) {
+ return false;
+ }
+ if (type != rhs.type) {
+ return false;
+ }
+ return true;
+ }
+ }
+}
diff --git a/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/RoutingPolicyFactories.java b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/RoutingPolicyFactories.java new file mode 100755 index 00000000000..05e39503308 --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/RoutingPolicyFactories.java @@ -0,0 +1,148 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi.messagebus.protocol;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public abstract class RoutingPolicyFactories {
+
+ static class AndPolicyFactory implements RoutingPolicyFactory {
+ public DocumentProtocolRoutingPolicy createPolicy(String param) {
+ return new ANDPolicy(param);
+ }
+
+
+ public void destroy() {
+ }
+ }
+
+ static class StoragePolicyFactory implements RoutingPolicyFactory {
+ public DocumentProtocolRoutingPolicy createPolicy(String param) {
+ return new StoragePolicy(param);
+ }
+
+ public void destroy() {
+ }
+ }
+
+ static class ContentPolicyFactory implements RoutingPolicyFactory {
+ public DocumentProtocolRoutingPolicy createPolicy(String param) {
+ return new ContentPolicy(param);
+ }
+
+ public void destroy() {
+ }
+ }
+
+ static class MessageTypePolicyFactory implements RoutingPolicyFactory {
+ private final String configId;
+
+ public MessageTypePolicyFactory(String configId) {
+ this.configId = configId;
+ }
+ public DocumentProtocolRoutingPolicy createPolicy(String param) {
+ return new MessageTypePolicy((param == null || param.isEmpty()) ? configId : param);
+ }
+
+ public void destroy() {
+ }
+ }
+
+ static class DocumentRouteSelectorPolicyFactory implements RoutingPolicyFactory {
+
+ private final String configId;
+
+ public DocumentRouteSelectorPolicyFactory(String configId) {
+ this.configId = configId;
+ }
+
+ public DocumentProtocolRoutingPolicy createPolicy(String param) {
+ DocumentRouteSelectorPolicy ret = new DocumentRouteSelectorPolicy((param == null || param.isEmpty()) ?
+ configId : param);
+ String error = ret.getError();
+ if (error != null) {
+ return new ErrorPolicy(error);
+ }
+ return ret;
+ }
+
+
+ public void destroy() {
+ }
+ }
+
+ static class ExternPolicyFactory implements RoutingPolicyFactory {
+ public DocumentProtocolRoutingPolicy createPolicy(String param) {
+ ExternPolicy ret = new ExternPolicy(param);
+ String error = ret.getError();
+ if (error != null) {
+ return new ErrorPolicy(error);
+ }
+ return ret;
+ }
+
+
+ public void destroy() {
+ }
+ }
+
+ static class LocalServicePolicyFactory implements RoutingPolicyFactory {
+ public DocumentProtocolRoutingPolicy createPolicy(String param) {
+ return new LocalServicePolicy(param);
+ }
+
+
+ public void destroy() {
+ }
+ }
+
+ static class RoundRobinPolicyFactory implements RoutingPolicyFactory {
+ public DocumentProtocolRoutingPolicy createPolicy(String param) {
+ return new RoundRobinPolicy();
+ }
+
+
+ public void destroy() {
+ }
+ }
+
+ static class LoadBalancerPolicyFactory implements RoutingPolicyFactory {
+ public DocumentProtocolRoutingPolicy createPolicy(String param) {
+ return new LoadBalancerPolicy(param);
+ }
+
+
+ public void destroy() {
+ }
+ }
+
+ static class SearchColumnPolicyFactory implements RoutingPolicyFactory {
+ public DocumentProtocolRoutingPolicy createPolicy(String param) {
+ return new SearchColumnPolicy(param);
+ }
+
+
+ public void destroy() {
+ }
+ }
+
+ static class SearchRowPolicyFactory implements RoutingPolicyFactory {
+ public DocumentProtocolRoutingPolicy createPolicy(String param) {
+ return new SearchRowPolicy(param);
+ }
+
+
+ public void destroy() {
+ }
+ }
+
+ static class SubsetServicePolicyFactory implements RoutingPolicyFactory {
+ public DocumentProtocolRoutingPolicy createPolicy(String param) {
+ return new SubsetServicePolicy(param);
+ }
+
+
+ public void destroy() {
+ }
+ }
+}
diff --git a/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/RoutingPolicyFactory.java b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/RoutingPolicyFactory.java new file mode 100755 index 00000000000..93e1aa7fbb5 --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/RoutingPolicyFactory.java @@ -0,0 +1,34 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi.messagebus.protocol; + +import com.yahoo.messagebus.routing.RoutingPolicy; + +/** + * This interface defines the necessary methods of a routing policy factory that can be plugged into a {@link + * DocumentProtocol} using the {@link DocumentProtocol#putRoutingPolicyFactory(String, RoutingPolicyFactory)} method. + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public interface RoutingPolicyFactory { + + /** + * This method creates and returns a routing policy that corresponds to the implementing class, using the given + * parameter string. There is only ever one instance of a routing policy for a given name and parameter combination, + * and because of this the policies must be state-less beyond what can be derived from the parameter string. Because + * there is only a single thread running route resolution within message bus, it is not necessary to make policies + * thread-safe. For more information see {@link RoutingPolicy}. + * + * Do NOT throw exceptions out of this method because that will cause the running thread to die, just return null to + * signal failure instead. + * + * @param param The parameter to use when creating the policy. + * @return The created routing policy. + */ + public DocumentProtocolRoutingPolicy createPolicy(String param); + + /** + * Destroys this factory and frees up any resources it has held. Making further calls on a destroyed + * factory causes a runtime exception. + */ + public void destroy(); +} diff --git a/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/RoutingPolicyRepository.java b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/RoutingPolicyRepository.java new file mode 100755 index 00000000000..3bfa85ac4d5 --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/RoutingPolicyRepository.java @@ -0,0 +1,76 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi.messagebus.protocol;
+
+import com.yahoo.documentapi.metrics.DocumentProtocolMetricSet;
+import com.yahoo.messagebus.routing.RoutingPolicy;
+import com.yahoo.log.LogLevel;
+
+import java.util.Map;
+import java.util.logging.Logger;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+class RoutingPolicyRepository {
+
+ private static final Logger log = Logger.getLogger(RoutingPolicyRepository.class.getName());
+ private final Map<String, RoutingPolicyFactory> factories = new ConcurrentHashMap<String, RoutingPolicyFactory>();
+ private final DocumentProtocolMetricSet metrics;
+
+ RoutingPolicyRepository(DocumentProtocolMetricSet metrics) {
+ this.metrics = metrics;
+ }
+
+ /**
+ * Registers a routing policy factory for a given name.
+ *
+ * @param name The name of the factory to register.
+ * @param factory The factory to register.
+ */
+ void putFactory(String name, RoutingPolicyFactory factory) {
+ factories.put(name, factory);
+ }
+
+ /**
+ * Returns the routing policy factory for a given name.
+ *
+ * @param name The name of the factory to return.
+ * @return The routing policy factory matching the criteria, or null.
+ */
+ RoutingPolicyFactory getFactory(String name) {
+ return factories.get(name);
+ }
+
+ /**
+ * Creates and returns a routing policy using the named factory and the given parameter.
+ *
+ * @param name The name of the factory to use.
+ * @param param The parameter to pass to the factory.
+ * @return The created policy.
+ */
+ RoutingPolicy createPolicy(String name, String param) {
+ RoutingPolicyFactory factory = getFactory(name);
+ if (factory == null) {
+ log.log(LogLevel.ERROR, "No routing policy factory found for name '" + name + "'.");
+ return null;
+ }
+ DocumentProtocolRoutingPolicy ret;
+ try {
+ ret = factory.createPolicy(param);
+ } catch (Exception e) {
+ ret = new ErrorPolicy(e.getMessage());
+ }
+
+ if (ret.getMetrics() != null) {
+ metrics.routingPolicyMetrics.addMetric(ret.getMetrics());
+ }
+
+ if (ret == null) {
+ log.log(LogLevel.ERROR, "Routing policy factory " + factory.getClass().getName() + " failed to create a " +
+ "routing policy for parameter '" + name + "'.");
+ return null;
+ }
+ return ret;
+ }
+}
diff --git a/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/SearchColumnPolicy.java b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/SearchColumnPolicy.java new file mode 100644 index 00000000000..3a329e6e6cf --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/SearchColumnPolicy.java @@ -0,0 +1,183 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi.messagebus.protocol; + +import com.yahoo.document.BucketId; +import com.yahoo.document.BucketIdFactory; +import com.yahoo.document.DocumentId; +import com.yahoo.log.LogLevel; +import com.yahoo.messagebus.EmptyReply; +import com.yahoo.messagebus.ErrorCode; +import com.yahoo.messagebus.Message; +import com.yahoo.messagebus.Reply; +import com.yahoo.messagebus.metrics.MetricSet; +import com.yahoo.messagebus.routing.Route; +import com.yahoo.messagebus.routing.RoutingContext; +import com.yahoo.messagebus.routing.RoutingNodeIterator; +import com.yahoo.vdslib.BucketDistribution; + +import java.util.*; +import java.util.logging.Logger; + +/** + * <p>This policy implements the logic to select recipients for a single search column. It has 2 different modes of + * operation;</p> + * + * <ol> + * <li>If the "maxbadparts" parameter is 0, select recipient based on document id hash and use + * shared merge logic. Do not allow any out-of-service replies.</li> + * <li>Else do best-effort validation of system + * state. This means; + * <ol> + * <li>if the message is sending to all recipients (typicall start- and + * end-of-feed), allow at most "maxbadparts" out-of-service replies,</li> + * <li>else always allow out-of-service reply by masking it with an empty + * reply.</li> + * </ol> + * </li> + * </ol> + * <p>For systems that allow bad parts, one will not know whether or not feeding + * was a success until the RTX attempts to set the new index live, because it is + * only the RTX that is now able to verify that the service level requirements + * are met. Feeding will still break if a message that was supposed to be sent + * to all recipients receives more than "maxbadparts" out-of-service replies, + * according to (2.a) above.</p> + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class SearchColumnPolicy implements DocumentProtocolRoutingPolicy { + + private static Logger log = Logger.getLogger(SearchColumnPolicy.class.getName()); + private BucketIdFactory factory = new BucketIdFactory(); + private Map<Integer, BucketDistribution> distributions = new HashMap<Integer, BucketDistribution>(); + private int maxOOS = 0; // The maximum OUT_OF_SERVICE replies to hide. + + public static final int DEFAULT_NUM_BUCKET_BITS = 16; + + /** + * Constructs a new policy object for the given parameter string. The string can be null or empty, which is a + * request to not allow any bad columns. + * + * @param param The maximum number of allowed bad columns. + */ + public SearchColumnPolicy(String param) { + if (param != null && param.length() > 0) { + try { + maxOOS = Integer.parseInt(param); + } catch (NumberFormatException e) { + log.log(LogLevel.WARNING, "Parameter '" + param + "' could not be parsed as an integer.", e); + } + if (maxOOS < 0) { + log.log(LogLevel.WARNING, "Ignoring a request to set the maximum number of OOS replies to " + maxOOS + + " because it makes no sense. This routing policy will not allow any recipient" + + " to be out of service."); + } + } + } + + @Override + public void select(RoutingContext context) { + List<Route> recipients = context.getMatchedRecipients(); + if (recipients == null || recipients.size() == 0) { + return; + } + DocumentId id = null; + BucketId bucketId = null; + Message msg = context.getMessage(); + switch (msg.getType()) { + + case DocumentProtocol.MESSAGE_PUTDOCUMENT: + id = ((PutDocumentMessage)msg).getDocumentPut().getDocument().getId(); + break; + + case DocumentProtocol.MESSAGE_GETDOCUMENT: + id = ((GetDocumentMessage)msg).getDocumentId(); + break; + + case DocumentProtocol.MESSAGE_REMOVEDOCUMENT: + id = ((RemoveDocumentMessage)msg).getDocumentId(); + break; + + case DocumentProtocol.MESSAGE_UPDATEDOCUMENT: + id = ((UpdateDocumentMessage)msg).getDocumentUpdate().getId(); + break; + + case DocumentProtocol.MESSAGE_BATCHDOCUMENTUPDATE: + bucketId = ((BatchDocumentUpdateMessage)msg).getBucketId(); + break; + + case DocumentProtocol.MESSAGE_GETBUCKETSTATE: + bucketId = ((GetBucketStateMessage)msg).getBucketId(); + break; + + default: + throw new UnsupportedOperationException("Message type '" + msg.getType() + "' not supported."); + } + if (bucketId == null && id != null) { + bucketId = factory.getBucketId(id); + } + int recipient = getRecipient(bucketId, recipients.size()); + context.addChild(recipients.get(recipient)); + context.setSelectOnRetry(true); + if (maxOOS > 0) { + context.addConsumableError(ErrorCode.SERVICE_OOS); + } + } + + @Override + public void merge(RoutingContext context) { + if (maxOOS > 0) { + if (context.getNumChildren() > 1) { + Set<Integer> oosReplies = new HashSet<Integer>(); + int idx = 0; + for (RoutingNodeIterator it = context.getChildIterator(); + it.isValid(); it.next()) + { + Reply ref = it.getReplyRef(); + if (ref.hasErrors() && DocumentProtocol.hasOnlyErrorsOfType(ref, ErrorCode.SERVICE_OOS)) { + oosReplies.add(idx); + } + ++idx; + } + if (oosReplies.size() <= maxOOS) { + DocumentProtocol.merge(context, oosReplies); + return; // may the rtx be with you + } + } else { + Reply ref = context.getChildIterator().getReplyRef(); + if (ref.hasErrors() && DocumentProtocol.hasOnlyErrorsOfType(ref, ErrorCode.SERVICE_OOS)) { + context.setReply(new EmptyReply()); + return; // god help us all + } + } + } + DocumentProtocol.merge(context); + } + + /** + * Returns the recipient index for the given bucket id. This updates the shared internal distribution map, so it + * needs to be synchronized. + * + * @param bucketId The bucket whose recipient to return. + * @param numRecipients The number of recipients being distributed to. + * @return The recipient to use. + */ + private synchronized int getRecipient(BucketId bucketId, int numRecipients) { + BucketDistribution distribution = distributions.get(numRecipients); + if (distribution == null) { + distribution = new BucketDistribution(1, DEFAULT_NUM_BUCKET_BITS); + distribution.setNumColumns(numRecipients); + distributions.put(numRecipients, distribution); + } + return distribution.getColumn(bucketId); + } + + @Override + public void destroy() { + // empty + } + + @Override + public MetricSet getMetrics() { + return null; + } +} diff --git a/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/SearchResultMessage.java b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/SearchResultMessage.java new file mode 100644 index 00000000000..9dd4085ffb3 --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/SearchResultMessage.java @@ -0,0 +1,27 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi.messagebus.protocol; + +import com.yahoo.vdslib.SearchResult; + +public class SearchResultMessage extends VisitorMessage { + + private SearchResult searchResult = null; + + public SearchResult getResult() { + return searchResult; + } + + public void setSearchResult(SearchResult result) { + searchResult = result; + } + + @Override + public DocumentReply createReply() { + return new VisitorReply(DocumentProtocol.REPLY_SEARCHRESULT); + } + + @Override + public int getType() { + return DocumentProtocol.MESSAGE_SEARCHRESULT; + } +} diff --git a/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/SearchRowPolicy.java b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/SearchRowPolicy.java new file mode 100755 index 00000000000..d36d3ee1e4c --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/SearchRowPolicy.java @@ -0,0 +1,85 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi.messagebus.protocol;
+
+import com.yahoo.log.LogLevel;
+import com.yahoo.messagebus.ErrorCode;
+import com.yahoo.messagebus.Reply;
+import com.yahoo.messagebus.metrics.MetricSet;
+import com.yahoo.messagebus.routing.RoutingContext;
+import com.yahoo.messagebus.routing.RoutingNodeIterator;
+
+import java.util.HashSet;
+import java.util.Set;
+import java.util.logging.Logger;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class SearchRowPolicy implements DocumentProtocolRoutingPolicy {
+
+ private static Logger log = Logger.getLogger(SearchRowPolicy.class.getName());
+ private int minOk = 0; // Hide OUT_OF_SERVICE as long as this number of replies are something else.
+
+ /**
+ * Creates a search row policy that wraps the underlying search group policy in case the parameter is something
+ * other than an empty string.
+ *
+ * @param param The number of minimum non-OOS replies that this policy requires.
+ */
+ public SearchRowPolicy(String param) {
+ if (param != null && param.length() > 0) {
+ try {
+ minOk = Integer.parseInt(param);
+ }
+ catch (NumberFormatException e) {
+ log.log(LogLevel.WARNING, "Parameter '" + param + "' could not be parsed as an integer.", e);
+ }
+ if (minOk <= 0) {
+ log.log(LogLevel.WARNING, "Ignoring a request to set the minimum number of OK replies to " + minOk + " " +
+ "because it makes no sense. This routing policy will not allow any recipient " +
+ "to be out of service.");
+ }
+ }
+ }
+
+ @Override
+ public void select(RoutingContext context) {
+ context.addChildren(context.getMatchedRecipients());
+ context.setSelectOnRetry(false);
+ if (minOk > 0) {
+ context.addConsumableError(ErrorCode.SERVICE_OOS);
+ }
+ }
+
+ @Override
+ public void merge(RoutingContext context) {
+ if (minOk > 0) {
+ Set<Integer> oosReplies = new HashSet<Integer>();
+ int idx = 0;
+ for (RoutingNodeIterator it = context.getChildIterator();
+ it.isValid(); it.next())
+ {
+ Reply ref = it.getReplyRef();
+ if (ref.hasErrors() && DocumentProtocol.hasOnlyErrorsOfType(ref, ErrorCode.SERVICE_OOS)) {
+ oosReplies.add(idx);
+ }
+ ++idx;
+ }
+ if (context.getNumChildren() - oosReplies.size() >= minOk) {
+ DocumentProtocol.merge(context, oosReplies);
+ return;
+ }
+ }
+ DocumentProtocol.merge(context);
+ }
+
+ @Override
+ public void destroy() {
+ // empty
+ }
+
+ @Override
+ public MetricSet getMetrics() {
+ return null;
+ }
+}
diff --git a/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/StatBucketMessage.java b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/StatBucketMessage.java new file mode 100755 index 00000000000..3854637ba5f --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/StatBucketMessage.java @@ -0,0 +1,60 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi.messagebus.protocol;
+
+import com.yahoo.document.BucketId;
+
+public class StatBucketMessage extends DocumentMessage {
+
+ private BucketId bucketId;
+ private String documentSelection;
+
+ StatBucketMessage() {
+ // need to deserialize into
+ }
+
+ public StatBucketMessage(BucketId bucket, String documentSelection) {
+ this.bucketId = bucket;
+ this.documentSelection = documentSelection;
+ }
+
+ public BucketId getBucketId() {
+ return bucketId;
+ }
+
+ void setBucketId(BucketId bucket) {
+ bucketId = bucket;
+ }
+
+ public String getDocumentSelection() {
+ return documentSelection;
+ }
+
+ void setDocumentSelection(String documentSelection) {
+ this.documentSelection = documentSelection;
+ }
+
+ @Override
+ public DocumentReply createReply() {
+ return new StatBucketReply();
+ }
+
+ @Override
+ public int getApproxSize() {
+ return super.getApproxSize() + 8 + documentSelection.length();
+ }
+
+ @Override
+ public boolean hasSequenceId() {
+ return true;
+ }
+
+ @Override
+ public long getSequenceId() {
+ return bucketId.getRawId();
+ }
+
+ @Override
+ public int getType() {
+ return DocumentProtocol.MESSAGE_STATBUCKET;
+ }
+}
diff --git a/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/StatBucketReply.java b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/StatBucketReply.java new file mode 100755 index 00000000000..43c369106d1 --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/StatBucketReply.java @@ -0,0 +1,19 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi.messagebus.protocol;
+
+public class StatBucketReply extends DocumentReply {
+
+ private String results = "";
+
+ public StatBucketReply() {
+ super(DocumentProtocol.REPLY_STATBUCKET);
+ }
+
+ public String getResults() {
+ return results;
+ }
+
+ public void setResults(String result) {
+ results = result;
+ }
+}
diff --git a/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/StoragePolicy.java b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/StoragePolicy.java new file mode 100644 index 00000000000..ae554b8b0c3 --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/StoragePolicy.java @@ -0,0 +1,469 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi.messagebus.protocol; + +import com.yahoo.config.subscription.ConfigSourceSet; +import com.yahoo.document.BucketId; +import com.yahoo.document.BucketIdFactory; +import com.yahoo.jrt.slobrok.api.IMirror; +import com.yahoo.jrt.slobrok.api.Mirror; +import com.yahoo.log.LogLevel; +import com.yahoo.messagebus.*; +import com.yahoo.messagebus.Error; +import com.yahoo.messagebus.metrics.MetricSet; +import com.yahoo.messagebus.routing.*; +import com.yahoo.vdslib.distribution.Distribution; +import com.yahoo.vdslib.state.ClusterState; +import com.yahoo.vdslib.state.Node; +import com.yahoo.vdslib.state.NodeType; +import com.yahoo.vdslib.state.State; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.logging.Logger; + +/** + * Routing policy to determine which distributor in a storage cluster to send data to. + * Using different key=value parameters separated by semicolon (";"), the user can control which cluster to send to. + * + * cluster=[clusterName] (Mandatory, determines the cluster name) + * config=[config] (Optional, a comma separated list of config servers to use. Used to talk to clusters not defined in this vespa application) + * slobrokconfigid=[id] (Optional, use given config id for slobrok instead of default) + * clusterconfigid=[id] (Optional, use given config id for distribution instead of default) + * + * @author <a href="mailto:humbe@yahoo-inc.com">Haakon Humberset</a> + */ +public class StoragePolicy extends ExternalSlobrokPolicy { + + private static final Logger log = Logger.getLogger(StoragePolicy.class.getName()); + public static final String owningBucketStates = "uim"; + public static final String upStates = "ui"; + + /** This class merely generates slobrok a host pattern for a given distributor. */ + public static class SlobrokHostPatternGenerator { + private final String clusterName; + public SlobrokHostPatternGenerator(String clusterName) { this.clusterName = clusterName; } + + /** + * Find host pattern of the hosts that are valid targets for this request. + * @param distributor Set to -1 if any distributor is valid target. + */ + public String getDistributorHostPattern(Integer distributor) { + return "storage/cluster." + clusterName + "/distributor/" + (distributor == null ? "*" : distributor) + "/default"; + } + } + + /** Helper class to match a host pattern with node to use. */ + public abstract static class HostFetcher { + private int requiredUpPercentageToSendToKnownGoodNodes = 60; + private List<Integer> validRandomTargets = new ArrayList<>(); + private int totalTargets = 1; + protected final Random randomizer = new Random(12345); // Use same randomizer each time to make unit testing easy. + + public void setRequiredUpPercentageToSendToKnownGoodNodes(int percent) { this.requiredUpPercentageToSendToKnownGoodNodes = percent; } + + public void updateValidTargets(ClusterState state) { + List<Integer> validRandomTargets = new ArrayList<>(); + for (int i=0; i<state.getNodeCount(NodeType.DISTRIBUTOR); ++i) { + if (state.getNodeState(new Node(NodeType.DISTRIBUTOR, i)).getState().oneOf(upStates)) validRandomTargets.add(i); + } + this.validRandomTargets = validRandomTargets; + this.totalTargets = state.getNodeCount(NodeType.DISTRIBUTOR); + } + public abstract String getTargetSpec(Integer distributor, RoutingContext context); + public String getRandomTargetSpec(RoutingContext context) { + // Try to use list of random targets, if at least X % of the nodes are up + while (100 * validRandomTargets.size() / totalTargets >= requiredUpPercentageToSendToKnownGoodNodes) { + int randIndex = randomizer.nextInt(validRandomTargets.size()); + String targetSpec = getTargetSpec(validRandomTargets.get(randIndex), context); + if (targetSpec != null) { + context.trace(3, "Sending to random node seen up in cluster state"); + return targetSpec; + } + validRandomTargets.remove(randIndex); + } + context.trace(3, "Too few nodes seen up in state. Sending totally random."); + return getTargetSpec(null, context); + } + public void close() {} + } + + /** Host fetcher using a slobrok mirror to find the hosts. */ + public static class SlobrokHostFetcher extends HostFetcher { + private final SlobrokHostPatternGenerator patternGenerator; + ExternalSlobrokPolicy policy; + + public SlobrokHostFetcher(SlobrokHostPatternGenerator patternGenerator, ExternalSlobrokPolicy policy) { + this.patternGenerator = patternGenerator; + this.policy = policy; + } + + private Mirror.Entry[] getEntries(String hostPattern, RoutingContext context) { + return policy.lookup(context, hostPattern); + } + + private String convertSlobrokNameToSessionName(String slobrokName) { return slobrokName + "/default"; } + + public IMirror getMirror(RoutingContext context) { return context.getMirror(); } + + public String getTargetSpec(Integer distributor, RoutingContext context) { + Mirror.Entry[] arr = getEntries(patternGenerator.getDistributorHostPattern(distributor), context); + if (arr.length == 0) return null; + if (distributor != null) { + if (arr.length == 1) { + return convertSlobrokNameToSessionName(arr[0].getSpec()); + } else { + log.log(LogLevel.WARNING, "Got " + arr.length + " matches for a distributor."); + } + } else { + return convertSlobrokNameToSessionName(arr[randomizer.nextInt(arr.length)].getSpec()); + } + return null; + } + } + + /** Class parsing the semicolon separated parameter string and exposes the appropriate value to the policy. */ + public static class Parameters { + protected String clusterName = null; + protected String distributionConfigId = null; + protected SlobrokHostPatternGenerator slobrokHostPatternGenerator = null; + + public Parameters(Map<String, String> params) { + clusterName = params.get("cluster"); + distributionConfigId = params.get("clusterconfigid"); + slobrokHostPatternGenerator = createPatternGenerator(); + if (clusterName == null) throw new IllegalArgumentException("Required parameter cluster with clustername not set"); + } + + public String getDistributionConfigId() { + return (distributionConfigId == null ? "storage/cluster." + clusterName : distributionConfigId); + } + public String getClusterName() { + return clusterName; + } + public SlobrokHostPatternGenerator createPatternGenerator() { + return new SlobrokHostPatternGenerator(getClusterName()); + } + public HostFetcher createHostFetcher(ExternalSlobrokPolicy policy) { + return new SlobrokHostFetcher(slobrokHostPatternGenerator, policy); + } + public Distribution createDistribution(ExternalSlobrokPolicy policy) { + return (policy.configSources != null ? + new Distribution(getDistributionConfigId(), new ConfigSourceSet(policy.configSources)) + : new Distribution(getDistributionConfigId())); + } + + /** + * When we have gotten this amount of failures from a node (Any kind of failures). We try to send to a random other node, just to see if the + * failure was related to node being bad. (Hard to detect from failure) + */ + public int getAttemptRandomOnFailuresLimit() { return 5; } + + /** + * If we receive more than this number of wrong distribution replies with old cluster states, we throw the current cached state and takes the + * old one. This guards us against version resets. + */ + public int maxOldClusterStatesSeenBeforeThrowingCachedState() { return 20; } + + /** + * When getting new cluster states we update good nodes. If we have more than this percentage of up nodes, we send to up nodes instead of totally random. + * (To avoid hitting trashing bad nodes still in slobrok) + */ + public int getRequiredUpPercentageToSendToKnownGoodNodes() { return 60; } + } + + /** Helper class to get the bucket identifier of a message. */ + public static class BucketIdCalculator { + private static final BucketIdFactory factory = new BucketIdFactory(); + + @SuppressWarnings("deprecation") + private BucketId getBucketId(Message msg) { + switch (msg.getType()) { + case DocumentProtocol.MESSAGE_PUTDOCUMENT: return factory.getBucketId(((PutDocumentMessage)msg).getDocumentPut().getDocument().getId()); + case DocumentProtocol.MESSAGE_GETDOCUMENT: return factory.getBucketId(((GetDocumentMessage)msg).getDocumentId()); + case DocumentProtocol.MESSAGE_REMOVEDOCUMENT: return factory.getBucketId(((RemoveDocumentMessage)msg).getDocumentId()); + case DocumentProtocol.MESSAGE_UPDATEDOCUMENT: return factory.getBucketId(((UpdateDocumentMessage)msg).getDocumentUpdate().getId()); + case DocumentProtocol.MESSAGE_GETBUCKETLIST: return ((GetBucketListMessage)msg).getBucketId(); + case DocumentProtocol.MESSAGE_STATBUCKET: return ((StatBucketMessage)msg).getBucketId(); + case DocumentProtocol.MESSAGE_CREATEVISITOR: return ((CreateVisitorMessage)msg).getBuckets().get(0); + case DocumentProtocol.MESSAGE_REMOVELOCATION: return ((RemoveLocationMessage)msg).getBucketId(); + case DocumentProtocol.MESSAGE_BATCHDOCUMENTUPDATE: return ((BatchDocumentUpdateMessage)msg).getBucketId(); + default: + log.log(LogLevel.ERROR, "Message type '" + msg.getType() + "' not supported."); + return null; + } + } + + public BucketId handleBucketIdCalculation(RoutingContext context) { + BucketId id = getBucketId(context.getMessage()); + if (id == null || id.getRawId() == 0) { + Reply reply = new EmptyReply(); + reply.addError(new Error(ErrorCode.APP_FATAL_ERROR, "No bucket id available in message.")); + context.setReply(reply); + } + return id; + } + } + + /** Class handling the logic of picking a distributor */ + public static class DistributorSelectionLogic { + /** Class that tracks a failure of a given type per node. */ + public static class InstabilityChecker { + private List<Integer> nodeFailures = new ArrayList<>(); + private int failureLimit; + + public InstabilityChecker(int failureLimit) { this.failureLimit = failureLimit; } + + public boolean tooManyFailures(int nodeIndex) { + if (nodeFailures.size() > nodeIndex && nodeFailures.get(nodeIndex) > failureLimit) { + nodeFailures.set(nodeIndex, 0); + return true; + } else { + return false; + } + } + + public void addFailure(Integer calculatedDistributor) { + while (nodeFailures.size() <= calculatedDistributor) nodeFailures.add(0); + nodeFailures.set(calculatedDistributor, nodeFailures.get(calculatedDistributor) + 1); + } + } + /** Message context class. Contains data we want to inspect about a request at reply time. */ + private static class MessageContext { + Integer calculatedDistributor; + ClusterState usedState; + + public MessageContext(ClusterState usedState) { this.usedState = usedState; } + + public String toString() { + return "Context(Distributor " + calculatedDistributor + + ", state version " + usedState.getVersion() + ")"; + } + } + + private final HostFetcher hostFetcher; + private final Distribution distribution; + private final InstabilityChecker persistentFailureChecker; + private ClusterState cachedClusterState = null; + private int oldClusterVersionGottenCount = 0; + private final int maxOldClusterVersionBeforeSendingRandom; // Reset cluster version protection + + public DistributorSelectionLogic(Parameters params, ExternalSlobrokPolicy policy) { + this.hostFetcher = params.createHostFetcher(policy); + this.hostFetcher.setRequiredUpPercentageToSendToKnownGoodNodes(params.getRequiredUpPercentageToSendToKnownGoodNodes()); + this.distribution = params.createDistribution(policy); + persistentFailureChecker = new InstabilityChecker(params.getAttemptRandomOnFailuresLimit()); + maxOldClusterVersionBeforeSendingRandom = params.maxOldClusterStatesSeenBeforeThrowingCachedState(); + } + + public void destroy() { + hostFetcher.close(); + distribution.close(); + } + + public String getTargetSpec(RoutingContext context, BucketId bucketId) { + String sendRandomReason = null; + MessageContext messageContext = new MessageContext(cachedClusterState); + context.setContext(messageContext); + if (cachedClusterState != null) { // If we have a cached cluster state (regular case), we use that to calculate correct node. + try{ + Integer target = distribution.getIdealDistributorNode(cachedClusterState, bucketId, owningBucketStates); + // If we have had too many failures towards existing node, reset failure count and send to random + if (persistentFailureChecker.tooManyFailures(target)) { + sendRandomReason = "Too many failures detected versus distributor " + target + ". Sending to random instead of using cached state."; + target = null; + } + // If we have found a target, and the target exist in slobrok, send to it. + if (target != null) { + messageContext.calculatedDistributor = target; + String targetSpec = hostFetcher.getTargetSpec(target, context); + if (targetSpec != null) { + if (context.shouldTrace(1)) { + context.trace(1, "Using distributor " + messageContext.calculatedDistributor + " for " + + bucketId + " as our state version is " + cachedClusterState.getVersion()); + } + messageContext.usedState = cachedClusterState; + return targetSpec; + } else { + sendRandomReason = "Want to use distributor " + messageContext.calculatedDistributor + " but it is not in slobrok. Sending to random."; + log.log(LogLevel.DEBUG, "Target distributor is not in slobrok"); + } + } + } catch (Distribution.TooFewBucketBitsInUseException e) { + Reply reply = new WrongDistributionReply(cachedClusterState.toString(true)); + reply.addError(new Error(DocumentProtocol.ERROR_WRONG_DISTRIBUTION, + "Too few distribution bits used for given cluster state")); + context.setReply(reply); + return null; + } catch (Distribution.NoDistributorsAvailableException e) { + log.log(LogLevel.DEBUG, "No distributors available; clearing cluster state"); + cachedClusterState = null; + sendRandomReason = "No distributors available. Sending to random distributor."; + } + } else { + sendRandomReason = "No cluster state cached. Sending to random distributor."; + } + if (context.shouldTrace(1)) { + context.trace(1, sendRandomReason != null ? sendRandomReason : "Sending to random distributor for unknown reason"); + } + return hostFetcher.getRandomTargetSpec(context); + } + + public void handleWrongDistribution(WrongDistributionReply reply, RoutingContext routingContext) { + MessageContext context = (MessageContext) routingContext.getContext(); + ClusterState newState; + try { + newState = new ClusterState(reply.getSystemState()); + } catch (Exception e) { + reply.getTrace().trace(1, "Error when parsing system state string " + reply.getSystemState()); + return; + } + if (cachedClusterState != null && cachedClusterState.getVersion() > newState.getVersion()) { + if (++oldClusterVersionGottenCount >= maxOldClusterVersionBeforeSendingRandom) { + oldClusterVersionGottenCount = 0; + cachedClusterState = null; + } + } + if (context.usedState != null && newState.getVersion() <= context.usedState.getVersion()) { + reply.setRetryDelay(-1); + } else { + reply.setRetryDelay(0); + } + if (context.calculatedDistributor == null) { + if (cachedClusterState == null) { + if (reply.getTrace().shouldTrace(1)) { + reply.getTrace().trace(1, "Message sent to * with no previous state, received version " + newState.getVersion()); + } + } else if (newState.getVersion() == cachedClusterState.getVersion()) { + if (reply.getTrace().shouldTrace(1)) { + reply.getTrace().trace(1, "Message sent to * found that cluster state version " + newState.getVersion() + " was correct."); + } + } else if (newState.getVersion() > cachedClusterState.getVersion()) { + if (reply.getTrace().shouldTrace(1)) { + reply.getTrace().trace(1, "Message sent to * updated cluster state to version " + newState.getVersion()); + } + } else { + if (reply.getTrace().shouldTrace(1)) { + reply.getTrace().trace(1, "Message sent to * retrieved older cluster state version " + newState.getVersion()); + } + } + } else { + if (context.usedState == null) { + String msg = "Used state must be set as distributor is calculated. Bug."; + reply.getTrace().trace(1, msg); + log.log(LogLevel.ERROR, msg); + } else if (newState.getVersion() == context.usedState.getVersion()) { + String msg = "Message sent to distributor " + context.calculatedDistributor + + " retrieved cluster state version " + newState.getVersion() + + " which was the state we used to calculate distributor as target last time."; + reply.getTrace().trace(1, msg); + log.log(LogLevel.WARNING, msg); + } else if (newState.getVersion() > context.usedState.getVersion()) { + if (reply.getTrace().shouldTrace(1)) { + reply.getTrace().trace(1, "Message sent to distributor " + context.calculatedDistributor + + " updated cluster state from version " + context.usedState.getVersion() + + " to " + newState.getVersion()); + } + } else { + if (reply.getTrace().shouldTrace(1)) { + reply.getTrace().trace(1, "Message sent to distributor " + context.calculatedDistributor + + " returned older cluster state version " + newState.getVersion()); + } + } + } + if (cachedClusterState == null || newState.getVersion() >= cachedClusterState.getVersion()) { + cachedClusterState = newState; + if (newState.getClusterState().equals(State.UP)) { + hostFetcher.updateValidTargets(newState); + } + } else if (newState.getVersion() + 2000000000 < cachedClusterState.getVersion()) { + cachedClusterState = null; + } else if (context.calculatedDistributor != null) { + persistentFailureChecker.addFailure(context.calculatedDistributor); + } + } + public void handleErrorReply(Reply reply, Object untypedContext) { + MessageContext messageContext = (MessageContext) untypedContext; + if (messageContext.calculatedDistributor != null) { + persistentFailureChecker.addFailure(messageContext.calculatedDistributor); + if (reply.getTrace().shouldTrace(1)) { + reply.getTrace().trace(1, "Failed with " + messageContext.toString()); + } + } + } + } + + private final BucketIdCalculator bucketIdCalculator = new BucketIdCalculator(); + private DistributorSelectionLogic distributorSelectionLogic = null; + private Parameters parameters; + + /** Constructor used in production. */ + public StoragePolicy(String param) { + this(parse(param)); + } + + public StoragePolicy(Map<String, String> params) { + this(new Parameters(params), params); + } + + /** Constructor specifying a bit more in detail, so we can override what needs to be overridden in tests */ + public StoragePolicy(Parameters p, Map<String, String> params) { + super(params); + parameters = p; + } + + @Override + public void init() { + super.init(); + this.distributorSelectionLogic = new DistributorSelectionLogic(parameters, this); + } + + @Override + public void doSelect(RoutingContext context) { + if (context.shouldTrace(1)) { + context.trace(1, "Selecting route"); + } + + BucketId bucketId = bucketIdCalculator.handleBucketIdCalculation(context); + if (context.hasReply()) return; + + String targetSpec = distributorSelectionLogic.getTargetSpec(context, bucketId); + if (context.hasReply()) return; + if (targetSpec != null) { + Route route = new Route(context.getRoute()); + route.setHop(0, new Hop().addDirective(new VerbatimDirective(targetSpec))); + context.addChild(route); + } else { + context.setError(ErrorCode.NO_ADDRESS_FOR_SERVICE, + "Could not resolve any distributors to send to in cluster " + parameters.clusterName); + } + } + + @Override + public void merge(RoutingContext context) { + RoutingNodeIterator it = context.getChildIterator(); + Reply reply = it.removeReply(); + + if (reply instanceof WrongDistributionReply) { + distributorSelectionLogic.handleWrongDistribution((WrongDistributionReply) reply, context); + } else if (reply.hasErrors()) { + distributorSelectionLogic.handleErrorReply(reply, context.getContext()); + } else if (reply instanceof WriteDocumentReply) { + if (context.shouldTrace(9)) { + context.trace(9, "Modification timestamp: " + ((WriteDocumentReply)reply).getHighestModificationTimestamp()); + } + } + context.setReply(reply); + } + + @Override + public void destroy() { + distributorSelectionLogic.destroy(); + } + + @Override + public MetricSet getMetrics() { + return null; + } +} diff --git a/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/SubsetServicePolicy.java b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/SubsetServicePolicy.java new file mode 100755 index 00000000000..dc06fe7042d --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/SubsetServicePolicy.java @@ -0,0 +1,145 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi.messagebus.protocol;
+
+import com.yahoo.jrt.slobrok.api.Mirror;
+import com.yahoo.log.LogLevel;
+import com.yahoo.messagebus.metrics.MetricSet;
+import com.yahoo.messagebus.routing.*;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.logging.Logger;
+
+/**
+ * This policy implements the logic to select a subset of services that matches a slobrok pattern.
+ *
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class SubsetServicePolicy implements DocumentProtocolRoutingPolicy {
+
+ private static Logger log = Logger.getLogger(SubsetServicePolicy.class.getName());
+ private final int subsetSize;
+ private final Map<String, CacheEntry> cache = new HashMap<String, CacheEntry>();
+
+ /**
+ * Creates an instance of a subset service policy. The parameter string is parsed as an integer number that is the
+ * number of services to include in the set to choose from.
+ *
+ * @param param The number of services to include in the set.
+ */
+ public SubsetServicePolicy(String param) {
+ int subsetSize = 5;
+ if (param != null && param.length() > 0) {
+ try {
+ subsetSize = Integer.parseInt(param);
+ }
+ catch (NumberFormatException e) {
+ log.log(LogLevel.WARNING, "Parameter '" + param + "' could not be parsed as an integer.", e);
+ }
+ if (subsetSize <= 0) {
+ log.warning("Ignoring a request to set the subset size to " + subsetSize + " because it makes no " +
+ "sense. This routing policy will choose any one matching service.");
+ }
+ } else {
+ log.warning("No parameter given to SubsetService policy, using default value " + subsetSize + ".");
+ }
+ this.subsetSize = subsetSize;
+ }
+
+ // Inherit doc from RoutingPolicy.
+ public void select(RoutingContext ctx) {
+ Route route = new Route(ctx.getRoute());
+ route.setHop(0, getRecipient(ctx));
+ ctx.addChild(route);
+ }
+
+ // Inherit doc from RoutingPolicy.
+ public void merge(RoutingContext ctx) {
+ DocumentProtocol.merge(ctx);
+ }
+
+ /**
+ * Returns the appropriate recipient hop for the given routing context. This method provides synchronized access to
+ * the internal cache.
+ *
+ * @param ctx The routing context.
+ * @return The recipient hop to use.
+ */
+ private Hop getRecipient(RoutingContext ctx) {
+ Hop hop = null;
+ if (subsetSize > 0) {
+ synchronized (this) {
+ CacheEntry entry = update(ctx);
+ if (!entry.recipients.isEmpty()) {
+ if (++entry.offset >= entry.recipients.size()) {
+ entry.offset = 0;
+ }
+ hop = new Hop(entry.recipients.get(entry.offset));
+ }
+ }
+ }
+ if (hop == null) {
+ hop = new Hop(ctx.getRoute().getHop(0));
+ hop.setDirective(ctx.getDirectiveIndex(), new VerbatimDirective("*"));
+ }
+ return hop;
+ }
+
+ /**
+ * Updates and returns the cache entry for the given routing context. This method assumes that synchronization is
+ * handled outside of it.
+ *
+ * @param ctx The routing context.
+ * @return The updated cache entry.
+ */
+ private CacheEntry update(RoutingContext ctx) {
+ String key = getCacheKey(ctx);
+ CacheEntry entry = cache.get(key);
+ if (entry == null) {
+ entry = new CacheEntry();
+ cache.put(key, entry);
+ }
+
+ int upd = ctx.getMirror().updates();
+ if (entry.generation != upd) {
+ entry.generation = upd;
+ entry.recipients.clear();
+
+ Mirror.Entry[] arr = ctx.getMirror().lookup(ctx.getHopPrefix() + "*" + ctx.getHopSuffix());
+ int pos = ctx.getMessageBus().getConnectionSpec().hashCode();
+ for (int i = 0; i < subsetSize && i < arr.length; ++i) {
+ entry.recipients.add(Hop.parse(arr[((pos + i) & Integer.MAX_VALUE) % arr.length].getName()));
+ }
+ }
+ return entry;
+ }
+
+ /**
+ * Returns a cache key for this instance of the policy. Because behaviour is based on the hop in which the policy
+ * occurs, the cache key is the hop string itself.
+ *
+ * @param ctx The routing context.
+ * @return The cache key.
+ */
+ private String getCacheKey(RoutingContext ctx) {
+ return ctx.getRoute().getHop(0).toString();
+ }
+
+ /**
+ * Defines the necessary cache data.
+ */
+ private class CacheEntry {
+ private final List<Hop> recipients = new ArrayList<Hop>();
+ private int generation = 0;
+ private int offset = 0;
+ }
+
+ public void destroy() {
+ }
+
+ public MetricSet getMetrics() {
+ return null;
+ }
+}
diff --git a/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/TestAndSetMessage.java b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/TestAndSetMessage.java new file mode 100644 index 00000000000..1ffd956f0ba --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/TestAndSetMessage.java @@ -0,0 +1,16 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi.messagebus.protocol; + +import com.google.common.annotations.Beta; +import com.yahoo.document.TestAndSetCondition; + +/** + * This class represents messages having an optional "test and set" condition + * + * @author Vegard Sjonfjell + */ +@Beta +public abstract class TestAndSetMessage extends DocumentMessage { + public abstract void setCondition(TestAndSetCondition condition); + public abstract TestAndSetCondition getCondition(); +} diff --git a/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/UpdateDocumentMessage.java b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/UpdateDocumentMessage.java new file mode 100755 index 00000000000..8a5a733d026 --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/UpdateDocumentMessage.java @@ -0,0 +1,175 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi.messagebus.protocol; + +import com.yahoo.document.DocumentUpdate; +import com.yahoo.document.TestAndSetCondition; +import com.yahoo.document.serialization.DocumentDeserializer; + +import java.nio.ByteBuffer; +import java.util.Arrays; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class UpdateDocumentMessage extends TestAndSetMessage { + + private DocumentDeserializer buffer = null; + private DocumentUpdate update = null; + private long oldTime = 0; + private long newTime = 0; + private LazyDecoder decoder = null; + + /** + * Constructs a new message for deserialization. + */ + UpdateDocumentMessage() { + // empty + } + + /** + * Constructs a new message from a byte buffer. + * @param decoder The decoder to use for deserialization. + * @param buffer A byte buffer that contains a serialized message. + */ + public UpdateDocumentMessage(LazyDecoder decoder, DocumentDeserializer buffer) { + this.decoder = decoder; + this.buffer = buffer; + } + + /** + * Constructs a new document update message. + * + * @param upd The document update to perform. + */ + public UpdateDocumentMessage(DocumentUpdate upd) { + super(); + update = upd; + } + + /** + * Creates an empty UpdateDocumentMessage + */ + public static UpdateDocumentMessage createEmpty() { + return new UpdateDocumentMessage(null); + } + + /** + * This method will make sure that any serialized content is deserialized into proper message content on first + * entry. Any subsequent entry into this function will do nothing. + */ + private void deserialize() { + if (decoder != null && buffer != null) { + decoder.decode(this, buffer); + decoder = null; + buffer = null; + } + } + + /** + * Returns the document update to perform. + * + * @return The update. + */ + public DocumentUpdate getDocumentUpdate() { + deserialize(); + return update; + } + + /** + * Sets the document update to perform. + * + * @param upd The document update to set. + */ + public void setDocumentUpdate(DocumentUpdate upd) { + if (upd == null) { + throw new IllegalArgumentException("Document update can not be null."); + } + buffer = null; + decoder = null; + update = upd; + } + + /** + * Returns the timestamp required for this update to be applied. + * + * @return The document timestamp. + */ + public long getOldTimestamp() { + deserialize(); + return oldTime; + } + + /** + * Sets the timestamp required for this update to be applied. + * + * @param time The timestamp to set. + */ + public void setOldTimestamp(long time) { + buffer = null; + decoder = null; + oldTime = time; + } + + /** + * Returns the timestamp to assign to the updated document. + * + * @return The document timestamp. + */ + public long getNewTimestamp() { + deserialize(); + return newTime; + } + + /** + * Sets the timestamp to assign to the updated document. + * + * @param time The timestamp to set. + */ + public void setNewTimestamp(long time) { + buffer = null; + decoder = null; + newTime = time; + } + + /** + * Returns the raw serialized buffer. This buffer is stored as the message is received from accross the network, and + * deserialized from as soon as a member is requested. This method will return null if the buffer has been decoded. + * + * @return The buffer containing the serialized data for this message, or null. + */ + ByteBuffer getSerializedBuffer() { + return buffer != null ? buffer.getBuf().getByteBuffer() : null; + } + + @Override + public DocumentReply createReply() { + return new UpdateDocumentReply(); + } + + @Override + public boolean hasSequenceId() { + return true; + } + + @Override + public long getSequenceId() { + deserialize(); + return Arrays.hashCode(update.getId().getGlobalId()); + } + + @Override + public int getType() { + return DocumentProtocol.MESSAGE_UPDATEDOCUMENT; + } + + @Override + public TestAndSetCondition getCondition() { + deserialize(); + return update.getCondition(); + } + + @Override + public void setCondition(TestAndSetCondition condition) { + this.update.setCondition(condition); + } +} diff --git a/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/UpdateDocumentReply.java b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/UpdateDocumentReply.java new file mode 100755 index 00000000000..5f091101554 --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/UpdateDocumentReply.java @@ -0,0 +1,35 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi.messagebus.protocol;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class UpdateDocumentReply extends WriteDocumentReply {
+
+ private boolean found = true;
+
+ /**
+ * Constructs a new reply with no content.
+ */
+ public UpdateDocumentReply() {
+ super(DocumentProtocol.REPLY_UPDATEDOCUMENT);
+ }
+
+ /**
+ * Returns whether or not the document was found and updated.
+ *
+ * @return true if document was found
+ */
+ public boolean wasFound() {
+ return found;
+ }
+
+ /**
+ * Sets whether or not the document was found and updated.
+ *
+ * @param found True if the document was found
+ */
+ public void setWasFound(boolean found) {
+ this.found = found;
+ }
+}
\ No newline at end of file diff --git a/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/VisitorInfoMessage.java b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/VisitorInfoMessage.java new file mode 100644 index 00000000000..10e56bb5ef8 --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/VisitorInfoMessage.java @@ -0,0 +1,63 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi.messagebus.protocol; + +import com.yahoo.document.BucketId; + +import java.util.Iterator; +import java.util.Set; +import java.util.TreeSet; + +public class VisitorInfoMessage extends VisitorMessage { + + private Set<BucketId> finishedBuckets = new TreeSet<BucketId>(); + private String errorMessage = ""; + + public String getErrorMessage() { + return errorMessage; + } + + public void setErrorMessage(String s) { + errorMessage = s; + } + + public Set<BucketId> getFinishedBuckets() { + return finishedBuckets; + } + + public void setFinishedBuckets(Set<BucketId> finishedBuckets) { + this.finishedBuckets = finishedBuckets; + } + + @Override + public DocumentReply createReply() { + return new VisitorReply(DocumentProtocol.REPLY_VISITORINFO); + } + + @Override + public int getType() { + return DocumentProtocol.MESSAGE_VISITORINFO; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder().append("VisitorInfoMessage("); + if (finishedBuckets.size() == 0) { + sb.append("No buckets"); + } else if (finishedBuckets.size() == 1) { + sb.append("Bucket ").append(finishedBuckets.iterator().next().toString()); + } else if (finishedBuckets.size() < 65536) { + sb.append(finishedBuckets.size()).append(" buckets:"); + Iterator<BucketId> it = finishedBuckets.iterator(); + for (int i = 0; it.hasNext() && i < 3; ++i) { + sb.append(' ').append(it.next().toString()); + } + if (it.hasNext()) { + sb.append(" ..."); + } + } else { + sb.append("All buckets"); + } + sb.append(", error message '").append(errorMessage).append('\''); + return sb.append(')').toString(); + } +} diff --git a/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/VisitorMessage.java b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/VisitorMessage.java new file mode 100644 index 00000000000..e775ea756a7 --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/VisitorMessage.java @@ -0,0 +1,6 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi.messagebus.protocol; + +public abstract class VisitorMessage extends DocumentMessage { + // empty +} diff --git a/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/VisitorReply.java b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/VisitorReply.java new file mode 100644 index 00000000000..bc62bb578b7 --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/VisitorReply.java @@ -0,0 +1,9 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi.messagebus.protocol; + +public class VisitorReply extends WriteDocumentReply { + + public VisitorReply(int type) { + super(type); + } +} diff --git a/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/WriteDocumentReply.java b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/WriteDocumentReply.java new file mode 100755 index 00000000000..0db02c10b31 --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/WriteDocumentReply.java @@ -0,0 +1,34 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi.messagebus.protocol; + +/** + * This reply class is used by operations that perform writes to VDS/search, + * that is: Put, Remove, Update. + */ +public class WriteDocumentReply extends DocumentAcceptedReply { + + private long highestModificationTimestamp = 0; + + public WriteDocumentReply(int type) { + super(type); + } + + /** + * Returns a unique VDS timestamp so that visiting up to and including that timestamp + * will return a state including this operation but not any operations sent to the same distributor + * after it. For PUT/UPDATE/REMOVE operations this timestamp will be the timestamp of the operation. + * + * @return Returns the modification timestamp. + */ + public long getHighestModificationTimestamp() { + return highestModificationTimestamp; + } + + /** + * Sets the modification timestamp. + */ + public void setHighestModificationTimestamp(long timestamp) { + this.highestModificationTimestamp = timestamp; + } + +} diff --git a/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/WrongDistributionReply.java b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/WrongDistributionReply.java new file mode 100644 index 00000000000..75e2525289d --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/WrongDistributionReply.java @@ -0,0 +1,27 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi.messagebus.protocol; + +/** + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + */ +public class WrongDistributionReply extends DocumentReply { + + private String systemState; + + public WrongDistributionReply() { + super(DocumentProtocol.REPLY_WRONGDISTRIBUTION); + } + + public WrongDistributionReply(String systemState) { + super(DocumentProtocol.REPLY_WRONGDISTRIBUTION); + this.systemState = systemState; + } + + public String getSystemState() { + return systemState; + } + + public void setSystemState(String state) { + systemState = state; + } +} diff --git a/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/package-info.java b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/package-info.java new file mode 100644 index 00000000000..f3b73bfa8a4 --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/package-info.java @@ -0,0 +1,7 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +@ExportPackage +@PublicApi +package com.yahoo.documentapi.messagebus.protocol; + +import com.yahoo.api.annotations.PublicApi; +import com.yahoo.osgi.annotation.ExportPackage; diff --git a/documentapi/src/main/java/com/yahoo/documentapi/messagebus/systemstate/rule/Argument.java b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/systemstate/rule/Argument.java new file mode 100755 index 00000000000..3a434eab101 --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/systemstate/rule/Argument.java @@ -0,0 +1,40 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi.messagebus.systemstate.rule;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class Argument {
+
+ private final String name;
+ private final String value;
+
+ /**
+ * Constructs a new argument.
+ *
+ * @param name The name of this argument.
+ * @param value The value of this argument.
+ */
+ public Argument(String name, String value) {
+ this.name = name;
+ this.value = value;
+ }
+
+ /**
+ * Returns the name of this argument.
+ *
+ * @return The name.
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Returns the value of this argument.
+ *
+ * @return The value.
+ */
+ public String getValue() {
+ return value;
+ }
+}
diff --git a/documentapi/src/main/java/com/yahoo/documentapi/messagebus/systemstate/rule/Location.java b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/systemstate/rule/Location.java new file mode 100755 index 00000000000..870a39c0122 --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/systemstate/rule/Location.java @@ -0,0 +1,120 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi.messagebus.systemstate.rule;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Arrays;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class Location {
+
+ private List<String> items = new ArrayList<String>();
+
+ /**
+ * Constructs a new location with no items.
+ */
+ public Location() {
+ // empty
+ }
+
+ /**
+ * Constructs a new location based on a location string.
+ *
+ * @param loc The location string to parse.
+ */
+ public Location(String loc) {
+ items.addAll(Arrays.asList(loc.split("/")));
+ normalize();
+ }
+
+ /**
+ * Constructs a new location based on a list of items.
+ *
+ * @param items The components that make up this location.
+ */
+ public Location(List<String> items) {
+ this.items.addAll(items);
+ normalize();
+ }
+
+ /**
+ * Constructs a new location as a copy of another.
+ *
+ * @param loc The location to copy.
+ */
+ public Location(Location loc) {
+ items.addAll(loc.items);
+ }
+
+ /**
+ * Constructs a new location based on a working directory and a list of items.
+ *
+ * @param pwd The path of the working directory.
+ * @param items The components that make up this location.
+ */
+ public Location(Location pwd, List<String> items) {
+ this.items.addAll(pwd.getItems());
+ this.items.addAll(items);
+ normalize();
+ }
+
+ /**
+ * Returns a location object that represents the "next" step along this location path. This means removing the first
+ * elements of this location's items and returning a new location for this sublist.
+ *
+ * @return The next location along this path.
+ */
+ public Location getNext() {
+ List<String> next = new ArrayList<String>(items);
+ next.remove(0);
+ return new Location(next);
+ }
+
+ /**
+ * Returns the components of this location.
+ *
+ * @return The component array.
+ */
+ public List<String> getItems() {
+ return items;
+ }
+
+ /**
+ * Normalizes the items list of this location so that all PREV or THIS locations are replaced by their actual
+ * meaning. This carries some overhead since it is not done in place.
+ *
+ * @return This, to allow chaining.
+ */
+ private Location normalize() {
+ List<String> norm = new ArrayList<String>();
+ for (String item : items) {
+ if (item.equals(NodeState.NODE_PARENT)) {
+ if (norm.size() == 0) {
+ // ignore
+ }
+ else {
+ norm.remove(norm.size() - 1);
+ }
+ }
+ else if (!item.equals(NodeState.NODE_CURRENT)) {
+ norm.add(item);
+ }
+ }
+ items = norm;
+ return this;
+ }
+
+ @Override
+ public String toString() {
+ StringBuffer ret = new StringBuffer();
+ for (int i = 0; i < items.size(); ++i) {
+ ret.append(items.get(i));
+ if (i < items.size() - 1) {
+ ret.append("/");
+ }
+ }
+ return ret.toString();
+ }
+}
diff --git a/documentapi/src/main/java/com/yahoo/documentapi/messagebus/systemstate/rule/NodeState.java b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/systemstate/rule/NodeState.java new file mode 100755 index 00000000000..f5920f32119 --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/messagebus/systemstate/rule/NodeState.java @@ -0,0 +1,310 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi.messagebus.systemstate.rule;
+
+import com.yahoo.log.LogLevel;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.logging.Logger;
+
+/**
+ * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a>
+ */
+public class NodeState {
+
+ /** A location string that expresses the use of the PARENT node. */
+ public static final String NODE_PARENT = "..";
+
+ /** A location string that expresses the use of THIS node. */
+ public static final String NODE_CURRENT = ".";
+
+ private static Logger log = Logger.getLogger(NodeState.class.getName());
+ private final Map<String, NodeState> children = new LinkedHashMap<String, NodeState>();
+ private final Map<String, String> state = new LinkedHashMap<String, String>();
+ private NodeState parent = null;
+ private String id = null;
+
+ /**
+ * Creates a node state that no internal content.
+ */
+ public NodeState() {
+ // empty
+ }
+
+ /**
+ * Creates a node state based on a list of argument objects. These arguments are iterated and added to this node's
+ * internal state map.
+ *
+ * @param args The arguments to use as state.
+ */
+ public NodeState(List<Argument> args) {
+ for (Argument arg : args) {
+ setState(arg.getName(), arg.getValue());
+ }
+ }
+
+ /**
+ * Adds a child to this node at the given location. The key can be a location string, in which case the necessary
+ * intermediate node states are created.
+ *
+ * @param key The location at which to add the child.
+ * @param child The child node to add.
+ * @return This, to allow chaining.
+ */
+ public NodeState addChild(String key, NodeState child) {
+ getChild(key, true).copy(child);
+ return this;
+ }
+
+ /**
+ * Returns the child at the given location relative to this.
+ *
+ * @param key The location of the child to return.
+ * @return The child object, null if not found.
+ */
+ public NodeState getChild(String key) {
+ return getChild(key, false);
+ }
+
+ /**
+ * Returns the child at the given location relative to this. This method can be forced to return a child node even
+ * if it does not exist, by adding all intermediate nodes and the target node itself.
+ *
+ * @param key The location of the child to return.
+ * @param force Whether or not to force a return value by creating missing nodes.
+ * @return The child object, null if not found.
+ */
+ public NodeState getChild(String key, boolean force) {
+ if (key == null || key.length() == 0) {
+ return this;
+ }
+ String arr[] = key.split("/", 2);
+ while (arr.length == 2 && arr[0].equals(NODE_CURRENT)) {
+ arr = arr[1].split("/", 2);
+ }
+ if (arr[0].equals(NODE_CURRENT)) {
+ return this;
+ }
+ if (arr[0].equals(NODE_PARENT)) {
+ if (parent == null) {
+ log.log(LogLevel.ERROR, "Location string '" + key + "' requests a parent above the top-most node, " +
+ "returning self to avoid crash.");
+ }
+ return parent.getChild(arr[1], force);
+ }
+ if (!children.containsKey(arr[0])) {
+ if (!force) {
+ return null;
+ }
+ children.put(arr[0], new NodeState());
+ children.get(arr[0]).setParent(this, arr[0]);
+ }
+ if (arr.length == 2) {
+ return children.get(arr[0]).getChild(arr[1], force);
+ }
+ return children.get(arr[0]);
+ }
+
+ /**
+ * Returns the map of child nodes for iteration.
+ *
+ * @return The internal child map.
+ */
+ public Map<String, NodeState> getChildren() {
+ return children;
+ }
+
+ /**
+ * Removes the named child node from this node, and attempts to compact the system state from this node upwards by
+ * removing empty nodes.
+ *
+ * @param key The child to remove.
+ * @return The result of invoking {@link #compact} after the remove.
+ */
+ public NodeState removeChild(String key) {
+ if (key == null || key.length() == 0) {
+ return this;
+ }
+ int pos = key.lastIndexOf("/");
+ if (pos > -1) {
+ NodeState parent = getChild(key.substring(0, pos), false);
+ if (parent != null) {
+ return parent.removeChild(key.substring(pos + 1));
+ }
+ }
+ else {
+ children.remove(key);
+ }
+ return compact();
+ }
+
+ /**
+ * Retrieves some arbitrary state information for a given key. The key can be a location string, in which case the
+ * necessary intermediate nodes are traversed. If the key is not found, this method returns null.
+ *
+ * @param key The name of the state information to return.
+ * @return The value of the state key.
+ */
+ public String getState(String key) {
+ if (key == null || key.length() == 0) {
+ return null;
+ }
+ int pos = key.lastIndexOf("/");
+ if (pos > -1) {
+ NodeState parent = getChild(key.substring(0, pos), false);
+ return parent != null ? parent.getState(key.substring(pos + 1)) : null;
+ }
+ return state.get(key);
+ }
+
+ /**
+ * Sets some arbitrary state data in this node. The key can be a location string, in which case the necessary
+ * intermediate nodes are traversed and even created if missing.
+ *
+ * @param key The key to set.
+ * @param value The value to assign to the key.
+ * @return This, to allow chaining.
+ */
+ public NodeState setState(String key, String value) {
+ if (key == null || key.length() == 0) {
+ return this;
+ }
+ int pos = key.lastIndexOf("/");
+ if (pos > -1) {
+ getChild(key.substring(0, pos), true).setState(key.substring(pos + 1), value);
+ }
+ else {
+ if (value == null || value.length() == 0) {
+ return removeState(key);
+ }
+ else {
+ state.put(key, value);
+ }
+ }
+ return this;
+ }
+
+ /**
+ * Removes the named (key, value) state pair from this node, and attempts to compact the system state from this node
+ * upwards by removing empty nodes.
+ *
+ * @param key The state variable to clear.
+ * @return The result of invoking {@link #compact} after the remove.
+ */
+ public NodeState removeState(String key) {
+ if (key == null || key.length() == 0) {
+ return this;
+ }
+ int pos = key.lastIndexOf("/");
+ if (pos > -1) {
+ NodeState parent = getChild(key.substring(0, pos), false);
+ if (parent != null) {
+ return parent.removeState(key.substring(pos + 1));
+ }
+ }
+ else {
+ state.remove(key);
+ }
+ return compact();
+ }
+
+ /**
+ * Compacts the system state tree from this node upwards. This will delete itself if it has a parent, but no
+ * internal state and no children.
+ *
+ * @return This or the first non-null ancestor, to allow chaining.
+ */
+ private NodeState compact() {
+ if (state.isEmpty() && children.isEmpty()) {
+ if (parent != null) {
+ return parent.removeChild(id);
+ }
+ }
+ return this;
+ }
+
+ /**
+ * Copies the state content of another node state object into this.
+ *
+ * @param node The node state to copy into this.
+ * @return This, to allow chaining.
+ */
+ public NodeState copy(NodeState node) {
+ for (String key : node.state.keySet()) {
+ state.put(key, node.state.get(key));
+ }
+ for (String key : node.children.keySet()) {
+ getChild(key, true).copy(node.children.get(key));
+ }
+ return this;
+ }
+
+ /**
+ * Clears both the internal state and child list, then compacts the tree from this node upwards.
+ *
+ * @return The result of invoking {@link #compact} after the remove.
+ */
+ public NodeState clear() {
+ state.clear();
+ children.clear();
+ return compact();
+ }
+
+ /**
+ * Sets the parent of this node.
+ *
+ * @param parent The parent node.
+ * @param id The identifier of this node as seen in the parent.
+ * @return This, to allow chaining.
+ */
+ public NodeState setParent(NodeState parent, String id) {
+ this.parent = parent;
+ this.id = id;
+ return this;
+ }
+
+ /**
+ * Returns a string representation of this node state.
+ *
+ * @param prefix The prefix to use for this string.
+ * @return A string representation of this.
+ * @throws UnsupportedEncodingException Thrown if the host system does not support UTF-8 encoding.
+ */
+ private String toString(String prefix) throws UnsupportedEncodingException {
+ StringBuffer buf = new StringBuffer();
+ if (!state.isEmpty()) {
+ buf.append(prefix.length() == 0 ? "." : prefix).append("?");
+ String[] arr = state.keySet().toArray(new String[state.keySet().size()]);
+ for (int i = 0; i < arr.length; ++i) {
+ buf.append(arr[i]).append("=").append(URLEncoder.encode(state.get(arr[i]), "UTF-8"));
+ if (i < arr.length - 1) {
+ buf.append("&");
+ }
+ }
+ buf.append(" ");
+ }
+ if (prefix.length() > 0) {
+ prefix += "/";
+ }
+ String[] keys = children.keySet().toArray(new String[children.keySet().size()]);
+ Arrays.sort(keys);
+ for (String loc : keys) {
+ buf.append(children.get(loc).toString(prefix + URLEncoder.encode(loc, "UTF-8")));
+ }
+ return buf.toString();
+ }
+
+ @Override
+ public String toString() {
+ try {
+ return toString("").trim();
+ }
+ catch (UnsupportedEncodingException e) {
+ return e.toString();
+ }
+ }
+}
diff --git a/documentapi/src/main/java/com/yahoo/documentapi/metrics/DocumentProtocolMetricSet.java b/documentapi/src/main/java/com/yahoo/documentapi/metrics/DocumentProtocolMetricSet.java new file mode 100644 index 00000000000..57090687867 --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/metrics/DocumentProtocolMetricSet.java @@ -0,0 +1,20 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.documentapi.metrics; + +import com.yahoo.messagebus.metrics.MetricSet; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author thomasg + */ +public class DocumentProtocolMetricSet extends MetricSet { + public MetricSet routingPolicyMetrics = new MetricSet("routingpolicies"); + + public DocumentProtocolMetricSet() { + super("document"); + addMetric(routingPolicyMetrics); + } + +} diff --git a/documentapi/src/main/java/com/yahoo/documentapi/package-info.java b/documentapi/src/main/java/com/yahoo/documentapi/package-info.java new file mode 100644 index 00000000000..1fa2e8c8375 --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapi/package-info.java @@ -0,0 +1,7 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +@ExportPackage +@PublicApi +package com.yahoo.documentapi; + +import com.yahoo.api.annotations.PublicApi; +import com.yahoo.osgi.annotation.ExportPackage; diff --git a/documentapi/src/main/java/com/yahoo/documentapiclient/.gitignore b/documentapi/src/main/java/com/yahoo/documentapiclient/.gitignore new file mode 100644 index 00000000000..e69de29bb2d --- /dev/null +++ b/documentapi/src/main/java/com/yahoo/documentapiclient/.gitignore diff --git a/documentapi/src/main/javacc/StateParser.jj b/documentapi/src/main/javacc/StateParser.jj new file mode 100755 index 00000000000..e2acfb446a9 --- /dev/null +++ b/documentapi/src/main/javacc/StateParser.jj @@ -0,0 +1,105 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +/** + * A system state parser. + * When this file is changed, do "ant compileparser" to rebuild the parser. + * + * @author <a href="mailto:simon@yahoo-inc.com">Simon Thoresen</a> + * @version $Id: StateParser.jj,v 1.7 2007-11-15 13:24:45 simon Exp $ + */ +options { + CACHE_TOKENS = true; + STATIC = false; + DEBUG_PARSER = false; + IGNORE_CASE = true; + + // Flip for higher performance + ERROR_REPORTING = true; +} + +PARSER_BEGIN(StateParser) + +package com.yahoo.documentapi.messagebus.systemstate.parser; + +import com.yahoo.documentapi.messagebus.systemstate.rule.*; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.util.List; +import java.util.ArrayList; + +public class StateParser { + +} + +PARSER_END(StateParser) + +TOKEN : +{ + <WHITESPACE: " " | "\t" | "\r" | "\n" | "\f"> | + <SLASH: "/"> | + <DOTDOT: ".."> | + <DOT: "."> | + <ARG: "?"> | + <AND: "&"> | + <EQ: "="> | + <STRING: (<SPACE> | <CODE> | <ALPHANUM>)+> | + <#SPACE: "+"> | + <#CODE: "%" ["0"-"9","A"-"F","a"-"f"] ["0"-"9","A"-"F","a"-"f"]> | + <#ALPHANUM: ["0"-"9","A"-"Z","a"-"z","-",".","_","~"]> +} + +NodeState systemState() throws UnsupportedEncodingException : +{ + NodeState node = new NodeState(); + Location loc, pwd = new Location(); + List<Argument> arg; +} +{ + ( ( <WHITESPACE> )* + ( loc = location(pwd) { arg = null; } + [ <ARG> arg = argumentList() ] ) { if (arg == null) { pwd = new Location(loc); } + else { node.addChild(loc.toString(), new NodeState(arg)); } } )+ + { return node; } +} + +Location location(Location pwd) throws UnsupportedEncodingException : +{ + String item; + List<String> list = new ArrayList<String>(); +} +{ + ( ( <SLASH> { pwd.getItems().clear(); } )? + item = locationItem() { list.add(item); } + ( LOOKAHEAD(2) <SLASH> item = locationItem() { list.add(item); } )* + ( LOOKAHEAD(2) <SLASH> )? ) + { Location ret = new Location(pwd, list); return ret; } +} + +String locationItem() throws UnsupportedEncodingException : +{ + String ret; +} +{ + ( <DOTDOT> { return NodeState.NODE_PARENT; } | + <DOT> { return NodeState.NODE_CURRENT; } | + <STRING> { return URLDecoder.decode(token.image, "UTF-8"); } ) +} + +List<Argument> argumentList () throws UnsupportedEncodingException : +{ + Argument item; + List<Argument> list = new ArrayList<Argument>(); +} +{ + ( item = argument() { list.add(item); } ( <AND> item = argument() { list.add(item); } )* ) + { return list; } +} + +Argument argument() throws UnsupportedEncodingException : +{ + String key, val; +} +{ + ( <STRING> { key = token.image; } <EQ> + <STRING> { val = token.image; } ) + { return new Argument(URLDecoder.decode(key, "UTF-8"), URLDecoder.decode(val, "UTF-8")); } +} diff --git a/documentapi/src/main/resources/configdefinitions/documentrouteselectorpolicy.def b/documentapi/src/main/resources/configdefinitions/documentrouteselectorpolicy.def new file mode 100644 index 00000000000..81606d6f580 --- /dev/null +++ b/documentapi/src/main/resources/configdefinitions/documentrouteselectorpolicy.def @@ -0,0 +1,12 @@ +# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +version=2 +namespace=documentapi.messagebus.protocol + +# The name of the route. +route[].name string + +# The document selector for this route. +route[].selector string + +# The feeds that this route accepts. +route[].feed string default="" |