All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.vmware.xenon.services.common.TransactionService Maven / Gradle / Ivy

There is a newer version: 1.6.18
Show newest version
/*
 * Copyright (c) 2014-2015 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 java.util.Collection;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Set;

import com.vmware.xenon.common.Operation;
import com.vmware.xenon.common.Operation.TransactionContext;
import com.vmware.xenon.common.OperationJoin;
import com.vmware.xenon.common.ServiceDocument;
import com.vmware.xenon.common.StatefulService;
import com.vmware.xenon.common.TaskState;
import com.vmware.xenon.common.Utils;

/**
 * Transaction Coordinator responsible for driving a single transaction. Naming conventions follow
 * the protocol conventions described in https://goo.gl/Mg4QcH
 * 

* FIXME: "Try" and "Ensure" are WIP terms. We will be changing these soon. */ public class TransactionService extends StatefulService { /** * Capture status of coordinator. */ public enum SubStage { /** * Collecting logs from services */ COLLECTING, /** * Received request to commit; in the process resolving conflicts */ RESOLVING, /** * No conflicts found, proceed with commit */ COMMITTING, /** * Received request to abort; in the process of rolling back */ ABORTING, /** * Commit tombstone */ COMMITTED, /** * Abort tombstone */ ABORTED } /** * Capture possible resolution requests; resolved by the PATCH handler */ public enum ResolutionKind { COMMIT, ABORT, COMMITTING, // fire up notifications upon completion self-patch COMMITTED, ABORTED } /** * Various options that affect the performance and guarantees transactions offer. Default values are * made explicit for self-documentation purposes. */ public static class Options { /** * Distinguish the semantics of HTTP errors from the properties transactions guarantee. For instance, * creating an existing service instance would cause an exception -- whereas this does not alter the * semantics of a "safe" transaction. */ public boolean allowErrorsCauseAbort = true; } /** * Captures a request to commit or abort */ public static class ResolutionRequest { public static final String KIND = Utils.buildKind(ResolutionRequest.class); public String kind = KIND; /** * Commit or Abort */ public ResolutionKind resolutionKind; } /** * A conflict check request to be sent to a parent coordinator */ public static class ConflictCheckRequest { /** * Link of service with potential conflict */ public String serviceLink; } /** * A conflict check response sent by a parent coordinator */ public static class ConflictCheckResponse { /** * Substage of the parent coordinator */ public SubStage subStage; /** * Whether the potentially conflicting service is in the parent * coordinator's write set */ public boolean serviceIsInWriteSet; } /** * The document backing up the service is consulted upon commit time. It includes a couple * of optimization fields that would allow it to proceed instantly in the common case (i.e., * global overlaps, per-service potential conflicts) as well as a single level of the dependency * graph. */ public static class TransactionServiceState extends ServiceDocument { /** * Set of services operations within the transaction read (e.g., GET) state from */ public Set readLinks; /** * Set of services operations within the transaction wrote (e.g., PUT, PATCH) state to */ public Set modifiedLinks; /** * Set of services that have been created in the context of this transaction */ public Set createdLinks; /** * Set of services that have been deleted in the context of this transaction */ public Set deletedLinks; /** * A mapping from services to coordinators responsible for pending operations on these services */ public LinkedHashMap> servicesToCoordinators; /** * Tracks the task's stages. Managed by DCP. */ public TaskState taskInfo = TaskState.create(); /** * Current stage in the protocol -- updated atomically */ public SubStage taskSubStage; /** * Options customizing the behavior (performance, guarantees) of the coordinator/transactions. */ public Options options; /** * Keeps track of services that have failed. */ public Set failedLinks; /** * A list of tenant links which can access this service. */ public Set tenantLinks; } private TransactionResolutionService resolutionHelper; /** * Using increased guarantees to make sure the coordinator can sustain failures. */ public TransactionService() { super(TransactionServiceState.class); super.toggleOption(ServiceOption.REPLICATION, true); super.toggleOption(ServiceOption.PERSISTENCE, true); super.toggleOption(ServiceOption.OWNER_SELECTION, true); } /** * Initiate transaction context. It maintains options, but corrects stage to "collecting" *

* TODO: How to check if service is restarting from disk? */ @Override public void handleStart(Operation start) { TransactionServiceState s = start.getBody(TransactionServiceState.class); // Default state s.taskSubStage = s.taskSubStage == null ? SubStage.COLLECTING : s.taskSubStage; s.options = s.options == null ? new Options() : s.options; s.servicesToCoordinators = s.servicesToCoordinators == null ? new LinkedHashMap<>() : s.servicesToCoordinators; s.readLinks = s.readLinks == null ? new HashSet<>() : s.readLinks; s.modifiedLinks = s.modifiedLinks == null ? new HashSet<>() : s.modifiedLinks; s.createdLinks = s.createdLinks == null ? new HashSet<>() : s.createdLinks; s.deletedLinks = s.deletedLinks == null ? new HashSet<>() : s.deletedLinks; s.failedLinks = new HashSet<>(); setState(start, s); // unfortunately, we need to allocate before we complete, because otherwise the client // is racing to commit, and there is a chance he might make it first allocateResolutionService(start); } /** * Allocate the stateless subservice responsible for initiating the commit resolution, while it holds the client * until the resolution is complete. */ private void allocateResolutionService(Operation op) { this.resolutionHelper = new TransactionResolutionService(this); op.complete(); } /** * Log new operations -- requests come in the form {Action, URI, [URI]} */ @Override public void handlePut(Operation put) { if (!put.hasBody()) { put.fail(new IllegalArgumentException("Body is required")); return; } TransactionContext record = put.getBody(TransactionContext.class); TransactionServiceState existing = getState(put); String serviceLink = put.getRequestHeader(Operation.TRANSACTION_REFLINK_HEADER); if (record.action == Action.GET) { existing.readLinks.add(serviceLink); } else { existing.modifiedLinks.add(serviceLink); } if (record.action == Action.POST) { existing.createdLinks.add(serviceLink); } if (record.action == Action.DELETE) { existing.deletedLinks.add(serviceLink); } // This has the possibility of overwriting existing pending, but that's OK, because it means the service // evolved, either by (being asked to) commit/abort or having seen more operations -- in any case, this // "pending" is the most recent one, so we're good. if (record.coordinatorLinks != null) { existing.servicesToCoordinators.put(serviceLink, record.coordinatorLinks); } if (!record.isSuccessful) { existing.failedLinks.add(serviceLink); } setState(put, existing); put.complete(); } @Override public void handleConfigurationRequest(Operation request) { // resolution requests arriving directly from clients need to be routed through the resolution helper this.resolutionHelper.handleResolutionRequest(request); } /** * Handles commits and aborts (requests to cancel/rollback). * TODO 1: Should allow receiving number of transactions in flight * TODO 2: Does not support 2PC for integrity checks yet! This will require specific handler from services * TODO 3: Should eventually shield commit/abort against operations not coming via the /resolve utility suffix */ @Override public void handlePatch(Operation patch) { if (!patch.hasBody()) { patch.fail(new IllegalArgumentException("Body is required")); return; } // Check if this is a conflict resolution check from another coordinator if (Operation.TX_ENSURE_COMMIT .equals(patch.getRequestHeaderAsIs(Operation.TRANSACTION_HEADER))) { handleCheckConflicts(patch); return; } // Handle ResolutionRequest ResolutionRequest resolution = patch.getBody(ResolutionRequest.class); if (!resolution.kind.equals(ResolutionRequest.KIND)) { patch.fail(new IllegalArgumentException( "Unrecognized request kind: " + resolution.kind)); return; } TransactionServiceState currentState = getState(patch); if (resolution.resolutionKind == ResolutionKind.ABORT) { if (currentState.taskSubStage == SubStage.COMMITTED || currentState.taskSubStage == SubStage.COMMITTING) { patch.fail(new IllegalStateException( String.format("Already %s", currentState.taskSubStage))); return; } if (currentState.taskSubStage == SubStage.ABORTING || currentState.taskSubStage == SubStage.ABORTED) { logInfo("Alreading in sub-stage %s. Completing request.", currentState.taskSubStage); patch.complete(); return; } updateStage(patch, SubStage.ABORTING); patch.complete(); handleAbort(currentState); } else if (resolution.resolutionKind == ResolutionKind.COMMIT) { if (currentState.taskSubStage == SubStage.ABORTED || currentState.taskSubStage == SubStage.ABORTING) { patch.fail(new IllegalStateException("Already aborted")); return; } if (currentState.taskSubStage == SubStage.COMMITTED || currentState.taskSubStage == SubStage.COMMITTING) { logInfo("Alreading in sub-stage %s. Completing request.", currentState.taskSubStage); patch.complete(); return; } updateStage(patch, SubStage.RESOLVING); patch.complete(); handleCommit(currentState); } else if (resolution.resolutionKind == ResolutionKind.COMMITTING) { if (currentState.taskSubStage == SubStage.ABORTED || currentState.taskSubStage == SubStage.ABORTING) { patch.fail(new IllegalStateException("Already aborted")); return; } updateStage(patch, SubStage.COMMITTING); patch.complete(); notifyServicesToCommit(currentState); } else if (resolution.resolutionKind == ResolutionKind.COMMITTED) { updateStage(patch, SubStage.COMMITTED); patch.complete(); } else if (resolution.resolutionKind == ResolutionKind.ABORTED) { updateStage(patch, SubStage.ABORTED); patch.complete(); } else { patch.fail(new IllegalArgumentException( "Unrecognized resolution kind: " + resolution.resolutionKind)); } } /** * Handle the case where a client requests to commit. This initiates conflict resolution, state updates etc. */ private void handleCommit(TransactionServiceState existing) { if (existing.options.allowErrorsCauseAbort && !existing.failedLinks.isEmpty()) { logWarning("Failed to commit: some transactional operations have failed. Aborting."); selfPatch(ResolutionKind.ABORT); return; } checkPotentialConflicts(existing); } /** * Update stage to new stage */ private void updateStage(Operation op, SubStage stage) { TransactionServiceState existing = getState(op); existing.taskSubStage = stage; setState(op, existing); } /** * Sends a selfPatch resolution request */ private void selfPatch(ResolutionKind resolution) { ResolutionRequest resolve = new ResolutionRequest(); resolve.resolutionKind = resolution; Operation operation = Operation .createPatch(getUri()) .setCompletion((o, e) -> { if (e != null) { logWarning("Failure self patching: %s", e.getMessage()); } }) .setBody(resolve) .setTransactionId(null); sendRequest(operation); } /** * Handle the case when a client requests to abort. */ private void handleAbort(TransactionServiceState existing) { Collection ops = createNotifyServicesToAbort(existing); if (ops.isEmpty()) { selfPatch(ResolutionKind.ABORTED); return; } OperationJoin.create(ops) .setCompletion((operations, failures) -> { if (failures != null) { logWarning("Transaction failed to notify some services to abort: %s", failures.toString()); } selfPatch(ResolutionKind.ABORTED); }).sendWith(this); } /** * Handle the case when another, peer coordinator is about to commit, and * wants to learn about a coordinator (this) in its write set. */ private void handleCheckConflicts(Operation op) { TransactionServiceState existing = getState(op); ConflictCheckRequest req = op.getBody(ConflictCheckRequest.class); ConflictCheckResponse res = new ConflictCheckResponse(); res.subStage = existing.taskSubStage; res.serviceIsInWriteSet = existing.modifiedLinks.contains(req.serviceLink); boolean abort = false; if (existing.taskSubStage == SubStage.COLLECTING || existing.taskSubStage == SubStage.RESOLVING) { // potential conflict - resolve deterministically String txLink = op.getRequestHeader(Operation.TRANSACTION_REFLINK_HEADER); if (!compareTo(txLink)) { logInfo("Conflicting transaction %s is trying to commit, aborting this transaction...", txLink); abort = true; updateStage(op, SubStage.ABORTING); res.subStage = SubStage.ABORTING; } } op.setBodyNoCloning(res); op.complete(); if (abort) { handleAbort(existing); } } /** * Check potential conflicts with coordinators corresponding with * this coordinator's write-set. */ private void checkPotentialConflicts(TransactionServiceState state) { Collection operations = new HashSet<>(); boolean[] continueWithCommit = new boolean[1]; continueWithCommit[0] = true; for (String serviceLink : state.modifiedLinks) { if (!state.servicesToCoordinators.containsKey(serviceLink)) { continue; } for (String coordinator : state.servicesToCoordinators.get(serviceLink)) { if (coordinator.equals(getSelfLink())) { continue; } operations.add(createNotifyOp(coordinator, serviceLink, Operation.TX_ENSURE_COMMIT, (o, e) -> { if (e != null) { continueWithCommit[0] = false; logWarning( "Failed to receive response from transaction %s, aborting this transaction...", coordinator); selfPatch(ResolutionKind.ABORT); return; } ConflictCheckResponse res = o .getBody(ConflictCheckResponse.class); if (!res.serviceIsInWriteSet || res.subStage == SubStage.ABORTED || res.subStage == SubStage.ABORTING) { // no conflict return; } if (res.subStage == SubStage.COMMITTED || res.subStage == SubStage.COMMITTING || !compareTo(coordinator)) { continueWithCommit[0] = false; logInfo("Conflicting transaction %s is committing, aborting this transaction...", coordinator); selfPatch(ResolutionKind.ABORT); } })); } } if (operations.isEmpty()) { selfPatch(ResolutionKind.COMMITTING); return; } OperationJoin.create(operations).setCompletion((ops, failures) -> { if (failures != null) { logWarning("Failed to commit: %s. Aborting.", failures); selfPatch(ResolutionKind.ABORT); return; } if (continueWithCommit[0]) { selfPatch(ResolutionKind.COMMITTING); } }).sendWith(getHost()); } /** * Lexicographic comparison between this selfLink and the remote * @return true, this wins; false, remote wins */ private boolean compareTo(String remote) { return getSelfLink().compareTo(remote) < 0; } /** * Prepare a tiny metadata request to all services to abort */ private Collection createNotifyServicesToAbort(TransactionServiceState state) { Collection operations = new HashSet<>(); for (String service : state.createdLinks) { operations.add(createDeleteOp(service)); } for (String service : state.readLinks) { if (!state.createdLinks.contains(service)) { operations.add(createNotifyOp(service, Operation.TX_ABORT)); } } for (String service : state.modifiedLinks) { if (!state.createdLinks.contains(service) && !state.readLinks.contains(service)) { operations.add(createNotifyOp(service, Operation.TX_ABORT)); } } return operations; } /** * Send a tiny metadata request to all services to commit */ private void notifyServicesToCommit(TransactionServiceState state) { Collection operations = new HashSet<>(); for (String service : state.deletedLinks) { operations.add(createDeleteOp(service)); } for (String service : state.readLinks) { if (!state.deletedLinks.contains(service)) { operations.add(createNotifyOp(service, Operation.TX_COMMIT)); } } for (String service : state.modifiedLinks) { if (!state.deletedLinks.contains(service) && !state.readLinks.contains(service)) { operations.add(createNotifyOp(service, Operation.TX_COMMIT)); } } if (operations.isEmpty()) { selfPatch(ResolutionKind.COMMITTED); return; } OperationJoin.create(operations).setCompletion((ops, failures) -> { if (failures != null) { logWarning("Failed to commit: %s. Aborting.", failures); selfPatch(ResolutionKind.ABORT); return; } selfPatch(ResolutionKind.COMMITTED); }).sendWith(getHost()); } /** * Prepare a simple metadata request to a service */ private Operation createNotifyOp(String service, String header) { // no completion handler, worst case something will fail, nothing will change on part of the service // and transactional operations headed there will need to contact us prior to commit // (handler would not change this, we will solve this using a transaction GC service) return Operation .createPatch(this, service) .addRequestHeader(Operation.TRANSACTION_HEADER, header) // just an empty body .setBody(new TransactionServiceState()) .setReferer(getUri()) .addRequestHeader(Operation.TRANSACTION_REFLINK_HEADER, getSelfLink()) .setTransactionId(null) .setCompletion((o, e) -> { if (e != null) { logWarning("Notification %s of service %s failed: %s", header, service, e); } else { logInfo("Notification %s of service %s succeeded", header, service); } }); } /** * Prepare a delete request to a service */ private Operation createDeleteOp(String service) { Operation deleteOp = Operation .createDelete(this, service) .setReferer(getUri()) .addRequestHeader(Operation.TRANSACTION_REFLINK_HEADER, getSelfLink()) .setTransactionId(null) .setCompletion((o, e) -> { if (e != null) { logWarning("Deletion of service %s failed: %s", service, e); } else { logInfo("Deletion of service %s succeeded", service); } }); setAuthorizationContext(deleteOp, getSystemAuthorizationContext()); return deleteOp; } /** * Prepare an operation to check conflicts with a remote coordinator */ private Operation createNotifyOp(String coordinator, String serviceLink, String header, Operation.CompletionHandler callback) { ConflictCheckRequest body = new ConflictCheckRequest(); body.serviceLink = serviceLink; return Operation .createPatch(this, coordinator) .addRequestHeader(Operation.TRANSACTION_HEADER, header) .setBody(body) .setReferer(getUri()) .addRequestHeader(Operation.TRANSACTION_REFLINK_HEADER, getSelfLink()) .setTransactionId(null) .setCompletion(callback); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy