Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance. Project price only 1 $
You can buy this project and download/modify it how often you want.
/*
* Copyright (c) 2016 VMware, Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed
* under the License is distributed on an "AS IS" BASIS, without warranties or
* conditions of any kind, EITHER EXPRESS OR IMPLIED. See the License for the
* specific language governing permissions and limitations under the License.
*/
package com.vmware.xenon.services.common;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toMap;
import static java.util.stream.Collectors.toSet;
import java.net.URI;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import com.vmware.xenon.common.DeferredResult;
import com.vmware.xenon.common.FactoryService;
import com.vmware.xenon.common.FactoryService.FactoryServiceConfiguration;
import com.vmware.xenon.common.NodeSelectorState;
import com.vmware.xenon.common.Operation;
import com.vmware.xenon.common.Operation.CompletionHandler;
import com.vmware.xenon.common.OperationJoin;
import com.vmware.xenon.common.Service;
import com.vmware.xenon.common.ServiceConfiguration;
import com.vmware.xenon.common.ServiceDocument;
import com.vmware.xenon.common.ServiceDocumentDescription.PropertyUsageOption;
import com.vmware.xenon.common.ServiceDocumentDescription.TypeName;
import com.vmware.xenon.common.ServiceDocumentQueryResult;
import com.vmware.xenon.common.ServiceHost;
import com.vmware.xenon.common.ServiceMaintenanceRequest;
import com.vmware.xenon.common.ServiceMaintenanceRequest.MaintenanceReason;
import com.vmware.xenon.common.ServiceStatUtils;
import com.vmware.xenon.common.ServiceStats.ServiceStat;
import com.vmware.xenon.common.ServiceStats.TimeSeriesStats;
import com.vmware.xenon.common.ServiceStats.TimeSeriesStats.AggregationType;
import com.vmware.xenon.common.StatefulService;
import com.vmware.xenon.common.TaskState;
import com.vmware.xenon.common.TaskState.TaskStage;
import com.vmware.xenon.common.UriUtils;
import com.vmware.xenon.common.Utils;
import com.vmware.xenon.common.config.XenonConfiguration;
import com.vmware.xenon.services.common.NodeGroupService.NodeGroupState;
import com.vmware.xenon.services.common.QueryTask.NumericRange;
import com.vmware.xenon.services.common.QueryTask.Query;
import com.vmware.xenon.services.common.QueryTask.Query.Builder;
import com.vmware.xenon.services.common.QueryTask.QuerySpecification;
import com.vmware.xenon.services.common.QueryTask.QuerySpecification.QueryOption;
/**
* This service queries a Xenon node group for all service documents created from a specified factory link.
* It migrates service documents in three steps:
*
* 1. Retrieve service documents from the source node-group
* 2. Post service documents to the transformation service
* 3. Post transformed service documents to the destination node-group
*
* To retrieve the service documents from the source system by running local paginated queries against each node.
* We merge these results by selecting only the documents owned by the respective hosts and keeping track of the
* lastUpdatedTime per host. This will allow us to start a new migration task picking up all documents changed
* after the lastUpdatedTime.
*
* Before we start retrieving document from the source node group we verify that both the source and destination
* node groups are stable. Once both node group are stable we run a query to obtain the current count of
* documents matching the query. Since we patch the service document after each page is processed with the number
* of documents we migrated, we can report the progress of a migration.
*
* This task can also run continuously restarting during maintenance intervals if not running. If enabled, the
* query spec supplied will be modified by adding a numeric constraint to ensuring that only documents modified
* after latestSourceUpdateTimeMicros.
*
* TransformationService expectations:
* Version 1 of TransformationService:
* We post a map of source document to destination factory to the transformation service and expect a map of
* String (json of transformed object) to destination factory (can be different than the posted destination
* factory) as a response.
*
* Version 2 of TransformationService:
* We post a TransformRequest to the transformation service. The TransformRequest contains the source document
* and destination factory. The TransformResponse contains a map of the transformed json (key) and the factory
* to send the transformed json to (can be different than the posted destination factory).
*
* Suggested Use:
* For each service that needs to be migration start one MigrationTaskService instance. Common scenarios for the
* use of this service are:
*
* - Warming up new nodes to add to an existing node group to minimize the impact of adding a node to an existing
* - Upgrade
*/
public class MigrationTaskService extends StatefulService {
public static final String STAT_NAME_PROCESSED_DOCUMENTS = "processedServiceCount";
public static final String STAT_NAME_ESTIMATED_TOTAL_SERVICE_COUNT = "estimatedTotalServiceCount";
public static final String STAT_NAME_FETCHED_DOCUMENT_COUNT = "fetchedDocumentCount";
public static final String STAT_NAME_BEFORE_TRANSFORM_COUNT = "beforeTransformDocumentCount";
public static final String STAT_NAME_AFTER_TRANSFORM_COUNT = "afterTransformDocumentCount";
public static final String STAT_NAME_DELETE_RETRY_COUNT = "deleteRetryCount";
public static final String STAT_NAME_DELETED_DOCUMENT_COUNT = "deletedDocumentCount";
public static final String STAT_NAME_COUNT_QUERY_TIME_DURATION_MICRO = "countQueryTimeDurationMicros";
public static final String STAT_NAME_RETRIEVAL_OPERATIONS_DURATION_MICRO = "retrievalOperationsDurationMicros";
public static final String STAT_NAME_RETRIEVAL_QUERY_TIME_DURATION_MICRO_FORMAT = "retrievalQueryTimeDurationMicros-%s";
public static final String FACTORY_LINK = ServiceUriPaths.MIGRATION_TASKS;
private static final Integer DEFAULT_PAGE_SIZE = 10_000;
private static final Long DEFAULT_MAINTENANCE_INTERVAL_MILLIS = TimeUnit.MINUTES.toMicros(1);
private static final Integer DEFAULT_MAXIMUM_CONVERGENCE_CHECKS = 10;
// used for the result value of DeferredResult in order to workaround findbug warning for passing null by "defered.complete(null)".
private static final Object DUMMY_OBJECT = new Object();
private static final boolean USE_FORWARD_ONLY_QUERY = XenonConfiguration.bool(
MigrationTaskService.class,
"useForwardOnlyQuery",
false
);
public enum MigrationOption {
/**
* Enables continuous data migration.
*/
CONTINUOUS,
/**
* Enables delete post upgrade fall back if idempotent post does not work.
*/
DELETE_AFTER,
/**
* Enables v2 of TransformationService contract, which sends an object instead of a map.
*/
USE_TRANSFORM_REQUEST,
/**
* Enable migrating historical document(old document versions).
* The migrated versions may not have the same document versions in source, but the order of the history is
* maintained.
*
* NOTE:
* When migrating history with DELETE, destination will only have histories after delete.
* This is due to the DELETE change in xenon 1.3.7+ that DELETE now purges past histories.
* In prior versions, POST with PRAGMA_DIRECTIVE_FORCE_INDEX_UPDATE after DELETE added new version on top of
* existing histories.
*/
ALL_VERSIONS,
/**
* When this option is specified, the migration task calculates estimated number of documents to migrate
* before starting actual migration.
* The number is available via "estimatedTotalServiceCount" stats and in the log.
*
* NOTE:
* It uses count query to calculate the estimate. If target is not IMMUTABLE documents, count is expensive
* operation especially when target has a large number of documents. Thus, specifying this option may cause a
* delay to start actual migration.
*/
ESTIMATE_COUNT,
}
/**
* Create a default factory service that starts instances of this task service on POST.
*/
public static Service createFactory() {
Service fs = FactoryService.create(MigrationTaskService.class, State.class);
// Set additional factory service option. This can be set in service constructor as well
// but its really relevant on the factory of a service.
fs.toggleOption(ServiceOption.IDEMPOTENT_POST, true);
fs.toggleOption(ServiceOption.INSTRUMENTATION, true);
return fs;
}
public static class State extends ServiceDocument {
/**
* URI pointing to the source systems node group. This link takes the form of
* {protocol}://{address}:{port}/core/node-groups/{nodegroup}.
*
* Cannot combine with {@link #sourceReferences}.
*/
public URI sourceNodeGroupReference;
/**
* URIs of source nodes.
*
* IMPORTANT
* Convergence check will NOT be performed on these uri references; Therefore, caller needs to check the convergence
* before starting the migration.
* Also, migration task retrieves each document from its owner node. This sourceReferences list needs to include
* all node uris in source node-group; Otherwise, only partial number of documents will be migrated.
*
* Cannot combine with {@link #sourceNodeGroupReference}.
*/
@UsageOption(option = PropertyUsageOption.AUTO_MERGE_IF_NOT_NULL)
public List sourceReferences = new ArrayList<>();
/**
* Factory link of the source factory.
*/
@UsageOption(option = PropertyUsageOption.AUTO_MERGE_IF_NOT_NULL)
public String sourceFactoryLink;
/**
* URI pointing to the destination system node group. This link takes the form of
* {protocol}://{address}:{port}/core/node-groups/{nodegroup}.
*
* Cannot combine with {@link #destinationReferences}.
*/
public URI destinationNodeGroupReference;
/**
* URIs of destination nodes.
*
* Convergence check will NOT be performed on these uris; Therefore, caller needs to check the convergence
* before starting the migration.
*
* Cannot combine with {@link #destinationNodeGroupReference}.
*/
@UsageOption(option = PropertyUsageOption.AUTO_MERGE_IF_NOT_NULL)
public List destinationReferences = new ArrayList<>();
/**
* Factory link to post the new data to.
*/
@UsageOption(option = PropertyUsageOption.AUTO_MERGE_IF_NOT_NULL)
public String destinationFactoryLink;
/**
* (Optional) Link to the service transforming migrated data on the destination system.
*/
@UsageOption(option = PropertyUsageOption.AUTO_MERGE_IF_NOT_NULL)
public String transformationServiceLink;
/**
* (Optional) Additional query terms used when querying the source system.
*/
@UsageOption(option = PropertyUsageOption.AUTO_MERGE_IF_NOT_NULL)
public QuerySpecification querySpec;
/**
* (Optional) Status of the migration task.
*/
@UsageOption(option = PropertyUsageOption.AUTO_MERGE_IF_NOT_NULL)
public TaskState taskInfo;
/**
* (Optional) This time is used to setting the maintenance interval and as scheduling time.
*/
@UsageOption(option = PropertyUsageOption.AUTO_MERGE_IF_NOT_NULL)
public Long maintenanceIntervalMicros;
/**
* (Optional) Number of checks for node group convergence before failing the task.
*/
@UsageOption(option = PropertyUsageOption.AUTO_MERGE_IF_NOT_NULL)
public Integer maximumConvergenceChecks;
/**
* (Optional) Flag enabling continuous data migration (default: false).
*/
@UsageOption(option = PropertyUsageOption.AUTO_MERGE_IF_NOT_NULL)
public Boolean continuousMigration;
/**
* (Optional) Migration options as destribed in {@link MigrationOption}.
*/
@UsageOption(option = PropertyUsageOption.AUTO_MERGE_IF_NOT_NULL)
public EnumSet migrationOptions;
/**
* (Optional) Operation timeout value to be applied to operations created by the migration task.
*/
@UsageOption(option = PropertyUsageOption.AUTO_MERGE_IF_NOT_NULL)
public Long operationTimeoutMicros;
// The following attributes are the outputs of the task.
/**
* Timestamp of the newest document migrated. This will only be accurate once the migration
* finished successfully.
*/
public Long latestSourceUpdateTimeMicros = 0L;
/**
* Child options used by the factory being migrated.
*/
public EnumSet factoryChildOptions;
/**
* Keeps resolved node-selector path in source group. When it is null,
* default node-selector will be used.
*/
private String nodeSelectorPath;
@Override
public String toString() {
String stage = this.taskInfo != null && this.taskInfo.stage != null ? this.taskInfo.stage.name() : "null";
return String.format(
"MigrationTaskService: [documentSelfLink=%s] [stage=%s] [sourceFactoryLink=%s]",
this.documentSelfLink, stage, this.sourceFactoryLink);
}
}
/** The request body that is sent to a transform service as input. */
public static class TransformRequest {
/** The JSON-encoded original document from the source node group. */
public String originalDocument;
/** The original destination factory, as specified by the migration's {@link State}. */
public String destinationLink;
}
/** The response body that is returned by a transform service as output. */
public static class TransformResponse {
/**
* Key: transformed JSON, and Value: the target destination factory to send it to.
* We use a map to support the use case where we want to break down one object into
* multiple objects that need to be POSTed to different factories after they are
* transformed.
*/
public Map destinationLinks;
}
public MigrationTaskService() {
super(MigrationTaskService.State.class);
super.toggleOption(ServiceOption.CORE, true);
super.toggleOption(ServiceOption.REPLICATION, true);
super.toggleOption(ServiceOption.OWNER_SELECTION, true);
super.toggleOption(ServiceOption.PERIODIC_MAINTENANCE, true);
super.toggleOption(ServiceOption.INSTRUMENTATION, true);
}
@Override
public void handleStart(Operation startPost) {
State initState = getBody(startPost);
logInfo("Starting migration with initState: %s", initState);
initState = initialize(initState);
if (TaskState.isFinished(initState.taskInfo)) {
startPost.complete();
return;
}
if (!verifyState(initState, startPost)) {
return;
}
startPost.complete();
State patchState = new State();
if (initState.taskInfo == null) {
patchState.taskInfo = TaskState.create();
}
if (initState.continuousMigration) {
setMaintenanceIntervalMicros(initState.maintenanceIntervalMicros);
}
if (initState.taskInfo.stage == TaskStage.CANCELLED) {
logInfo("In stage %s, will restart on next maintenance interval",
initState.taskInfo.stage);
return;
}
Operation.createPatch(getUri())
.setBody(patchState)
.sendWith(this);
}
private State initialize(State initState) {
if (initState.querySpec == null) {
initState.querySpec = new QuerySpecification();
}
if (initState.querySpec.resultLimit == null) {
initState.querySpec.resultLimit = DEFAULT_PAGE_SIZE;
}
initState.querySpec.options.addAll(
EnumSet.of(QueryOption.EXPAND_CONTENT, QueryOption.BROADCAST, QueryOption.OWNER_SELECTION));
// when configured to use FORWARD_QUERY option, add it to default query spec.
if (USE_FORWARD_ONLY_QUERY) {
initState.querySpec.options.add(QueryOption.FORWARD_ONLY);
}
if (initState.querySpec.query == null
|| initState.querySpec.query.booleanClauses == null
|| initState.querySpec.query.booleanClauses.isEmpty()) {
initState.querySpec.query = buildFieldClause(initState);
} else {
initState.querySpec.query.addBooleanClause(buildFieldClause(initState));
}
if (initState.taskInfo == null) {
initState.taskInfo = new TaskState();
}
if (initState.taskInfo.stage == null) {
initState.taskInfo.stage = TaskStage.CREATED;
}
if (initState.maintenanceIntervalMicros == null) {
initState.maintenanceIntervalMicros = DEFAULT_MAINTENANCE_INTERVAL_MILLIS;
}
if (initState.maximumConvergenceChecks == null) {
initState.maximumConvergenceChecks = DEFAULT_MAXIMUM_CONVERGENCE_CHECKS;
}
if (initState.migrationOptions == null) {
initState.migrationOptions = EnumSet.noneOf(MigrationOption.class);
}
if (initState.continuousMigration == null) {
initState.continuousMigration = Boolean.FALSE;
}
if (initState.continuousMigration) {
initState.migrationOptions.add(MigrationOption.CONTINUOUS);
}
return initState;
}
private Query buildFieldClause(State initState) {
Query query = Query.Builder.create()
.addFieldClause(ServiceDocument.FIELD_NAME_SELF_LINK, addSlash(initState.sourceFactoryLink),
QueryTask.QueryTerm.MatchType.PREFIX)
.build();
return query;
}
private boolean verifyState(State state, Operation operation) {
List errMsgs = new ArrayList<>();
if (state.sourceFactoryLink == null) {
errMsgs.add("sourceFactory cannot be null.");
}
if (state.sourceNodeGroupReference == null && state.sourceReferences.isEmpty()) {
errMsgs.add("sourceNode or sourceUri need to be specified.");
}
if (state.sourceNodeGroupReference != null && !state.sourceReferences.isEmpty()) {
errMsgs.add("cannot specify both sourceNode and sourceReferences.");
}
if (state.destinationFactoryLink == null) {
errMsgs.add("destinationFactory cannot be null.");
}
if (state.destinationNodeGroupReference == null && state.destinationReferences.isEmpty()) {
errMsgs.add("destinationNode or destinationReferences need to be specified.");
}
if (state.destinationNodeGroupReference != null && !state.destinationReferences.isEmpty()) {
errMsgs.add("cannot specify both destinationNode and destinationReferences.");
}
if (!errMsgs.isEmpty()) {
operation.fail(new IllegalArgumentException(String.join(" ", errMsgs)));
}
return errMsgs.isEmpty();
}
@Override
public void handlePatch(Operation patchOperation) {
State patchState = getBody(patchOperation);
State currentState = getState(patchOperation);
applyPatch(patchState, currentState);
if (!verifyState(currentState, patchOperation)
&& !verifyPatchedState(currentState, patchOperation)) {
return;
}
patchOperation.complete();
logInfo("After PATCH, the latest state is: %s", currentState);
if (TaskState.isFinished(currentState.taskInfo) ||
TaskState.isFailed(currentState.taskInfo) ||
TaskState.isCancelled(currentState.taskInfo)) {
return;
}
if ((patchState.maintenanceIntervalMicros != null || patchState.continuousMigration != null)
&& currentState.continuousMigration) {
setMaintenanceIntervalMicros(currentState.maintenanceIntervalMicros);
}
resolveNodeGroupReferences(currentState);
}
private URI extractBaseUri(NodeState state) {
URI uri = state.groupReference;
return UriUtils.buildUri(uri.getScheme(), uri.getHost(), uri.getPort(), null, null);
}
@Override
public void handleMaintenance(Operation maintenanceOperation) {
maintenanceOperation.complete();
ServiceMaintenanceRequest serviceMaintenanceRequest = maintenanceOperation.getBody(ServiceMaintenanceRequest.class);
if (!serviceMaintenanceRequest.reasons.contains(MaintenanceReason.PERIODIC_SCHEDULE)) {
return;
}
CompletionHandler c = (o, t) -> {
if (t != null) {
logWarning("Error retrieving document %s, %s", getUri(), t);
return;
}
State state = o.getBody(State.class);
if (!state.continuousMigration) {
return;
}
if (state.taskInfo.stage == TaskStage.STARTED
|| state.taskInfo.stage == TaskStage.CREATED) {
return;
}
State patch = new State();
logInfo("Continuous migration enabled, restarting");
// iff the task finished, it is safe to pick up the latestSourceUpdateTimeMicros
// otherwise we will use the last used query
if (state.taskInfo.stage == TaskStage.FINISHED) {
patch.querySpec = state.querySpec;
// update or add a the numeric query clause
Query q = findUpdateTimeMicrosRangeClause(patch.querySpec.query);
if (q != null) {
q.setNumericRange(NumericRange
.createGreaterThanOrEqualRange(state.latestSourceUpdateTimeMicros));
} else {
Query timeClause = Query.Builder
.create()
.addRangeClause(
ServiceDocument.FIELD_NAME_UPDATE_TIME_MICROS,
NumericRange
.createGreaterThanOrEqualRange(state.latestSourceUpdateTimeMicros))
.build();
patch.querySpec.query.addBooleanClause(timeClause);
}
}
// send state update putting service back into started state
patch.taskInfo = TaskState.createAsStarted();
Operation.createPatch(getUri()).setBody(patch).sendWith(this);
};
Operation.createGet(getUri()).setCompletion(c).sendWith(this);
}
private Query findUpdateTimeMicrosRangeClause(Query query) {
if (query.term != null
&& query.term.propertyName != null
&& query.term.propertyName.equals(ServiceDocument.FIELD_NAME_UPDATE_TIME_MICROS)
&& query.term.range != null) {
return query;
}
if (query.booleanClauses == null) {
return null;
}
for (Query q : query.booleanClauses) {
Query match = findUpdateTimeMicrosRangeClause(q);
if (match != null) {
return match;
}
}
return null;
}
/**
* Resolve source and destination nodes.
*
* When sourceReferences or destinationReferences are specified in migration request, those specified uris will be used instead
* of resolving from node-groups. In that case, convergence check of source/destination node-groups will not be
* performed. This is because specified URIs may not be resolvable from node-group service. Therefore, caller needs
* to make sure node-groups are currently converged.
* When node group reference is specified instead of uris, it will access node-group service and obtain currently
* AVAILABLE nodes, and perform convergence check.
*/
private void resolveNodeGroupReferences(State currentState) {
logInfo("Resolving node group differences. [source=%s] [destination=%s]",
currentState.sourceNodeGroupReference, currentState.destinationNodeGroupReference);
// to workaround findbug warning for passing null on complete(), make DeferredResult parameterize with Object
// and return dummy object. The result of DeferredResult here will not be used.
DeferredResult