use of com.netflix.titus.common.framework.reconciler.MultiEngineChangeAction 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.MultiEngineChangeAction 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.MultiEngineChangeAction in project titus-control-plane by Netflix.
the class DefaultReconciliationFrameworkTest method testMultiEngineChangeAction.
@Test
public void testMultiEngineChangeAction() {
EntityHolder root1 = EntityHolder.newRoot("myRoot1", "myEntity1");
EntityHolder root2 = EntityHolder.newRoot("myRoot2", "myEntity2");
framework.newEngine(root1).subscribe();
framework.newEngine(root2).subscribe();
testScheduler.triggerActions();
MultiEngineChangeAction multiEngineChangeAction = () -> Observable.just(ImmutableMap.of("myRoot1", ModelActionHolder.allModels(new SimpleModelUpdateAction(EntityHolder.newRoot("myRoot1", "myEntity1#v2"), true)), "myRoot2", ModelActionHolder.allModels(new SimpleModelUpdateAction(EntityHolder.newRoot("myRoot2", "myEntity2#v2"), true))));
Map<String, List<ModelActionHolder>> holders = new HashMap<>();
Observable<Void> multiChangeObservable = framework.changeReferenceModel(multiEngineChangeAction, (id, modelUpdates) -> {
ChangeAction changeAction = () -> modelUpdates.doOnNext(next -> holders.put(id, next));
return changeAction;
}, "myRoot1", "myRoot2");
verify(engine1, times(0)).changeReferenceModel(any());
verify(engine2, times(0)).changeReferenceModel(any());
ExtTestSubscriber<Void> multiChangeSubscriber = new ExtTestSubscriber<>();
multiChangeObservable.subscribe(multiChangeSubscriber);
assertThat(multiChangeSubscriber.isUnsubscribed()).isTrue();
verify(engine1, times(1)).changeReferenceModel(any());
verify(engine2, times(1)).changeReferenceModel(any());
// one action per view (Running, Store, Reference)
assertThat(holders.get("myRoot1")).hasSize(3);
SimpleModelUpdateAction modelAction1 = (SimpleModelUpdateAction) holders.get("myRoot1").get(0).getAction();
assertThat((String) modelAction1.getEntityHolder().getEntity()).isEqualTo("myEntity1#v2");
// one action per view (Running, Store, Reference)
assertThat(holders.get("myRoot2")).hasSize(3);
SimpleModelUpdateAction modelAction2 = (SimpleModelUpdateAction) holders.get("myRoot2").get(0).getAction();
assertThat((String) modelAction2.getEntityHolder().getEntity()).isEqualTo("myEntity2#v2");
}
use of com.netflix.titus.common.framework.reconciler.MultiEngineChangeAction in project titus-control-plane by Netflix.
the class DefaultReconciliationFrameworkTest method testMultiEngineChangeActionWithInvalidEngineId.
@Test
public void testMultiEngineChangeActionWithInvalidEngineId() {
EntityHolder root1 = EntityHolder.newRoot("myRoot1", "myEntity1");
framework.newEngine(root1).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 IllegalStateException("invocation not expected"));
}
}, // Keep anonymous class instead of lambda for readability
(id, modelUpdates) -> new ChangeAction() {
@Override
public Observable<List<ModelActionHolder>> apply() {
return Observable.error(new IllegalStateException("invocation not expected"));
}
}, "myRoot1", "badRootId");
ExtTestSubscriber<Void> multiChangeSubscriber = new ExtTestSubscriber<>();
multiChangeObservable.subscribe(multiChangeSubscriber);
assertThat(multiChangeSubscriber.isError()).isTrue();
assertThat(multiChangeSubscriber.getError().getMessage()).contains("badRootId");
}
Aggregations