Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Kill dispatching exceptions #1104

Merged
merged 114 commits into from
Jul 10, 2019
Merged

Kill dispatching exceptions #1104

merged 114 commits into from
Jul 10, 2019

Conversation

dmdashenkov
Copy link
Contributor

@dmdashenkov dmdashenkov commented Jun 28, 2019

Before this PR, all the errors caused by message dispatching were thrown as exceptions. Exception handling was distributed between various onError methods as well as between different levels of abstraction, from buses to transactions.

Now, all the dispatching errors are converted into system events. Users may choose to subscribe to those events for diagnostics and recovery.

System Events

There are 4 new types of system events:

  1. HandlerFailedUnexpectedly is the most common one. It is emitted at any generic error which occurs during a message handling, including runtime exceptions in handler methods, errors in the framework code, and errors in integrating user code and the framework code. Non-entity constraint violations also lead to HandlerFailedUnexpectedly events (while entity constraint violations cause ConstraintViolated events to be emitted).
  2. RoutingFailed event is emitted when the user code which routes messages to entity instances fails with an exception. The main difference between this event and HandlerFailedUnexpectedly is that the framework does not know the ID of the target entity; thus, the data included in the event payload is a bit different.
  3. CannotDispatchEventTwice and 4. CannotDispatchCommandTwice are events emitted when a duplicate event or command is dispatched to an entity. These events can be emitted at the Inbox level, i.e. before the delivery.

Message Propagation

Previously, when propagating a message through the system, the control flow heavily relied on the exceptions to be thrown when something does wrong. Now, a method dispatching a message would not throw an exception even if dispatching fails. That's why we introduce PropagationOutcome, a structure which aggregates an outcome of a message propagation, successful or otherwise.

A successful propagation may result in events, a rejection, or commands. Those results are posted into the respective buses on the MessageEndpoint level. Previously, commands used to be posted from within the entity level. Now, they are processed side-by-side with the events.

An unsuccessful propagation is described with a spine.core.Error. Errors are converted into system events and posted into the system Event Bus.

When many events are processed in a single transaction, one of them may fail and halt processing. Events which were not sent for processing because another event had failed result in interrupted outcomes.

Some messages may be forced on an entity by measures beyond normal propagation (such as catch-up processes). If an event does not belong to an entity, the entity may ignore it. Such processing is reflected in the PropagationOutcome as the ignored state.

MessageDispatcher contract

For quite a long time, MessageDispathcers returned the IDs of the target entities after dispatching a message. Those values were never used in production code and sometimes had to be artificially manufactured (e.g. in abstract message handlers). Now, MessageDispatcher.dispatch does not return anything.

MessageDispatcher.onError was deleted, since all the errors are now broadcast via system Event Bus.

@dmdashenkov dmdashenkov self-assigned this Jun 28, 2019
@codecov
Copy link

codecov bot commented Jul 1, 2019

Codecov Report

Merging #1104 into master will decrease coverage by 1.21%.
The diff coverage is 87.76%.

@@             Coverage Diff              @@
##             master    #1104      +/-   ##
============================================
- Coverage        92%   90.78%   -1.22%     
+ Complexity     3993     3928      -65     
============================================
  Files           539      540       +1     
  Lines         12850    12865      +15     
  Branches        711      723      +12     
============================================
- Hits          11822    11679     -143     
- Misses          807      959     +152     
- Partials        221      227       +6

@dmdashenkov dmdashenkov marked this pull request as ready for review July 9, 2019 16:26
@dmdashenkov
Copy link
Contributor Author

@armiol, @alexander-yevsyukov, PTAL.

I'm planning on improving the coverage in further PRs.

@armiol
Copy link
Contributor

armiol commented Jul 9, 2019

@dmdashenkov please rephrase this one:

 The events which were not processed because of the premature handling abortion result in the interrupted outcomes.

Copy link
Contributor

@alexander-yevsyukov alexander-yevsyukov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The names CannotDispatchEventTwice and CannotDispatchCommandTwice sounds as if we want dispatch a signal twice but cannot do that because of some reason. Please think of names that reflect the fact that we do not want double delivery.

@alexander-yevsyukov
Copy link
Contributor

