aboutsummaryrefslogtreecommitdiffstats
path: root/documentapi/src/main
diff options
context:
space:
mode:
authorJon Bratseth <bratseth@yahoo-inc.com>2016-06-15 23:09:44 +0200
committerJon Bratseth <bratseth@yahoo-inc.com>2016-06-15 23:09:44 +0200
commit72231250ed81e10d66bfe70701e64fa5fe50f712 (patch)
tree2728bba1131a6f6e5bdf95afec7d7ff9358dac50 /documentapi/src/main
Publish
Diffstat (limited to 'documentapi/src/main')
-rw-r--r--documentapi/src/main/docapi-with-dependencies.xml19
-rw-r--r--documentapi/src/main/java/com/yahoo/documentapi/AckToken.java21
-rw-r--r--documentapi/src/main/java/com/yahoo/documentapi/AsyncParameters.java21
-rw-r--r--documentapi/src/main/java/com/yahoo/documentapi/AsyncSession.java142
-rw-r--r--documentapi/src/main/java/com/yahoo/documentapi/BucketListVisitorResponse.java29
-rw-r--r--documentapi/src/main/java/com/yahoo/documentapi/DocumentAccess.java172
-rw-r--r--documentapi/src/main/java/com/yahoo/documentapi/DocumentAccessException.java40
-rwxr-xr-xdocumentapi/src/main/java/com/yahoo/documentapi/DocumentAccessParams.java33
-rw-r--r--documentapi/src/main/java/com/yahoo/documentapi/DocumentIdResponse.java81
-rw-r--r--documentapi/src/main/java/com/yahoo/documentapi/DocumentListVisitorResponse.java28
-rw-r--r--documentapi/src/main/java/com/yahoo/documentapi/DocumentResponse.java82
-rw-r--r--documentapi/src/main/java/com/yahoo/documentapi/DocumentUpdateResponse.java82
-rw-r--r--documentapi/src/main/java/com/yahoo/documentapi/DocumentVisitor.java23
-rw-r--r--documentapi/src/main/java/com/yahoo/documentapi/DumpVisitorDataHandler.java59
-rw-r--r--documentapi/src/main/java/com/yahoo/documentapi/EmptyBucketsVisitorResponse.java27
-rw-r--r--documentapi/src/main/java/com/yahoo/documentapi/Parameters.java13
-rw-r--r--documentapi/src/main/java/com/yahoo/documentapi/ProgressToken.java816
-rw-r--r--documentapi/src/main/java/com/yahoo/documentapi/RemoveResponse.java47
-rw-r--r--documentapi/src/main/java/com/yahoo/documentapi/Response.java81
-rw-r--r--documentapi/src/main/java/com/yahoo/documentapi/ResponseHandler.java16
-rw-r--r--documentapi/src/main/java/com/yahoo/documentapi/Result.java85
-rw-r--r--documentapi/src/main/java/com/yahoo/documentapi/Session.java39
-rw-r--r--documentapi/src/main/java/com/yahoo/documentapi/SimpleVisitorDocumentQueue.java41
-rw-r--r--documentapi/src/main/java/com/yahoo/documentapi/SubscriptionParameters.java10
-rw-r--r--documentapi/src/main/java/com/yahoo/documentapi/SubscriptionSession.java19
-rwxr-xr-xdocumentapi/src/main/java/com/yahoo/documentapi/SyncParameters.java11
-rwxr-xr-xdocumentapi/src/main/java/com/yahoo/documentapi/SyncSession.java101
-rw-r--r--documentapi/src/main/java/com/yahoo/documentapi/ThroughputLimitQueue.java164
-rw-r--r--documentapi/src/main/java/com/yahoo/documentapi/UpdateResponse.java47
-rw-r--r--documentapi/src/main/java/com/yahoo/documentapi/VisitorControlHandler.java160
-rw-r--r--documentapi/src/main/java/com/yahoo/documentapi/VisitorControlSession.java53
-rw-r--r--documentapi/src/main/java/com/yahoo/documentapi/VisitorDataHandler.java106
-rw-r--r--documentapi/src/main/java/com/yahoo/documentapi/VisitorDataQueue.java82
-rw-r--r--documentapi/src/main/java/com/yahoo/documentapi/VisitorDestinationParameters.java29
-rw-r--r--documentapi/src/main/java/com/yahoo/documentapi/VisitorDestinationSession.java10
-rwxr-xr-xdocumentapi/src/main/java/com/yahoo/documentapi/VisitorIterator.java797
-rw-r--r--documentapi/src/main/java/com/yahoo/documentapi/VisitorParameters.java369
-rw-r--r--documentapi/src/main/java/com/yahoo/documentapi/VisitorResponse.java24
-rw-r--r--documentapi/src/main/java/com/yahoo/documentapi/VisitorSession.java43
-rw-r--r--documentapi/src/main/java/com/yahoo/documentapi/local/LocalAsyncSession.java137
-rw-r--r--documentapi/src/main/java/com/yahoo/documentapi/local/LocalDocumentAccess.java60
-rwxr-xr-xdocumentapi/src/main/java/com/yahoo/documentapi/local/LocalSyncSession.java103
-rw-r--r--documentapi/src/main/java/com/yahoo/documentapi/local/package-info.java7
-rw-r--r--documentapi/src/main/java/com/yahoo/documentapi/messagebus/MessageBusAsyncSession.java300
-rw-r--r--documentapi/src/main/java/com/yahoo/documentapi/messagebus/MessageBusDocumentAccess.java134
-rwxr-xr-xdocumentapi/src/main/java/com/yahoo/documentapi/messagebus/MessageBusParams.java193
-rwxr-xr-xdocumentapi/src/main/java/com/yahoo/documentapi/messagebus/MessageBusSession.java38
-rwxr-xr-xdocumentapi/src/main/java/com/yahoo/documentapi/messagebus/MessageBusSyncSession.java225
-rw-r--r--documentapi/src/main/java/com/yahoo/documentapi/messagebus/MessageBusVisitorDestinationSession.java83
-rwxr-xr-xdocumentapi/src/main/java/com/yahoo/documentapi/messagebus/MessageBusVisitorSession.java1071
-rwxr-xr-xdocumentapi/src/main/java/com/yahoo/documentapi/messagebus/ScheduledEventQueue.java189
-rw-r--r--documentapi/src/main/java/com/yahoo/documentapi/messagebus/loadtypes/LoadType.java39
-rw-r--r--documentapi/src/main/java/com/yahoo/documentapi/messagebus/loadtypes/LoadTypeSet.java100
-rw-r--r--documentapi/src/main/java/com/yahoo/documentapi/messagebus/loadtypes/package-info.java5
-rw-r--r--documentapi/src/main/java/com/yahoo/documentapi/messagebus/package-info.java7
-rwxr-xr-xdocumentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/ANDPolicy.java67
-rw-r--r--documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/AbstractRoutableFactory.java44
-rw-r--r--documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/AsyncInitializationPolicy.java118
-rw-r--r--documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/BatchDocumentUpdateMessage.java184
-rwxr-xr-xdocumentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/BatchDocumentUpdateReply.java29
-rw-r--r--documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/ContentPolicy.java43
-rw-r--r--documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/CreateVisitorMessage.java217
-rw-r--r--documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/CreateVisitorReply.java34
-rw-r--r--documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/DestroyVisitorMessage.java37
-rw-r--r--documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/DocumentAcceptedReply.java12
-rw-r--r--documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/DocumentIgnoredReply.java8
-rwxr-xr-xdocumentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/DocumentListEntry.java47
-rwxr-xr-xdocumentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/DocumentListMessage.java54
-rwxr-xr-xdocumentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/DocumentMessage.java85
-rwxr-xr-xdocumentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/DocumentProtocol.java578
-rw-r--r--documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/DocumentProtocolRoutingPolicy.java16
-rwxr-xr-xdocumentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/DocumentReply.java53
-rwxr-xr-xdocumentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/DocumentRouteSelectorPolicy.java179
-rw-r--r--documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/DocumentState.java131
-rw-r--r--documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/DocumentSummaryMessage.java27
-rw-r--r--documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/EmptyBucketsMessage.java42
-rwxr-xr-xdocumentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/ErrorPolicy.java44
-rwxr-xr-xdocumentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/ExternPolicy.java147
-rw-r--r--documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/ExternalSlobrokPolicy.java121
-rwxr-xr-xdocumentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/GetBucketListMessage.java50
-rwxr-xr-xdocumentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/GetBucketListReply.java70
-rwxr-xr-xdocumentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/GetBucketStateMessage.java69
-rwxr-xr-xdocumentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/GetBucketStateReply.java51
-rwxr-xr-xdocumentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/GetDocumentMessage.java94
-rwxr-xr-xdocumentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/GetDocumentReply.java108
-rw-r--r--documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/LazyDecoder.java11
-rw-r--r--documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/LoadBalancer.java151
-rw-r--r--documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/LoadBalancerPolicy.java118
-rwxr-xr-xdocumentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/LocalServicePolicy.java138
-rw-r--r--documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/MapVisitorMessage.java47
-rw-r--r--documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/MessageTypePolicy.java61
-rwxr-xr-xdocumentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/PutDocumentMessage.java156
-rw-r--r--documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/QueryResultMessage.java39
-rwxr-xr-xdocumentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/RemoveDocumentMessage.java101
-rwxr-xr-xdocumentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/RemoveDocumentReply.java35
-rwxr-xr-xdocumentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/RemoveLocationMessage.java52
-rw-r--r--documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/ReplyMerger.java109
-rwxr-xr-xdocumentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/RoundRobinPolicy.java125
-rwxr-xr-xdocumentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/RoutableFactories50.java984
-rwxr-xr-xdocumentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/RoutableFactories51.java126
-rw-r--r--documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/RoutableFactories52.java87
-rwxr-xr-xdocumentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/RoutableFactory.java44
-rwxr-xr-xdocumentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/RoutableRepository.java237
-rwxr-xr-xdocumentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/RoutingPolicyFactories.java148
-rwxr-xr-xdocumentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/RoutingPolicyFactory.java34
-rwxr-xr-xdocumentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/RoutingPolicyRepository.java76
-rw-r--r--documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/SearchColumnPolicy.java183
-rw-r--r--documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/SearchResultMessage.java27
-rwxr-xr-xdocumentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/SearchRowPolicy.java85
-rwxr-xr-xdocumentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/StatBucketMessage.java60
-rwxr-xr-xdocumentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/StatBucketReply.java19
-rw-r--r--documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/StoragePolicy.java469
-rwxr-xr-xdocumentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/SubsetServicePolicy.java145
-rw-r--r--documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/TestAndSetMessage.java16
-rwxr-xr-xdocumentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/UpdateDocumentMessage.java175
-rwxr-xr-xdocumentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/UpdateDocumentReply.java35
-rw-r--r--documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/VisitorInfoMessage.java63
-rw-r--r--documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/VisitorMessage.java6
-rw-r--r--documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/VisitorReply.java9
-rwxr-xr-xdocumentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/WriteDocumentReply.java34
-rw-r--r--documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/WrongDistributionReply.java27
-rw-r--r--documentapi/src/main/java/com/yahoo/documentapi/messagebus/protocol/package-info.java7
-rwxr-xr-xdocumentapi/src/main/java/com/yahoo/documentapi/messagebus/systemstate/rule/Argument.java40
-rwxr-xr-xdocumentapi/src/main/java/com/yahoo/documentapi/messagebus/systemstate/rule/Location.java120
-rwxr-xr-xdocumentapi/src/main/java/com/yahoo/documentapi/messagebus/systemstate/rule/NodeState.java310
-rw-r--r--documentapi/src/main/java/com/yahoo/documentapi/metrics/DocumentProtocolMetricSet.java20
-rw-r--r--documentapi/src/main/java/com/yahoo/documentapi/package-info.java7
-rw-r--r--documentapi/src/main/java/com/yahoo/documentapiclient/.gitignore0
-rwxr-xr-xdocumentapi/src/main/javacc/StateParser.jj105
-rw-r--r--documentapi/src/main/resources/configdefinitions/documentrouteselectorpolicy.def12
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&aring;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&aring;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&aring;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&aring;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&aring;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&aring;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&aring;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&aring;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:&lt;routename&gt;" 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=&lt;pattern&gt; (mandatory, determines the pattern of nodes to send to)<br>
+ * slobroks=&lt;comma-separated connectionspecs&gt; (optional, list of slobroks to use to find the pattern)<br>
+ * config=&lt;comma-separated list of config servers&gt; (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=""