Skip to content

Commit a53e687

Browse files
committed
Add actor state TTL support.
Signed-off-by: Artur Souza <[email protected]>
1 parent 363b6f9 commit a53e687

18 files changed

+487
-157
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/*
2+
* Copyright 2021 The Dapr Authors
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
* http://www.apache.org/licenses/LICENSE-2.0
7+
* Unless required by applicable law or agreed to in writing, software
8+
* distributed under the License is distributed on an "AS IS" BASIS,
9+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
* See the License for the specific language governing permissions and
11+
limitations under the License.
12+
*/
13+
14+
package io.dapr.actors.runtime;
15+
16+
import java.time.Instant;
17+
18+
/**
19+
* Represents a state change for an actor.
20+
*/
21+
final class ActorState<T> {
22+
23+
/**
24+
* Name of the state being changed.
25+
*/
26+
private final String name;
27+
28+
/**
29+
* New value for the state being changed.
30+
*/
31+
private final T value;
32+
33+
/**
34+
* Expiration.
35+
*/
36+
private final Instant expiration;
37+
38+
/**
39+
* Creates a new instance of the metadata on actor state.
40+
*
41+
* @param name Name of the state being changed.
42+
* @param value Value to be set.
43+
*/
44+
ActorState(String name, T value) {
45+
this(name, value, null);
46+
}
47+
48+
/**
49+
* Creates a new instance of the metadata on actor state.
50+
*
51+
* @param name Name of the state being changed.
52+
* @param value Value to be set.
53+
* @param expiration When the value is set to expire (recommended but accepts null).
54+
*/
55+
ActorState(String name, T value, Instant expiration) {
56+
this.name = name;
57+
this.value = value;
58+
this.expiration = expiration;
59+
}
60+
61+
/**
62+
* Gets the name of the state being changed.
63+
*
64+
* @return Name of the state.
65+
*/
66+
String getName() {
67+
return name;
68+
}
69+
70+
/**
71+
* Gets the new value of the state being changed.
72+
*
73+
* @return New value.
74+
*/
75+
T getValue() {
76+
return value;
77+
}
78+
79+
/**
80+
* Gets the expiration of the state.
81+
*
82+
* @return State expiration.
83+
*/
84+
Instant getExpiration() {
85+
return expiration;
86+
}
87+
88+
}

sdk-actors/src/main/java/io/dapr/actors/runtime/ActorStateChange.java

