Skip to content

Commit 69dd272

Browse files
Merge pull request #1353 from SpineEventEngine/fix-empty-dispatching-delivery
Fix empty dispatching delivery
2 parents 967fd9a + 0383a6b commit 69dd272

File tree

12 files changed

+374
-40
lines changed

12 files changed

+374
-40
lines changed

.idea/codeStyles/Project.xml

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

server/src/main/java/io/spine/server/aggregate/AggregateEndpoint.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ public final void dispatchTo(I aggregateId) {
9090

9191
private void storeAndPost(A aggregate, DispatchOutcome outcome) {
9292
Success success = outcome.getSuccess();
93-
if (success.hasProducedEvents()) {
93+
if (success.hasEvents()) {
9494
store(aggregate);
9595
List<Event> events = success.getProducedEvents()
9696
.getEventList();
@@ -126,7 +126,7 @@ private A loadOrCreate(I aggregateId) {
126126
final DispatchOutcome handleAndApplyEvents(A aggregate) {
127127
DispatchOutcome outcome = invokeDispatcher(aggregate);
128128
Success successfulOutcome = outcome.getSuccess();
129-
return successfulOutcome.hasProducedEvents()
129+
return successfulOutcome.hasEvents()
130130
? applyProducedEvents(aggregate, outcome)
131131
: outcome;
132132
}

server/src/main/java/io/spine/server/dispatch/SuccessMixin.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,4 +49,24 @@ default Object readValue(FieldDescriptor field) {
4949
return getField(field);
5050
}
5151
}
52+
53+
/**
54+
* Determines if the outcome has any produced events.
55+
*
56+
* @implNote Prefer using this method over the generated {@code hasProducedEvents}
57+
* while the latter only checks if the message is set.
58+
*/
59+
default boolean hasEvents() {
60+
return hasProducedEvents() && getProducedEvents().getEventCount() > 0;
61+
}
62+
63+
/**
64+
* Determines if the outcome has any produced commands.
65+
*
66+
* @implNote Prefer using this method over the generated {@code hasProducedCommands}
67+
* while the latter only checks if the message is set.
68+
*/
69+
default boolean hasCommands() {
70+
return hasProducedCommands() && getProducedCommands().getCommandCount() > 0;
71+
}
5272
}

server/src/test/java/io/spine/server/aggregate/AggregateTest.java

Lines changed: 73 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import io.spine.core.Ack;
3838
import io.spine.core.Command;
3939
import io.spine.core.Event;
40+
import io.spine.core.EventContext;
4041
import io.spine.core.MessageId;
4142
import io.spine.core.TenantId;
4243
import io.spine.server.BoundedContext;
@@ -50,6 +51,11 @@
5051
import io.spine.server.aggregate.given.aggregate.TaskAggregateRepository;
5152
import io.spine.server.aggregate.given.aggregate.TestAggregate;
5253
import io.spine.server.aggregate.given.aggregate.TestAggregateRepository;
54+
import io.spine.server.aggregate.given.thermometer.SafeThermometer;
55+
import io.spine.server.aggregate.given.thermometer.SafeThermometerRepo;
56+
import io.spine.server.aggregate.given.thermometer.Thermometer;
57+
import io.spine.server.aggregate.given.thermometer.ThermometerId;
58+
import io.spine.server.aggregate.given.thermometer.event.TemperatureChanged;
5359
import io.spine.server.commandbus.CommandBus;
5460
import io.spine.server.delivery.MessageEndpoint;
5561
import io.spine.server.dispatch.BatchDispatchOutcome;
@@ -81,7 +87,7 @@
8187
import io.spine.test.aggregate.rejection.Rejections.AggCannotReassignUnassignedTask;
8288
import io.spine.testing.logging.MuteLogging;
8389
import io.spine.testing.server.EventSubject;
84-
import io.spine.testing.server.blackbox.BlackBoxContext;
90+
import io.spine.testing.server.blackbox.ContextAwareTest;
8591
import io.spine.testing.server.model.ModelTests;
8692
import io.spine.time.testing.TimeTests;
8793
import org.junit.jupiter.api.AfterEach;
@@ -317,12 +323,13 @@ void writeVersionIntoEventContext() {
317323
dispatchCommand(aggregate, command(createProject));
318324

319325
// Get the first event since the command handler produces only one event message.
320-
Aggregate<?, ?, ?> agg = this.aggregate;
321-
List<Event> uncommittedEvents = agg.getUncommittedEvents().list();
326+
Aggregate<?, ?, ?> agg = aggregate;
327+
List<Event> uncommittedEvents = agg.getUncommittedEvents()
328+
.list();
322329
Event event = uncommittedEvents.get(0);
323-
324-
assertEquals(this.aggregate.version(), event.context()
325-
.getVersion());
330+
EventContext context = event.context();
331+
assertThat(aggregate.version())
332+
.isEqualTo(context.getVersion());
326333
}
327334

328335
@Test
@@ -719,7 +726,8 @@ void throughNewestEventsFirst() {
719726

720727
private ProtoSubject assertNextCommandId() {
721728
Event event = history.next();
722-
return assertThat(event.rootMessage().asCommandId());
729+
return assertThat(event.rootMessage()
730+
.asCommandId());
723731
}
724732

725733
@Test
@@ -818,27 +826,20 @@ void checkEventsUponHistory() {
818826
private static void dispatch(TenantId tenant,
819827
Supplier<MessageEndpoint<ProjectId, ?>> endpoint) {
820828
with(tenant).run(
821-
() -> endpoint.get().dispatchTo(ID)
829+
() -> endpoint.get()
830+
.dispatchTo(ID)
822831
);
823832
}
824833

825834
@Nested
826835
@DisplayName("create a single event when emitting a pair without second value")
827-
class CreateSingleEventForPair {
828-
829-
private BlackBoxContext context;
830-
831-
@BeforeEach
832-
void prepareContext() {
833-
context = BlackBoxContext.from(
834-
BoundedContextBuilder.assumingTests()
835-
.add(new TaskAggregateRepository())
836-
);
837-
}
836+
class CreateSingleEventForPair extends ContextAwareTest {
838837

839-
@AfterEach
840-
void closeContext() {
841-
context.close();
838+
@Override
839+
protected BoundedContextBuilder contextBuilder() {
840+
return BoundedContextBuilder
841+
.assumingTests()
842+
.add(new TaskAggregateRepository());
842843
}
843844

844845
/**
@@ -852,10 +853,10 @@ void closeContext() {
852853
@Test
853854
@DisplayName("when dispatching a command")
854855
void fromCommandDispatch() {
855-
context.receivesCommand(createTask())
856-
.assertEvents()
857-
.withType(AggTaskCreated.class)
858-
.isNotEmpty();
856+
context().receivesCommand(createTask())
857+
.assertEvents()
858+
.withType(AggTaskCreated.class)
859+
.isNotEmpty();
859860
}
860861

861862
/**
@@ -870,8 +871,8 @@ void fromCommandDispatch() {
870871
@Test
871872
@DisplayName("when reacting on an event")
872873
void fromEventReact() {
873-
EventSubject assertEvents = context.receivesCommand(assignTask())
874-
.assertEvents();
874+
EventSubject assertEvents = context().receivesCommand(assignTask())
875+
.assertEvents();
875876
assertEvents.hasSize(2);
876877
assertEvents.withType(AggTaskAssigned.class)
877878
.hasSize(1);
@@ -891,13 +892,55 @@ void fromEventReact() {
891892
@Test
892893
@DisplayName("when reacting on a rejection")
893894
void fromRejectionReact() {
894-
EventSubject assertEvents = context.receivesCommand(reassignTask())
895-
.assertEvents();
895+
EventSubject assertEvents = context().receivesCommand(reassignTask())
896+
.assertEvents();
896897
assertEvents.hasSize(2);
897898
assertEvents.withType(AggCannotReassignUnassignedTask.class)
898899
.hasSize(1);
899900
assertEvents.withType(AggUserNotified.class)
900901
.hasSize(1);
901902
}
902903
}
904+
905+
@Nested
906+
@DisplayName("allow having validation on the aggregate state and")
907+
class AllowValidatedAggregates extends ContextAwareTest {
908+
909+
private final ThermometerId thermometer = ThermometerId.generate();
910+
911+
@Override
912+
protected BoundedContextBuilder contextBuilder() {
913+
return BoundedContextBuilder
914+
.assumingTests()
915+
.add(new SafeThermometerRepo(thermometer));
916+
}
917+
918+
@Test
919+
@DisplayName("not change the Aggregate state when there is no reaction on the event")
920+
void notChangeStateIfNoReaction() {
921+
TemperatureChanged booksOnFire =
922+
TemperatureChanged.newBuilder()
923+
.setFahrenheit(451)
924+
.vBuild();
925+
context().receivesExternalEvent(booksOnFire)
926+
.assertEntity(thermometer, SafeThermometer.class)
927+
.doesNotExist();
928+
}
929+
930+
@Test
931+
@DisplayName("save valid aggregate state on change")
932+
void safelySaveValidState() {
933+
TemperatureChanged gettingWarmer =
934+
TemperatureChanged.newBuilder()
935+
.setFahrenheit(72)
936+
.vBuild();
937+
context().receivesExternalEvent(gettingWarmer);
938+
Thermometer expected = Thermometer
939+
.newBuilder()
940+
.setId(thermometer)
941+
.setFahrenheit(72)
942+
.vBuild();
943+
context().assertState(thermometer, expected);
944+
}
945+
}
903946
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/*
2+
* Copyright 2021, TeamDev. All rights reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Redistribution and use in source and/or binary forms, with or without
11+
* modification, must retain the above copyright notice and the following
12+
* disclaimer.
13+
*
14+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
15+
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
16+
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
17+
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
18+
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
19+
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
20+
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
21+
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
22+
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
23+
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
24+
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
25+
*/
26+
27+
package io.spine.server.aggregate.given.thermometer;
28+
29+
import io.spine.core.External;
30+
import io.spine.server.aggregate.Aggregate;
31+
import io.spine.server.aggregate.Apply;
32+
import io.spine.server.aggregate.given.thermometer.event.TemperatureChanged;
33+
import io.spine.server.aggregate.given.thermometer.event.TermTemperatureChanged;
34+
import io.spine.server.event.React;
35+
36+
import java.util.Optional;
37+
38+
/**
39+
* Ignores temperature changes outside its own range.
40+
*/
41+
public final class SafeThermometer extends Aggregate<ThermometerId, Thermometer, Thermometer.Builder> {
42+
43+
private static final double MIN = 0;
44+
private static final double MAX = 120;
45+
46+
@React
47+
Optional<TermTemperatureChanged> on(@External TemperatureChanged e) {
48+
double temperature = e.getFahrenheit();
49+
if (!withinBounds(temperature)) {
50+
return Optional.empty();
51+
}
52+
return Optional.of(
53+
TermTemperatureChanged
54+
.newBuilder()
55+
.setThermometer(id())
56+
.setChange(
57+
TemperatureChange
58+
.newBuilder()
59+
.setNewValue(temperature)
60+
.setPreviousValue(state().getFahrenheit())
61+
)
62+
.vBuild()
63+
);
64+
}
65+
66+
private static boolean withinBounds(double temperature) {
67+
return temperature > MIN && temperature < MAX;
68+
}
69+
70+
@Apply
71+
private void on(TermTemperatureChanged e) {
72+
builder().setFahrenheit(e.getChange()
73+
.getNewValue());
74+
}
75+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/*
2+
* Copyright 2021, TeamDev. All rights reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Redistribution and use in source and/or binary forms, with or without
11+
* modification, must retain the above copyright notice and the following
12+
* disclaimer.
13+
*
14+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
15+
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
16+
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
17+
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
18+
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
19+
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
20+
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
21+
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
22+
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
23+
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
24+
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
25+
*/
26+
27+
package io.spine.server.aggregate.given.thermometer;
28+
29+
import io.spine.server.aggregate.AggregateRepository;
30+
import io.spine.server.aggregate.given.thermometer.event.TemperatureChanged;
31+
import io.spine.server.route.EventRouting;
32+
33+
import static io.spine.util.Preconditions2.checkNotDefaultArg;
34+
35+
/**
36+
* A {@link SafeThermometer thermometer} repository.
37+
*/
38+
public final class SafeThermometerRepo extends AggregateRepository<ThermometerId, SafeThermometer> {
39+
40+
private final ThermometerId thermometer;
41+
42+
/**
43+
* Creates a new repository for the {@code thermometer}.
44+
*/
45+
public SafeThermometerRepo(ThermometerId thermometer) {
46+
this.thermometer = checkNotDefaultArg(thermometer);
47+
}
48+
49+
@Override
50+
protected void setupEventRouting(EventRouting<ThermometerId> routing) {
51+
routing.unicast(TemperatureChanged.class, (e) -> thermometer);
52+
}
53+
}

0 commit comments

Comments
 (0)