Skip to content

Commit e60650d

Browse files
Merge pull request #1343 from SpineEventEngine/improve-signals-dispatching
Improve signals dispatching
2 parents 29e6531 + 4bd38de commit e60650d

File tree

9 files changed

+194
-84
lines changed

9 files changed

+194
-84
lines changed

server/src/main/java/io/spine/server/event/model/EventReceivingClassDelegate.java

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@
4646
*/
4747
@Immutable(containerOf = "M")
4848
public class EventReceivingClassDelegate<T extends EventReceiver,
49-
P extends MessageClass<?>,
50-
M extends HandlerMethod<?, EventClass, ?, P>>
49+
P extends MessageClass<?>,
50+
M extends HandlerMethod<?, EventClass, ?, P>>
5151
extends ModelClass<T> {
5252

5353
private static final long serialVersionUID = 0L;
@@ -120,19 +120,18 @@ public ImmutableSet<P> producedTypes() {
120120

121121
/**
122122
* Obtains the method which handles the passed event class.
123-
*
124-
* @throws IllegalStateException if there is such method in the class
125123
*/
126-
public ImmutableSet<M> handlersOf(EventClass eventClass, MessageClass originClass) {
124+
public ImmutableSet<M> handlersOf(EventClass eventClass, MessageClass<?> originClass) {
127125
return handlers.handlersOf(eventClass, originClass);
128126
}
129127

130128
/**
131129
* Obtains the method which handles the passed event class.
132130
*
133-
* @throws IllegalStateException if there is such method in the class
131+
* @throws IllegalStateException
132+
* if there is no such method in the class
134133
*/
135-
public M handlerOf(EventClass eventClass, MessageClass originClass) {
134+
public M handlerOf(EventClass eventClass, MessageClass<?> originClass) {
136135
return handlers.handlerOf(eventClass, originClass);
137136
}
138137

@@ -146,9 +145,10 @@ private ImmutableSet<StateClass> extractStates(boolean external) {
146145
}
147146
ImmutableSet<M> stateHandlers = handlers.handlersOf(updateEvent);
148147
ImmutableSet<StateClass> result =
149-
stateHandlers.stream()
150-
.filter(h -> h instanceof StateSubscriberMethod)
151-
.map(h -> (StateSubscriberMethod) h)
148+
stateHandlers
149+
.stream()
150+
.filter(StateSubscriberMethod.class::isInstance)
151+
.map(StateSubscriberMethod.class::cast)
152152
.filter(external ? HandlerMethod::isExternal : HandlerMethod::isDomestic)
153153
.map(StateSubscriberMethod::stateType)
154154
.map(StateClass::from)

server/src/main/java/io/spine/server/model/HandlerMap.java

Lines changed: 15 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@
3737
import java.util.function.Predicate;
3838

3939
import static com.google.common.base.Preconditions.checkNotNull;
40-
import static com.google.common.base.Preconditions.checkState;
4140
import static com.google.common.collect.ImmutableSet.toImmutableSet;
4241
import static com.google.common.collect.Iterables.getOnlyElement;
4342
import static io.spine.server.model.MethodScan.findMethodsBy;
@@ -86,8 +85,7 @@ HandlerMap<M, P, H> create(Class<?> declaringClass, MethodSignature<H, ?> signat
8685
return new HandlerMap<>(map, messageClasses);
8786
}
8887

89-
private HandlerMap(ImmutableSetMultimap<DispatchKey, H> map,
90-
ImmutableSet<M> messageClasses) {
88+
private HandlerMap(ImmutableSetMultimap<DispatchKey, H> map, ImmutableSet<M> messageClasses) {
9189
this.map = map;
9290
this.messageClasses = messageClasses;
9391
}
@@ -131,35 +129,18 @@ public ImmutableSet<R> producedTypes() {
131129
return result;
132130
}
133131

134-
/**
135-
* Obtains the method for handling by the passed key.
136-
*
137-
* @param key
138-
* the key of the handler to get
139-
* @return a handler method
140-
* @throws IllegalStateException
141-
* if there is no method found in the map
142-
*/
143-
private ImmutableSet<H> handlersOf(DispatchKey key) {
144-
ImmutableSet<H> handlers = map.get(key);
145-
checkState(!handlers.isEmpty(),
146-
"Unable to find handler with the key: %s.", key);
147-
return handlers;
148-
}
149-
150132
/**
151133
* Obtains methods for handling messages of the given class and with the given origin.
152134
*
153135
* <p>If there is no handler matching both the message and origin class, handlers will be
154136
* searched by the message class only.
155137
*
138+
* <p>If no handlers for a specified criteria is found, returns an empty set.
139+
*
156140
* @param messageClass
157141
* the message class of the handled message
158142
* @param originClass
159143
* the class of the message, from which the handled message is originate
160-
* @return a handler method
161-
* @throws IllegalStateException
162-
* if there is no method found in the map
163144
*/
164145
public ImmutableSet<H> handlersOf(M messageClass, MessageClass<?> originClass) {
165146
DispatchKey key =
@@ -171,7 +152,8 @@ public ImmutableSet<H> handlersOf(M messageClass, MessageClass<?> originClass) {
171152
DispatchKey presentKey = map.containsKey(key)
172153
? key
173154
: new DispatchKey(messageClass.value(), null, null);
174-
return handlersOf(presentKey);
155+
ImmutableSet<H> handlers = map.get(presentKey);
156+
return handlers;
175157
}
176158

177159
/**
@@ -180,16 +162,15 @@ public ImmutableSet<H> handlersOf(M messageClass, MessageClass<?> originClass) {
180162
* <p>If there is no handler matching both the message and origin class, a handler will be
181163
* searched by the message class only.
182164
*
183-
* <p>If there is no such method or several such methods, an {@link IllegalStateException} is
184-
* thrown.
185-
*
186165
* @param messageClass
187166
* the message class of the handled message
188167
* @return a handler method
189168
* @throws IllegalStateException
190-
* if there is no such method or several such methods found in the map
169+
* if no handler methods were found
170+
* @throws DuplicateHandlerMethodError
171+
* if multiple handler methods were found
191172
*/
192-
public H handlerOf(M messageClass, MessageClass originClass) {
173+
public H handlerOf(M messageClass, MessageClass<?> originClass) {
193174
ImmutableSet<H> methods = handlersOf(messageClass, originClass);
194175
return singleMethod(methods, messageClass);
195176
}
@@ -200,8 +181,6 @@ public H handlerOf(M messageClass, MessageClass originClass) {
200181
* @param messageClass
201182
* the message class of the handled message
202183
* @return handlers method
203-
* @throws IllegalStateException
204-
* if there is no method found in the map
205184
*/
206185
public ImmutableSet<H> handlersOf(M messageClass) {
207186
return handlersOf(messageClass, EmptyClass.instance());
@@ -217,7 +196,9 @@ public ImmutableSet<H> handlersOf(M messageClass) {
217196
* the message class of the handled message
218197
* @return a handler method
219198
* @throws IllegalStateException
220-
* if there is no such method or several such methods found in the map
199+
* if no handler methods were found
200+
* @throws DuplicateHandlerMethodError
201+
* if multiple handler methods were found
221202
*/
222203
public H handlerOf(M messageClass) {
223204
ImmutableSet<H> methods = handlersOf(messageClass);
@@ -233,11 +214,9 @@ private H singleMethod(Collection<H> handlers, M targetType) {
233214
private void checkSingle(Collection<H> handlers, M targetType) {
234215
int count = handlers.size();
235216
if (count == 0) {
236-
_error().log("No handler method found for the type `%s`.", targetType);
237-
throw newIllegalStateException(
238-
"Unexpected number of handlers for messages of class %s: %d.%n%s",
239-
targetType, count, handlers
240-
);
217+
String error = String.format("No handler method found for the type `%s`.", targetType);
218+
_error().log(error);
219+
throw newIllegalStateException(error);
241220
} else if (count > 1) {
242221
/*
243222
The map should have found all the duplicates during construction.

server/src/main/java/io/spine/server/model/MethodScan.java

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@
2828
import java.lang.reflect.Method;
2929
import java.util.HashMap;
3030
import java.util.Map;
31-
import java.util.Optional;
3231

3332
/**
3433
* A class method scan operation.
@@ -86,11 +85,8 @@ private ImmutableSetMultimap<DispatchKey, H> perform() {
8685
}
8786

8887
private void scanMethod(Method method) {
89-
Optional<H> handlerMethod = signature.classify(method);
90-
if (handlerMethod.isPresent()) {
91-
H handler = handlerMethod.get();
92-
remember(handler);
93-
}
88+
signature.classify(method)
89+
.ifPresent(this::remember);
9490
}
9591

9692
private void remember(H handler) {

server/src/main/java/io/spine/server/projection/Projection.java

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import io.spine.base.EntityState;
2525
import io.spine.base.Error;
2626
import io.spine.core.Event;
27+
import io.spine.core.EventValidationError;
2728
import io.spine.protobuf.ValidatingBuilder;
2829
import io.spine.server.dispatch.BatchDispatchOutcome;
2930
import io.spine.server.dispatch.DispatchOutcome;
@@ -130,10 +131,12 @@ DispatchOutcome apply(EventEnvelope event) {
130131
private DispatchOutcome unhandledEvent(EventEnvelope event) {
131132
Error error = Error
132133
.newBuilder()
134+
.setType(EventValidationError.getDescriptor().getFullName())
133135
.setCode(UNSUPPORTED_EVENT_VALUE)
134-
.setMessage(format("Projection %s cannot handle event %s.",
135-
thisClass(),
136-
event.messageTypeName()))
136+
.setMessage(format(
137+
"Projection `%s` cannot handle event `%s`.",
138+
thisClass(), event.messageTypeName()
139+
))
137140
.buildPartial();
138141
return DispatchOutcome
139142
.newBuilder()

server/src/test/java/io/spine/server/model/HandlerMapTest.java

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,23 +20,30 @@
2020

2121
package io.spine.server.model;
2222

23+
import io.spine.base.Identifier;
24+
import io.spine.model.contexts.projects.ProjectId;
25+
import io.spine.model.contexts.projects.command.SigCreateProject;
26+
import io.spine.server.BoundedContextBuilder;
2327
import io.spine.server.command.model.CommandHandlerSignature;
2428
import io.spine.server.model.given.map.DupEventFilterValue;
2529
import io.spine.server.model.given.map.DupEventFilterValueWhere;
2630
import io.spine.server.model.given.map.DuplicateCommandHandlers;
31+
import io.spine.server.model.given.map.RejectionsDispatchingTestEnv;
2732
import io.spine.server.model.given.map.TwoFieldsInSubscription;
2833
import io.spine.server.model.given.method.OneParamSignature;
2934
import io.spine.server.model.given.method.StubHandler;
3035
import io.spine.server.type.EventClass;
3136
import io.spine.string.StringifierRegistry;
3237
import io.spine.string.Stringifiers;
3338
import io.spine.test.event.ProjectStarred;
39+
import io.spine.testing.server.blackbox.ContextAwareTest;
3440
import org.junit.jupiter.api.BeforeAll;
3541
import org.junit.jupiter.api.DisplayName;
3642
import org.junit.jupiter.api.Nested;
3743
import org.junit.jupiter.api.Test;
3844

3945
import static io.spine.server.projection.model.ProjectionClass.asProjectionClass;
46+
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
4047
import static org.junit.jupiter.api.Assertions.assertThrows;
4148

4249
@DisplayName("`HandlerMap` should")
@@ -97,4 +104,30 @@ void failIfNotFound() {
97104
assertThrows(IllegalStateException.class,
98105
() -> map.handlerOf(EventClass.from(ProjectStarred.class)));
99106
}
107+
108+
@Nested
109+
@DisplayName("gracefully handle a missing handler for a rejection with a specific origin")
110+
class SpecificRejection extends ContextAwareTest {
111+
112+
@Override
113+
protected BoundedContextBuilder contextBuilder() {
114+
return BoundedContextBuilder
115+
.assumingTests()
116+
.add(RejectionsDispatchingTestEnv.ProjectAgg.class)
117+
.addEventDispatcher(new RejectionsDispatchingTestEnv.CompletionWatch());
118+
}
119+
120+
@Test
121+
void handleThrownRejectionGracefully() {
122+
ProjectId project = ProjectId
123+
.newBuilder()
124+
.setId(Identifier.newUuid())
125+
.vBuild();
126+
SigCreateProject createProject = SigCreateProject
127+
.newBuilder()
128+
.setId(project)
129+
.vBuild();
130+
assertDoesNotThrow(() -> context().receivesCommand(createProject));
131+
}
132+
}
100133
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/*
2+
* Copyright 2020, TeamDev. All rights reserved.
3+
*
4+
* Redistribution and use in source and/or binary forms, with or without
5+
* modification, must retain the above copyright notice and the following
6+
* disclaimer.
7+
*
8+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
9+
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
10+
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
11+
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
12+
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
13+
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
14+
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
15+
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
16+
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
17+
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
18+
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
19+
*/
20+
21+
package io.spine.server.model.given.map;
22+
23+
import io.spine.core.Subscribe;
24+
import io.spine.model.contexts.projects.Project;
25+
import io.spine.model.contexts.projects.ProjectId;
26+
import io.spine.model.contexts.projects.command.SigCreateProject;
27+
import io.spine.model.contexts.projects.command.SigStartProject;
28+
import io.spine.model.contexts.projects.event.SigProjectCreated;
29+
import io.spine.model.contexts.projects.event.SigProjectStarted;
30+
import io.spine.model.contexts.projects.rejection.ProjectRejections;
31+
import io.spine.model.contexts.projects.rejection.SigProjectAlreadyCompleted;
32+
import io.spine.server.aggregate.Aggregate;
33+
import io.spine.server.aggregate.Apply;
34+
import io.spine.server.command.Assign;
35+
import io.spine.server.event.AbstractEventSubscriber;
36+
37+
public final class RejectionsDispatchingTestEnv {
38+
39+
private RejectionsDispatchingTestEnv() {
40+
}
41+
42+
/**
43+
* A test aggregate that throws {@link SigProjectAlreadyCompleted} rejection for any
44+
* handled command.
45+
*/
46+
public static final class ProjectAgg extends Aggregate<ProjectId, Project, Project.Builder> {
47+
48+
@Assign
49+
SigProjectStarted handle(SigStartProject c) throws SigProjectAlreadyCompleted {
50+
throw SigProjectAlreadyCompleted
51+
.newBuilder()
52+
.setProject(c.getId())
53+
.build();
54+
}
55+
56+
@Assign
57+
SigProjectCreated handle(SigCreateProject c) throws SigProjectAlreadyCompleted {
58+
throw SigProjectAlreadyCompleted
59+
.newBuilder()
60+
.setProject(c.getId())
61+
.build();
62+
}
63+
64+
@Apply
65+
private void on(SigProjectStarted e) {
66+
// do nothing.
67+
}
68+
69+
@Apply
70+
private void on(SigProjectCreated e) {
71+
// do nothing.
72+
}
73+
}
74+
75+
/**
76+
* A test environment for the rejection subscriptions tests.
77+
*
78+
* <p>The subscriber is interested in the rejection thrown upon trying to
79+
* {@linkplain SigStartProject start} the project, but not in rejections thrown when the project
80+
* is just being {@linkplain SigCreateProject created}.
81+
*/
82+
public static final class CompletionWatch extends AbstractEventSubscriber {
83+
84+
@Subscribe
85+
void on(ProjectRejections.SigProjectAlreadyCompleted rejection, SigStartProject cmd) {
86+
// do nothing.
87+
}
88+
}
89+
}

0 commit comments

Comments
 (0)