private void instanceChanged(final IInstanceKey instance, final Optional<IScheduledTask> state) { storage.write( (NoResult.Quiet) storeProvider -> { IJobKey job = instance.getJobKey(); UpdateFactory.Update update = updates.get(job); if (update != null) { if (update.getUpdater().containsInstance(instance.getInstanceId())) { LOG.info("Forwarding task change for " + InstanceKeys.toString(instance)); try { evaluateUpdater( storeProvider, update, getOnlyMatch(storeProvider.getJobUpdateStore(), queryActiveByJob(job)), ImmutableMap.of(instance.getInstanceId(), state)); } catch (UpdateStateException e) { throw Throwables.propagate(e); } } else { LOG.info( "Instance " + instance + " is not part of active update for " + JobKeys.canonicalString(job)); } } }); }
private void evaluateUpdater( final MutableStoreProvider storeProvider, final UpdateFactory.Update update, IJobUpdateSummary summary, Map<Integer, Optional<IScheduledTask>> changedInstance) throws UpdateStateException { JobUpdateStatus updaterStatus = summary.getState().getStatus(); final IJobUpdateKey key = summary.getKey(); JobUpdateStore.Mutable updateStore = storeProvider.getJobUpdateStore(); if (!updateStore.getLockToken(key).isPresent()) { recordAndChangeJobUpdateStatus( storeProvider, key, newEvent(ERROR).setMessage(LOST_LOCK_MESSAGE)); return; } IJobUpdateInstructions instructions = updateStore.fetchJobUpdateInstructions(key).get(); if (isCoordinatedAndPulseExpired(key, instructions)) { // Move coordinated update into awaiting pulse state. JobUpdateStatus blockedStatus = getBlockedState(summary.getState().getStatus()); changeUpdateStatus( storeProvider, summary, newEvent(blockedStatus).setMessage(PULSE_TIMEOUT_MESSAGE)); return; } InstanceStateProvider<Integer, Optional<IScheduledTask>> stateProvider = instanceId -> getActiveInstance(storeProvider.getTaskStore(), key.getJob(), instanceId); EvaluationResult<Integer> result = update.getUpdater().evaluate(changedInstance, stateProvider); LOG.info(key + " evaluation result: " + result); for (Map.Entry<Integer, SideEffect> entry : result.getSideEffects().entrySet()) { Iterable<InstanceUpdateStatus> statusChanges; int instanceId = entry.getKey(); List<IJobInstanceUpdateEvent> savedEvents = updateStore.fetchInstanceEvents(key, instanceId); Set<JobUpdateAction> savedActions = FluentIterable.from(savedEvents).transform(EVENT_TO_ACTION).toSet(); // Don't bother persisting a sequence of status changes that represents an instance that // was immediately recognized as being healthy and in the desired state. if (entry.getValue().getStatusChanges().equals(NOOP_INSTANCE_UPDATE) && savedEvents.isEmpty()) { LOG.info("Suppressing no-op update for instance " + instanceId); statusChanges = ImmutableSet.of(); } else { statusChanges = entry.getValue().getStatusChanges(); } for (InstanceUpdateStatus statusChange : statusChanges) { JobUpdateAction action = STATE_MAP.get(Pair.of(statusChange, updaterStatus)); requireNonNull(action); // A given instance update action may only be issued once during the update lifecycle. // Suppress duplicate events due to pause/resume operations. if (savedActions.contains(action)) { LOG.info( String.format( "Suppressing duplicate update %s for instance %s.", action, instanceId)); } else { IJobInstanceUpdateEvent event = IJobInstanceUpdateEvent.build( new JobInstanceUpdateEvent() .setInstanceId(instanceId) .setTimestampMs(clock.nowMillis()) .setAction(action)); updateStore.saveJobInstanceUpdateEvent(summary.getKey(), event); } } } OneWayStatus status = result.getStatus(); if (status == SUCCEEDED || status == OneWayStatus.FAILED) { if (SideEffect.hasActions(result.getSideEffects().values())) { throw new IllegalArgumentException( "A terminal state should not specify actions: " + result); } JobUpdateEvent event = new JobUpdateEvent(); if (status == SUCCEEDED) { event.setStatus(update.getSuccessStatus()); } else { event.setStatus(update.getFailureStatus()); // Generate a transition message based on one (arbitrary) instance in the group that pushed // the update over the failure threshold (in all likelihood this group is of size 1). // This is done as a rough cut to aid in diagnosing a failed update, as generating a // complete summary would likely be of dubious value. for (Map.Entry<Integer, SideEffect> entry : result.getSideEffects().entrySet()) { Optional<Failure> failure = entry.getValue().getFailure(); if (failure.isPresent()) { event.setMessage(failureMessage(entry.getKey(), failure.get())); break; } } } changeUpdateStatus(storeProvider, summary, event); } else { LOG.info("Executing side-effects for update of " + key + ": " + result.getSideEffects()); for (Map.Entry<Integer, SideEffect> entry : result.getSideEffects().entrySet()) { IInstanceKey instance = InstanceKeys.from(key.getJob(), entry.getKey()); Optional<InstanceAction> action = entry.getValue().getAction(); if (action.isPresent()) { Optional<InstanceActionHandler> handler = action.get().getHandler(); if (handler.isPresent()) { Optional<Amount<Long, Time>> reevaluateDelay = handler .get() .getReevaluationDelay( instance, instructions, storeProvider, stateManager, updaterStatus); if (reevaluateDelay.isPresent()) { executor.schedule( getDeferredEvaluator(instance, key), reevaluateDelay.get().getValue(), reevaluateDelay.get().getUnit().getTimeUnit()); } } } } } }