Skip to content

Commit 9c1d20c

Browse files
authored
fix(spring-sdk): avoid exception when using currentState in VE after persistence (#1291)
1 parent e6181e1 commit 9c1d20c

File tree

4 files changed

+164
-3
lines changed

4 files changed

+164
-3
lines changed

sdk/spring-sdk/src/it/java/com/example/wiring/SpringSdkIntegrationTest.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -448,4 +448,7 @@ public TestUser(String id, String email, String name) {
448448
public TestUser withName(String newName) {
449449
return new TestUser(id, email, newName);
450450
}
451+
public TestUser withEmail(String newEmail) {
452+
return new TestUser(id, newEmail, name);
453+
}
451454
}
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
/*
2+
* Copyright 2021 Lightbend Inc.
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+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.example.wiring;
18+
19+
import com.example.Main;
20+
import com.example.wiring.valueentities.user.User;
21+
import kalix.springsdk.KalixConfigurationTest;
22+
import org.junit.jupiter.api.Assertions;
23+
import org.junit.jupiter.api.Test;
24+
import org.junit.jupiter.api.extension.ExtendWith;
25+
import org.springframework.beans.factory.annotation.Autowired;
26+
import org.springframework.boot.test.context.SpringBootTest;
27+
import org.springframework.context.annotation.Import;
28+
import org.springframework.test.context.TestPropertySource;
29+
import org.springframework.test.context.junit.jupiter.SpringExtension;
30+
import org.springframework.web.reactive.function.client.WebClient;
31+
32+
import java.time.Duration;
33+
34+
import static java.time.temporal.ChronoUnit.SECONDS;
35+
import static org.junit.jupiter.api.Assertions.fail;
36+
37+
@ExtendWith(SpringExtension.class)
38+
@SpringBootTest(classes = Main.class)
39+
@Import(KalixConfigurationTest.class)
40+
@TestPropertySource(properties = "spring.main.allow-bean-definition-overriding=true")
41+
public class ValueEntityIntegrationTest {
42+
43+
@Autowired
44+
private WebClient webClient;
45+
46+
private Duration timeout = Duration.of(10, SECONDS);
47+
48+
@Test
49+
public void verifyValueEntityCurrentState() {
50+
51+
var joe1 = new TestUser("veUser1", "[email protected]", "veJane");
52+
createUser(joe1);
53+
54+
var newEmail = joe1.email + "2";
55+
// change email uses the currentState internally
56+
changeEmail(joe1.withEmail(newEmail));
57+
58+
Assertions.assertEquals(newEmail, getUser(joe1).email);
59+
}
60+
61+
@Test
62+
public void verifyValueEntityCurrentStateAfterRestart() {
63+
64+
var joe2 = new TestUser("veUser2", "[email protected]", "veJane");
65+
createUser(joe2);
66+
67+
restartUserEntity(joe2);
68+
69+
var newEmail = joe2.email + "2";
70+
// change email uses the currentState internally
71+
changeEmail(joe2.withEmail(newEmail));
72+
73+
Assertions.assertEquals(newEmail, getUser(joe2).email);
74+
}
75+
76+
private void createUser(TestUser user) {
77+
String userCreation =
78+
webClient
79+
.post()
80+
.uri("/user/" + user.id + "/" + user.email + "/" + user.name)
81+
.retrieve()
82+
.bodyToMono(String.class)
83+
.block(timeout);
84+
Assertions.assertEquals("\"Ok\"", userCreation);
85+
}
86+
87+
private void changeEmail(TestUser user) {
88+
String userCreation =
89+
webClient
90+
.patch()
91+
.uri("/user/" + user.id + "/email/" + user.email)
92+
.retrieve()
93+
.bodyToMono(String.class)
94+
.block(timeout);
95+
Assertions.assertEquals("\"Ok from patch\"", userCreation);
96+
}
97+
98+
private User getUser(TestUser user) {
99+
return webClient
100+
.get()
101+
.uri("/user/" + user.id)
102+
.retrieve()
103+
.bodyToMono(User.class)
104+
.block(timeout);
105+
}
106+
107+
private void restartUserEntity(TestUser user) {
108+
try {
109+
webClient
110+
.post()
111+
.uri("/user/" + user.id +"/restart")
112+
.retrieve()
113+
.bodyToMono(Integer.class)
114+
.block(timeout);
115+
fail("This should not be reached");
116+
} catch (Exception ignored) { }
117+
}
118+
}

sdk/spring-sdk/src/it/java/com/example/wiring/valueentities/user/UserEntity.java

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,22 @@
1717
package com.example.wiring.valueentities.user;
1818

1919
import io.grpc.Status;
20+
import kalix.javasdk.eventsourcedentity.EventSourcedEntity;
2021
import kalix.javasdk.valueentity.ValueEntity;
2122
import kalix.javasdk.valueentity.ValueEntityContext;
2223
import kalix.springsdk.annotations.EntityKey;
2324
import kalix.springsdk.annotations.EntityType;
25+
import org.slf4j.Logger;
26+
import org.slf4j.LoggerFactory;
2427
import org.springframework.web.bind.annotation.*;
2528

2629
@EntityKey("id")
2730
@EntityType("user")
2831
@RequestMapping("/user/{id}")
2932
public class UserEntity extends ValueEntity<User> {
3033

34+
private Logger logger = LoggerFactory.getLogger(getClass());
35+
3136
private final ValueEntityContext context;
3237

3338
public UserEntity(ValueEntityContext context) {
@@ -53,12 +58,23 @@ public Effect<String> createUser(@PathVariable String email, @PathVariable Strin
5358
}
5459

5560
@PatchMapping("/email/{email}")
56-
public Effect<String> createUser(@PathVariable String email) {
61+
public Effect<String> updateEmail(@PathVariable String email) {
5762
return effects().updateState(new User(email, currentState().name)).thenReply("Ok from patch");
5863
}
5964

6065
@DeleteMapping
6166
public Effect<String> deleteUser() {
6267
return effects().deleteState().thenReply("Ok from delete");
6368
}
69+
70+
@PostMapping("/restart")
71+
public EventSourcedEntity.Effect<Integer> restart() { // force entity restart, useful for testing
72+
logger.info(
73+
"Restarting counter with commandId={} commandName={} current={}",
74+
commandContext().commandId(),
75+
commandContext().commandName(),
76+
currentState());
77+
78+
throw new RuntimeException("Forceful restarting entity!");
79+
}
6480
}

sdk/spring-sdk/src/main/scala/kalix/springsdk/impl/valueentity/ReflectiveValueEntityRouter.scala

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,15 @@
1717
package kalix.springsdk.impl.valueentity
1818

1919
import com.google.protobuf.any.{ Any => ScalaPbAny }
20+
import kalix.javasdk.JsonSupport
2021
import kalix.javasdk.impl.valueentity.ValueEntityRouter
2122
import kalix.javasdk.valueentity.CommandContext
2223
import kalix.javasdk.valueentity.ValueEntity
2324
import kalix.springsdk.impl.CommandHandler
2425
import kalix.springsdk.impl.InvocationContext
2526

27+
import java.lang.reflect.ParameterizedType
28+
2629
class ReflectiveValueEntityRouter[S, E <: ValueEntity[S]](
2730
override protected val entity: E,
2831
commandHandlers: Map[String, CommandHandler])
@@ -37,8 +40,7 @@ class ReflectiveValueEntityRouter[S, E <: ValueEntity[S]](
3740
command: Any,
3841
commandContext: CommandContext): ValueEntity.Effect[_] = {
3942

40-
// pass current state to entity
41-
entity._internalSetCurrentState(state)
43+
_extractAndSetCurrentState(state)
4244

4345
val commandHandler = commandHandlerLookup(commandName)
4446
val invocationContext =
@@ -54,4 +56,26 @@ class ReflectiveValueEntityRouter[S, E <: ValueEntity[S]](
5456
.invoke(entity, invocationContext)
5557
.asInstanceOf[ValueEntity.Effect[_]]
5658
}
59+
60+
private def _extractAndSetCurrentState(state: S): Unit = {
61+
val entityStateType: Class[S] =
62+
this.entity.getClass.getGenericSuperclass
63+
.asInstanceOf[ParameterizedType]
64+
.getActualTypeArguments
65+
.head
66+
.asInstanceOf[Class[S]]
67+
68+
// the state: S received can either be of the entity "state" type (if coming from emptyState/memory)
69+
// or PB Any type (if coming from the proxy)
70+
state match {
71+
case s if s == null || state.getClass == entityStateType =>
72+
// note that we set the state even if null, this is needed in order to
73+
// be able to call currentState() later
74+
entity._internalSetCurrentState(s)
75+
case s =>
76+
val deserializedState =
77+
JsonSupport.decodeJson(entityStateType, ScalaPbAny.toJavaProto(s.asInstanceOf[ScalaPbAny]))
78+
entity._internalSetCurrentState(deserializedState)
79+
}
80+
}
5781
}

0 commit comments

Comments
 (0)