use of com.netflix.titus.common.framework.reconciler.ModelActionHolder in project titus-control-plane by Netflix.
the class DefaultReconciliationFramework method changeReferenceModel.
@Override
public Observable<Void> changeReferenceModel(MultiEngineChangeAction multiEngineChangeAction, BiFunction<String, Observable<List<ModelActionHolder>>, ChangeAction> engineChangeActionFactory, String... rootEntityHolderIds) {
Preconditions.checkArgument(rootEntityHolderIds.length > 1, "Change action for multiple engines requested, but %s root id holders provided", rootEntityHolderIds.length);
return Observable.create(emitter -> {
List<ReconciliationEngine<EVENT>> engines = new ArrayList<>();
for (String id : rootEntityHolderIds) {
ReconciliationEngine<EVENT> engine = findEngineByRootId(id).orElseThrow(() -> new IllegalArgumentException("Reconciliation engine not found: rootId=" + id));
engines.add(engine);
}
List<Observable<Map<String, List<ModelActionHolder>>>> outputs = ObservableExt.propagate(multiEngineChangeAction.apply(), engines.size());
List<Observable<Void>> engineActions = new ArrayList<>();
for (int i = 0; i < engines.size(); i++) {
ReconciliationEngine<EVENT> engine = engines.get(i);
String rootId = engine.getReferenceView().getId();
ChangeAction engineAction = engineChangeActionFactory.apply(rootId, outputs.get(i).map(r -> r.get(rootId)));
engineActions.add(engine.changeReferenceModel(engineAction));
}
// Synchronize on subscription to make sure that this operation is not interleaved with concurrent
// subscriptions for the same set or subset of the reconciliation engines. The interleaving might result
// in a deadlock. For example with two engines engineA and engineB:
// - multi-engine change action M1 for engineA and engineB is scheduled
// - M1/engineA is added to its queue
// - another multi-engine change action M2 for engineA and engineB is scheduled
// - M2/engineB is added to its queue
// - M1/engineB is added to its queue, and next M2/engineA
// Executing M1 requires that both M1/engineA and M1/engineB are at the top of the queue, but in this case
// M2/engineB is ahead of the M1/engineB. On the other hand, M1/engineA is ahead of M2/engineB. Because
// of that we have deadlock. Please, note that we can ignore here the regular (engine scoped) change actions.
Subscription subscription;
synchronized (multiEngineChangeLock) {
subscription = Observable.mergeDelayError(engineActions).subscribe(emitter::onNext, emitter::onError, emitter::onCompleted);
}
emitter.setSubscription(subscription);
}, Emitter.BackpressureMode.NONE);
}
use of com.netflix.titus.common.framework.reconciler.ModelActionHolder in project titus-control-plane by Netflix.
the class SingleTransaction method applyModelUpdates.
@Override
public Optional<ModelHolder> applyModelUpdates(ModelHolder modelHolder) {
if (transactionStep != TransactionStep.ChangeActionCompleted) {
return Optional.empty();
}
this.transactionStep = TransactionStep.ModelsUpdated;
if (modelActionHolders.isEmpty()) {
return Optional.empty();
}
EntityHolder referenceRootHolder = modelHolder.getReference();
EntityHolder runningRootHolder = modelHolder.getRunning();
EntityHolder storeRootHolder = modelHolder.getStore();
try {
for (ModelActionHolder updateAction : modelActionHolders) {
switch(updateAction.getModel()) {
case Reference:
referenceRootHolder = applyModelUpdate(updateAction, referenceRootHolder).orElse(referenceRootHolder);
break;
case Running:
runningRootHolder = applyModelUpdate(updateAction, runningRootHolder).orElse(runningRootHolder);
break;
case Store:
storeRootHolder = applyModelUpdate(updateAction, storeRootHolder).orElse(storeRootHolder);
break;
}
}
} catch (Exception e) {
String message = String.format("Change action failure during model update for %s (%s)", referenceRootHolder.getId(), e.toString());
logger.warn(message, e);
engine.getTitusRuntime().getCodeInvariants().unexpectedError(message, e);
this.changeActionError = e;
this.transactionStep = TransactionStep.ChangeActionFailed;
return Optional.empty();
}
return Optional.of(new ModelHolder(referenceRootHolder, runningRootHolder, storeRootHolder));
}
use of com.netflix.titus.common.framework.reconciler.ModelActionHolder in project titus-control-plane by Netflix.
the class DefaultReconciliationFrameworkTest method testFailingMultiEngineChangeAction.
@Test
public void testFailingMultiEngineChangeAction() {
EntityHolder root1 = EntityHolder.newRoot("myRoot1", "myEntity1");
EntityHolder root2 = EntityHolder.newRoot("myRoot2", "myEntity2");
framework.newEngine(root1).subscribe();
framework.newEngine(root2).subscribe();
testScheduler.triggerActions();
Observable<Void> multiChangeObservable = framework.changeReferenceModel(// Keep anonymous class instead of lambda for readability
new MultiEngineChangeAction() {
@Override
public Observable<Map<String, List<ModelActionHolder>>> apply() {
return Observable.error(new RuntimeException("simulated error"));
}
}, // Keep anonymous class instead of lambda for readability
(id, modelUpdates) -> new ChangeAction() {
@Override
public Observable<List<ModelActionHolder>> apply() {
return modelUpdates;
}
}, "myRoot1", "myRoot2");
ExtTestSubscriber<Void> multiChangeSubscriber = new ExtTestSubscriber<>();
multiChangeObservable.subscribe(multiChangeSubscriber);
assertThat(multiChangeSubscriber.isError()).isTrue();
String errorMessage = ExceptionExt.toMessageChain(multiChangeSubscriber.getError());
assertThat(errorMessage).contains("simulated error");
}
use of com.netflix.titus.common.framework.reconciler.ModelActionHolder in project titus-control-plane by Netflix.
the class JobTransactionLogger method logJobModelUpdateErrorReconcilerEvent.
private static String logJobModelUpdateErrorReconcilerEvent(JobModelReconcilerEvent.JobModelUpdateErrorReconcilerEvent event) {
String jobId = event.getJob().getId();
String entityId = event.getPreviousEntityHolder().getId();
ModelActionHolder actionHolder = event.getModelActionHolder();
TitusModelAction action = (TitusModelAction) actionHolder.getAction();
return doFormat(jobId, event.getTransactionId(), "error", "modelUpdate/" + event.getModelActionHolder().getModel().name(), ((TitusModelAction) actionHolder.getAction()).getName(), event.getChangeAction().getTrigger(), toTargetName(jobId, entityId), entityId, 0, 0, event.getCallMetadata().getCallerId(), event.getCallMetadata().getCallReason(), action.getSummary());
}
use of com.netflix.titus.common.framework.reconciler.ModelActionHolder in project titus-control-plane by Netflix.
the class MoveTaskBetweenJobsAction method createModelUpdateActionsFrom.
private List<ModelActionHolder> createModelUpdateActionsFrom(Job<ServiceJobExt> updatedJobFrom, Job<ServiceJobExt> updatedJobTo, Task taskFrom, CallMetadata callMetadata) {
List<ModelActionHolder> actions = new ArrayList<>();
// Remove task from all models.
TitusModelAction removeTaskAction = TitusModelAction.newModelUpdate("moveTask").job(updatedJobFrom).trigger(Trigger.API).summary("Task moved to another job: jobTo=" + updatedJobTo.getId()).callMetadata(callMetadata).removeTask(taskFrom);
actions.addAll(ModelActionHolder.allModels(removeTaskAction));
String summary = "Decremented the desired job size by one, as its task was moved to another job: jobTo=" + updatedJobTo.getId();
// Change job size
TitusModelAction modelAction = TitusModelAction.newModelUpdate("decrementJobSize").job(updatedJobFrom).trigger(Trigger.API).summary(summary).jobUpdate(jobHolder -> jobHolder.setEntity(updatedJobFrom));
actions.addAll(ModelActionHolder.referenceAndStore(modelAction));
return actions;
}
Aggregations