alexander-yevsyukov commented Jul 10, 2019

PropagationOutcome also needs renaming. We already have a good common word for commands and events — Signal. Why don't we use it in this context? How about SignalOutcome or SignalResult, or SignalDispatchingResult?

If we also need to include entity states into this, it could be just DispatchingResult.

“Propagation” is not correct word for this context. We do not want this process be seen as creating more and more messages of the kind.

Copy link
Contributor

@alexander-yevsyukov alexander-yevsyukov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please see my comments.

@@ -135,7 +131,7 @@ protected BoundedContext(BoundedContextBuilder builder) {
*/
protected final void init() {
eventBus.init(this);
tenantIndex.registerWith(this);
tenantIndex.init(this);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This reads as if TenantIndex initializes the `BoundedContext.

@@ -201,7 +197,7 @@ public static BoundedContextBuilder multitenant(String name) {
public <I, E extends Entity<I, ?>> void register(Repository<I, E> repository) {
checkNotNull(repository);
Security.allowOnlyFrameworkServer();
repository.injectContext(this);
initialize(repository);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about just init()?

* @param context
* the Context to which this instance belongs
*/
void init(BoundedContext context);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Once again, it would be better to name it injectContext(), or register(), or (got forbid) setContext(). It would read better on the usage side.

@@ -71,7 +72,7 @@ private void checkNotAlreadyRegistered(Class<? extends Message> stateClass) {
if (alreadyRegistered != null) {
throw newIllegalStateException(
"A repository for the state class %s already registered: %s",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add backticks for the code related parts of the message and the period at the end of the sentence.

Copy link
Contributor

@alexander-yevsyukov alexander-yevsyukov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please see my comments.

* If the given {@code contextPart} is {@link ContextAware},
* {@linkplain ContextAware#registerWith registers} it with this context.
*/
private void registerWithThis(Object contextPart) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not just register()?

@@ -71,7 +71,7 @@ private void checkNotAlreadyRegistered(Class<? extends Message> stateClass) {
RepositoryAccess alreadyRegistered = repositories.get(stateClass);
if (alreadyRegistered != null) {
throw newIllegalStateException(
"A repository for the state class %s already registered: %s",
"A repository for the state class %s already registered: `%s`",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please finish with period.

@@ -33,7 +33,7 @@

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

final class PropagationProcess

Don't we need to rename this class too?

@@ -36,7 +36,7 @@ import "spine/core/event.proto";

// Report of a batch propagation of several signals.
//
message Propagation {
message BatchDispatch {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please rename to BatchDispatchOutcome.

@@ -36,7 +36,7 @@ import "spine/core/event.proto";

// Report of a batch propagation of several signals.
//
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove the empty line.

EmptyClass,
MethodResult<Empty>> {
EmptyClass>
implements NonProducingMethod<EventSubscriber, EventClass, EventEnvelope> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The name NonProducingMethod is a bit weird. VoidMethod may not be much better, but at least shorter.

import io.spine.core.ActorContext;
import io.spine.core.Command;
import io.spine.server.entity.ProducedCommands;
import io.spine.server.entity.Success;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Success does not seem to belong to entity package. Yes, most of the dispatching end up in entities, but we also have CommandHandler and EventHandler.

MessageEnvelope<?, ?, ?> handledSignal) {
if (result != null) {
String errorMessage = format(
"Method `%s` should not produce any result. Produced: %s",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please finish with dot.

/**
* A {@link io.spine.server.command.Commander Commander} which retries the task
* creation if it fails.
*/
public final class CreationRetry extends AbstractCommander {

private static final Set<TaskId> rejectedTasks = new HashSet<>();
private static final Set<TaskId> rejectedTasks = newHashSet();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Google considers these static factory method as deprecated since Java 7.

@dmdashenkov
Copy link
Contributor Author

@alexander-yevsyukov, PTAL again. Let's discuss the leftover comments vocally.

Copy link
Contributor

@alexander-yevsyukov alexander-yevsyukov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@dmdashenkov dmdashenkov merged commit 37ad9e7 into master Jul 10, 2019
@dmdashenkov dmdashenkov deleted the kill-exceptions branch July 10, 2019 20:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants