/** * Reschedules the task. */ private void reschedule(long timestamp) { if (interval > 0) { time = timestamp + interval; schedule(); } }
@Override public Commit<Void> mapToNull() { return new DefaultCommit<>(index, operation, null, session, timestamp); }
@Override public Scheduled schedule(Duration delay, Runnable callback) { checkOperation(OperationType.COMMAND, "callbacks can only be scheduled during command execution"); checkArgument(!delay.isNegative(), "delay cannot be negative"); checkNotNull(callback, "callback cannot be null"); log.trace("Scheduled callback {} with delay {}", callback, delay); return new ScheduledTask(callback, delay.toMillis()).schedule(); }
@Override public CompletableFuture<RestoreResponse> restore(RestoreRequest request) { logRequest(request); if (request.term() != context.currentTerm()) { return CompletableFuture.completedFuture(logResponse(RestoreResponse.error())); } HeapBuffer buffer = HeapBuffer.allocate(); try { Collection<PrimaryBackupSession> sessions = context.getSessions(); buffer.writeInt(sessions.size()); for (Session session : sessions) { buffer.writeLong(session.sessionId().id()); buffer.writeString(session.memberId().id()); } context.service().backup(new DefaultBackupOutput(buffer, context.service().serializer())); buffer.flip(); byte[] bytes = buffer.readBytes(buffer.remaining()); return CompletableFuture.completedFuture( RestoreResponse.ok(context.currentIndex(), context.currentTimestamp(), bytes)) .thenApply(this::logResponse); } finally { buffer.release(); } }
/** * Requests a restore from the primary. */ private void requestRestore(MemberId primary) { context.protocol().restore(primary, RestoreRequest.request(context.descriptor(), context.currentTerm())) .whenCompleteAsync((response, error) -> { if (error == null && response.status() == PrimaryBackupResponse.Status.OK) { context.resetIndex(response.index(), response.timestamp()); Buffer buffer = HeapBuffer.wrap(response.data()); int sessions = buffer.readInt(); for (int i = 0; i < sessions; i++) { context.getOrCreateSession(buffer.readLong(), MemberId.from(buffer.readString())); } context.service().restore(new DefaultBackupInput(buffer, context.service().serializer())); operations.clear(); } }, context.threadContext()); } }
@Override public void execute(Runnable callback) { checkOperation(OperationType.COMMAND, "callbacks can only be scheduled during command execution"); checkNotNull(callback, "callback cannot be null"); tasks.add(callback); }
@Override public String toString() { return toStringHelper(this) .add("index", index) .add("session", session) .add("time", wallClockTime()) .add("operation", operation) .add("value", value instanceof byte[] ? ArraySizeHashPrinter.of((byte[]) value) : value) .toString(); } }
@Override public <T> void register(OperationId operationId, Consumer<Commit<T>> callback) { checkNotNull(operationId, "operationId cannot be null"); checkNotNull(callback, "callback cannot be null"); handle(operationId, commit -> { callback.accept(commit.map(this::decode)); return null; }); }
@Override public byte[] apply(Commit<byte[]> commit) { log.trace("Executing {}", commit); this.operationType = commit.operation().type(); this.timestamp = commit.wallClockTime().unixTimestamp(); // Look up the registered callback for the operation. Function<Commit<byte[]>, byte[]> operation = operations.get(commit.operation().id()); if (operation == null) { throw new IllegalStateException("Unknown state machine operation: " + commit.operation()); } else { // Execute the operation. If the operation return value is a Future, await the result, // otherwise immediately complete the execution future. try { return operation.apply(commit); } catch (Exception e) { log.warn("State machine operation failed: {}", e.getMessage()); throw new PrimitiveException.ServiceException(e); } finally { runTasks(); } } }
@Override public final void init(ServiceContext context) { this.context = context; this.executor = new DefaultServiceExecutor(context, serializer()); this.log = ContextualLoggerFactory.getLogger(getClass(), LoggerContext.builder(PrimitiveService.class) .addValue(context.serviceId()) .add("type", context.serviceType()) .add("name", context.serviceName()) .build()); configure(executor); }
@Override public <U> Commit<U> map(Function<T, U> transcoder) { return new DefaultCommit<>(index, operation, transcoder.apply(value), session, timestamp); }
@Override public Scheduled schedule(Duration initialDelay, Duration interval, Runnable callback) { checkOperation(OperationType.COMMAND, "callbacks can only be scheduled during command execution"); checkArgument(!initialDelay.isNegative(), "initialDelay cannot be negative"); checkArgument(!interval.isNegative(), "interval cannot be negative"); checkNotNull(callback, "callback cannot be null"); log.trace("Scheduled repeating callback {} with initial delay {} and interval {}", callback, initialDelay, interval); return new ScheduledTask(callback, initialDelay.toMillis(), interval.toMillis()).schedule(); }
/** * Takes a snapshot of the service state. */ public void takeSnapshot(SnapshotWriter writer) { log.debug("Taking snapshot {}", writer.snapshot().index()); // Serialize sessions to the in-memory snapshot and request a snapshot from the state machine. writer.writeLong(primitiveId.id()); writer.writeString(primitiveType.name()); writer.writeString(serviceName); writer.writeLong(currentIndex); writer.writeLong(currentTimestamp); writer.writeLong(timestampDelta); writer.writeInt(sessions.getSessions().size()); for (RaftSession session : sessions.getSessions()) { writer.writeLong(session.sessionId().id()); writer.writeString(session.memberId().id()); writer.writeString(session.readConsistency().name()); writer.writeLong(session.minTimeout()); writer.writeLong(session.maxTimeout()); writer.writeLong(session.getLastUpdated()); writer.writeLong(session.getRequestSequence()); writer.writeLong(session.getCommandSequence()); writer.writeLong(session.getEventIndex()); writer.writeLong(session.getLastCompleted()); } service.backup(new DefaultBackupOutput(writer, service.serializer())); }
Commit<byte[]> commit = new DefaultCommit<>(currentIndex, operation.id(), operation.value(), session, timestamp);
/** * Applies the given commit to the state machine. */ private OperationResult applyCommand(long index, long sequence, long timestamp, PrimitiveOperation operation, RaftSession session) { long eventIndex = session.getEventIndex(); Commit<byte[]> commit = new DefaultCommit<>(index, operation.id(), operation.value(), session, timestamp); OperationResult result; try { currentSession = session; // Execute the state machine operation and get the result. byte[] output = service.apply(commit); // Store the result for linearizability and complete the command. result = OperationResult.succeeded(index, eventIndex, output); } catch (Exception e) { // If an exception occurs during execution of the command, store the exception. result = OperationResult.failed(index, eventIndex, e); } finally { currentSession = null; } // Once the operation has been applied to the state machine, commit events published by the command. // The state machine context will build a composite future for events published to all sessions. commit(); // Register the result in the session to ensure retries receive the same output for the command. session.registerResult(sequence, result); // Update the session timestamp and command sequence number. session.setCommandSequence(sequence); // Complete the command. return result; }
private ExecuteResponse applyQuery(ExecuteRequest request, Session session) { try { byte[] result = context.service().apply(new DefaultCommit<>( context.getIndex(), request.operation().id(), request.operation().value(), context.setSession(session), context.currentTimestamp())); return ExecuteResponse.ok(result); } catch (Exception e) { return ExecuteResponse.error(); } finally { context.setSession(null); } }
/** * Applies an execute operation to the service. */ private void applyExecute(ExecuteOperation operation) { Session session = context.getOrCreateSession(operation.session(), operation.node()); if (operation.operation() != null) { try { context.service().apply(new DefaultCommit<>( context.setIndex(operation.index()), operation.operation().id(), operation.operation().value(), context.setSession(session), context.setTimestamp(operation.timestamp()))); } catch (Exception e) { log.warn("Failed to apply operation: {}", e); } finally { context.setSession(null); } } }
private CompletableFuture<ExecuteResponse> executeCommand(ExecuteRequest request) { PrimaryBackupSession session = context.getOrCreateSession(request.session(), request.node()); long index = context.nextIndex(); long timestamp = System.currentTimeMillis(); return replicator.replicate(new ExecuteOperation( index, timestamp, session.sessionId().id(), session.memberId(), request.operation())) .thenApply(v -> { try { byte[] result = context.service().apply(new DefaultCommit<>( context.setIndex(index), request.operation().id(), request.operation().value(), context.setSession(session), context.setTimestamp(timestamp))); return ExecuteResponse.ok(result); } catch (Exception e) { return ExecuteResponse.error(); } finally { context.setSession(null); } }); }