+9-25
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,9 @@
1919
public final class ActorStateChange {
2020

2121
/**
22-
* Name of the state being changed.
22+
* State being changed.
2323
*/
24-
private final String stateName;
25-
26-
/**
27-
* New value for the state being changed.
28-
*/
29-
private final Object value;
24+
private final ActorState state;
3025

3126
/**
3227
* Type of change {@link ActorStateChangeKind}.
@@ -36,32 +31,21 @@ public final class ActorStateChange {
3631
/**
3732
* Creates an actor state change.
3833
*
39-
* @param stateName Name of the state being changed.
40-
* @param value New value for the state being changed.
34+
* @param state State being changed.
4135
* @param changeKind Kind of change.
4236
*/
43-
ActorStateChange(String stateName, Object value, ActorStateChangeKind changeKind) {
44-
this.stateName = stateName;
45-
this.value = value;
37+
ActorStateChange(ActorState state, ActorStateChangeKind changeKind) {
38+
this.state = state;
4639
this.changeKind = changeKind;
4740
}
4841

4942
/**
50-
* Gets the name of the state being changed.
51-
*
52-
* @return Name of the state.
53-
*/
54-
String getStateName() {
55-
return stateName;
56-
}
57-
58-
/**
59-
* Gets the new value of the state being changed.
43+
* Gets the state being changed.
6044
*
61-
* @return New value.
45+
* @return state.
6246
*/
63-
Object getValue() {
64-
return value;
47+
ActorState getState() {
48+
return state;
6549
}
6650

6751
/**

sdk-actors/src/main/java/io/dapr/actors/runtime/ActorStateManager.java

+64-17
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@
1717
import io.dapr.utils.TypeRef;
1818
import reactor.core.publisher.Mono;
1919

20+
import java.time.Duration;
21+
import java.time.Instant;
2022
import java.util.ArrayList;
23+
import java.util.Date;
2124
import java.util.List;
2225
import java.util.Map;
2326
import java.util.NoSuchElementException;
@@ -66,12 +69,13 @@ public class ActorStateManager {
6669
/**
6770
* Adds a given key/value to the Actor's state store's cache.
6871
*
69-
* @param stateName Name of the state being added.
70-
* @param value Value to be added.
71-
* @param <T> Type of the object being added.
72+
* @param stateName Name of the state being added.
73+
* @param value Value to be added.
74+
* @param expiration State's expiration.
75+
* @param <T> Type of the object being added.
7276
* @return Asynchronous void operation.
7377
*/
74-
public <T> Mono<Void> add(String stateName, T value) {
78+
public <T> Mono<Void> add(String stateName, T value, Instant expiration) {
7579
return Mono.fromSupplier(() -> {
7680
if (stateName == null) {
7781
throw new IllegalArgumentException("State's name cannot be null.");
@@ -84,7 +88,8 @@ public <T> Mono<Void> add(String stateName, T value) {
8488
StateChangeMetadata metadata = this.stateChangeTracker.get(stateName);
8589

8690
if (metadata.kind == ActorStateChangeKind.REMOVE) {
87-
this.stateChangeTracker.put(stateName, new StateChangeMetadata(ActorStateChangeKind.UPDATE, value));
91+
this.stateChangeTracker.put(
92+
stateName, new StateChangeMetadata(ActorStateChangeKind.UPDATE, value, expiration));
8893
return true;
8994
}
9095

@@ -95,7 +100,8 @@ public <T> Mono<Void> add(String stateName, T value) {
95100
throw new IllegalStateException("Duplicate state: " + stateName);
96101
}
97102

98-
this.stateChangeTracker.put(stateName, new StateChangeMetadata(ActorStateChangeKind.ADD, value));
103+
this.stateChangeTracker.put(
104+
stateName, new StateChangeMetadata(ActorStateChangeKind.ADD, value, expiration));
99105
return true;
100106
}))
101107
.then();
@@ -130,6 +136,10 @@ public <T> Mono<T> get(String stateName, TypeRef<T> type) {
130136
if (this.stateChangeTracker.containsKey(stateName)) {
131137
StateChangeMetadata metadata = this.stateChangeTracker.get(stateName);
132138

139+
if (metadata.isExpired()) {
140+
throw new NoSuchElementException("State is expired: " + stateName);
141+
}
142+
133143
if (metadata.kind == ActorStateChangeKind.REMOVE) {
134144
throw new NoSuchElementException("State is marked for removal: " + stateName);
135145
}
@@ -142,20 +152,37 @@ public <T> Mono<T> get(String stateName, TypeRef<T> type) {
142152
this.stateProvider.load(this.actorTypeName, this.actorId, stateName, type)
143153
.switchIfEmpty(Mono.error(new NoSuchElementException("State not found: " + stateName)))
144154
.map(v -> {
145-
this.stateChangeTracker.put(stateName, new StateChangeMetadata(ActorStateChangeKind.NONE, v));
146-
return (T) v;
155+
this.stateChangeTracker.put(
156+
stateName, new StateChangeMetadata(ActorStateChangeKind.NONE, v.getValue(), v.getExpiration()));
157+
return (T) v.getValue();
147158
}));
148159
}
149160

150161
/**
151162
* Updates a given key/value pair in the state store's cache.
163+
* Use the variation that takes in an TTL instead.
152164
*
153165
* @param stateName Name of the state being updated.
154166
* @param value Value to be set for given state.
155167
* @param <T> Type of the value being set.
156168
* @return Asynchronous void result.
157169
*/
170+
@Deprecated
158171
public <T> Mono<Void> set(String stateName, T value) {
172+
return this.set(stateName, value, Duration.ZERO);
173+
}
174+
175+
/**
176+
* Updates a given key/value pair in the state store's cache.
177+
* Using TTL is highly recommended to avoid state to be left in the state store forever.
178+
*
179+
* @param stateName Name of the state being updated.
180+
* @param value Value to be set for given state.
181+
* @param ttl Time to live.
182+
* @param <T> Type of the value being set.
183+
* @return Asynchronous void result.
184+
*/
185+
public <T> Mono<Void> set(String stateName, T value, Duration ttl) {
159186
return Mono.fromSupplier(() -> {
160187
if (stateName == null) {
161188
throw new IllegalArgumentException("State's name cannot be null.");
@@ -165,20 +192,23 @@ public <T> Mono<Void> set(String stateName, T value) {
165192
StateChangeMetadata metadata = this.stateChangeTracker.get(stateName);
166193

167194
ActorStateChangeKind kind = metadata.kind;
168-
if ((kind == ActorStateChangeKind.NONE) || (kind == ActorStateChangeKind.REMOVE)) {
195+
if (metadata.isExpired() || (kind == ActorStateChangeKind.NONE) || (kind == ActorStateChangeKind.REMOVE)) {
169196
kind = ActorStateChangeKind.UPDATE;
170197
}
171198

172-
this.stateChangeTracker.put(stateName, new StateChangeMetadata(kind, value));
199+
var expiration = buildExpiration(ttl);
200+
this.stateChangeTracker.put(stateName, new StateChangeMetadata(kind, value, expiration));
173201
return true;
174202
}
175203

176204
return false;
177205
}).filter(x -> x)
178206
.switchIfEmpty(this.stateProvider.contains(this.actorTypeName, this.actorId, stateName)
179207
.map(exists -> {
208+
var expiration = buildExpiration(ttl);
180209
this.stateChangeTracker.put(stateName,
181-
new StateChangeMetadata(exists ? ActorStateChangeKind.UPDATE : ActorStateChangeKind.ADD, value));
210+
new StateChangeMetadata(
211+
exists ? ActorStateChangeKind.UPDATE : ActorStateChangeKind.ADD, value, expiration));
182212
return exists;
183213
}))
184214
.then();
@@ -208,7 +238,7 @@ public Mono<Void> remove(String stateName) {
208238
return true;
209239
}
210240

211-
this.stateChangeTracker.put(stateName, new StateChangeMetadata(ActorStateChangeKind.REMOVE, null));
241+
this.stateChangeTracker.put(stateName, new StateChangeMetadata(ActorStateChangeKind.REMOVE, null, null));
212242
return true;
213243
}
214244

@@ -218,7 +248,7 @@ public Mono<Void> remove(String stateName) {
218248
.switchIfEmpty(this.stateProvider.contains(this.actorTypeName, this.actorId, stateName))
219249
.filter(exists -> exists)
220250
.map(exists -> {
221-
this.stateChangeTracker.put(stateName, new StateChangeMetadata(ActorStateChangeKind.REMOVE, null));
251+
this.stateChangeTracker.put(stateName, new StateChangeMetadata(ActorStateChangeKind.REMOVE, null, null));
222252
return exists;
223253
})
224254
.then();
@@ -239,7 +269,7 @@ public Mono<Boolean> contains(String stateName) {
239269
return this.stateChangeTracker.get(stateName);
240270
}
241271
).map(metadata -> {
242-
if (metadata.kind == ActorStateChangeKind.REMOVE) {
272+
if (metadata.isExpired() || (metadata.kind == ActorStateChangeKind.REMOVE)) {
243273
return Boolean.FALSE;
244274
}
245275

@@ -264,7 +294,8 @@ public Mono<Void> save() {
264294
continue;
265295
}
266296

267-
changes.add(new ActorStateChange(tuple.getKey(), tuple.getValue().value, tuple.getValue().kind));
297+
var actorState = new ActorState<>(tuple.getKey(), tuple.getValue().value, tuple.getValue().expiration);
298+
changes.add(new ActorStateChange(actorState, tuple.getValue().kind));
268299
}
269300

270301
return changes.toArray(new ActorStateChange[0]);
@@ -288,12 +319,17 @@ private void flush() {
288319
if (tuple.getValue().kind == ActorStateChangeKind.REMOVE) {
289320
this.stateChangeTracker.remove(stateName);
290321
} else {
291-
StateChangeMetadata metadata = new StateChangeMetadata(ActorStateChangeKind.NONE, tuple.getValue().value);
322+
StateChangeMetadata metadata =
323+
new StateChangeMetadata(ActorStateChangeKind.NONE, tuple.getValue().value, tuple.getValue().expiration);
292324
this.stateChangeTracker.put(stateName, metadata);
293325
}
294326
}
295327
}
296328

329+
private static Instant buildExpiration(Duration ttl) {
330+
return (ttl != null) && !ttl.isNegative() && !ttl.isZero() ? Instant.now().plus(ttl) : null;
331+
}
332+
297333
/**
298334
* Internal class to represent value and change kind.
299335
*/
@@ -309,15 +345,26 @@ private static final class StateChangeMetadata {
309345
*/
310346
private final Object value;
311347

348+
/**
349+
* Expiration.
350+
*/
351+
private final Instant expiration;
352+
312353
/**
313354
* Creates a new instance of the metadata on state change.
314355
*
315356
* @param kind Kind of change.
316357
* @param value Value to be set.
358+
* @param expiration When the value is set to expire (recommended but accepts null).
317359
*/
318-
private StateChangeMetadata(ActorStateChangeKind kind, Object value) {
360+
private StateChangeMetadata(ActorStateChangeKind kind, Object value, Instant expiration) {
319361
this.kind = kind;
320362
this.value = value;
363+
this.expiration = expiration;
364+
}
365+
366+
private boolean isExpired() {
367+
return (this.expiration != null) && Instant.now().isAfter(this.expiration);
321368
}
322369
}
323370
}

0 commit comments

Comments
 (0)