Skip to content

Commit 31ccb5f

Browse files
Conformance Tests: State- ttlExpireTime (#2863)
Signed-off-by: joshvanl <[email protected]> Signed-off-by: Alessandro (Ale) Segala <[email protected]> Co-authored-by: Alessandro (Ale) Segala <[email protected]>
1 parent b10ce96 commit 31ccb5f

File tree

2 files changed

+204
-26
lines changed

2 files changed

+204
-26
lines changed

state/etcd/etcd.go

Lines changed: 17 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -192,38 +192,29 @@ func (e *Etcd) doSet(ctx context.Context, key string, val any, etag *string, ttl
192192
return err
193193
}
194194

195+
var leaseID clientv3.LeaseID
195196
if ttlInSeconds != nil {
196-
resp, err := e.client.Grant(ctx, *ttlInSeconds)
197+
var resp *clientv3.LeaseGrantResponse
198+
resp, err = e.client.Grant(ctx, *ttlInSeconds)
197199
if err != nil {
198200
return fmt.Errorf("couldn't grant lease %s: %w", key, err)
199201
}
200-
if etag != nil {
201-
etag, _ := strconv.ParseInt(*etag, 10, 64)
202-
_, err = e.client.Txn(ctx).
203-
If(clientv3.Compare(clientv3.ModRevision(key), "=", etag)).
204-
Then(clientv3.OpPut(key, reqVal, clientv3.WithLease(resp.ID))).
205-
Commit()
206-
} else {
207-
_, err = e.client.Put(ctx, key, reqVal, clientv3.WithLease(resp.ID))
208-
}
209-
if err != nil {
210-
return fmt.Errorf("couldn't set key %s: %w", key, err)
211-
}
202+
leaseID = resp.ID
203+
}
204+
205+
if etag != nil {
206+
etag, _ := strconv.ParseInt(*etag, 10, 64)
207+
_, err = e.client.Txn(ctx).
208+
If(clientv3.Compare(clientv3.ModRevision(key), "=", etag)).
209+
Then(clientv3.OpPut(key, reqVal, clientv3.WithLease(leaseID))).
210+
Commit()
212211
} else {
213-
var err error
214-
if etag != nil {
215-
etag, _ := strconv.ParseInt(*etag, 10, 64)
216-
_, err = e.client.Txn(ctx).
217-
If(clientv3.Compare(clientv3.ModRevision(key), "=", etag)).
218-
Then(clientv3.OpPut(key, reqVal)).
219-
Commit()
220-
} else {
221-
_, err = e.client.Put(ctx, key, reqVal)
222-
}
223-
if err != nil {
224-
return fmt.Errorf("couldn't set key %s: %w", key, err)
225-
}
212+
_, err = e.client.Put(ctx, key, reqVal, clientv3.WithLease(leaseID))
213+
}
214+
if err != nil {
215+
return fmt.Errorf("couldn't set key %s: %w", key, err)
226216
}
217+
227218
return nil
228219
}
229220

tests/conformance/state/state.go

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -990,6 +990,16 @@ func ConformanceTests(t *testing.T, props map[string]string, statestore state.St
990990
}
991991

992992
if config.HasOperation("ttl") {
993+
t.Run("set ttl with bad value should error", func(t *testing.T) {
994+
require.Error(t, statestore.Set(context.Background(), &state.SetRequest{
995+
Key: key + "-ttl",
996+
Value: "⏱️",
997+
Metadata: map[string]string{
998+
"ttlInSeconds": "foo",
999+
},
1000+
}))
1001+
})
1002+
9931003
t.Run("set and get with TTL", func(t *testing.T) {
9941004
// Check if ttl feature is listed
9951005
features := statestore.Features()
@@ -1031,6 +1041,183 @@ func ConformanceTests(t *testing.T, props map[string]string, statestore state.St
10311041
features := statestore.Features()
10321042
require.False(t, state.FeatureTTL.IsPresent(features))
10331043
})
1044+
1045+
t.Run("no TTL should not return any expire time", func(t *testing.T) {
1046+
err := statestore.Set(context.Background(), &state.SetRequest{
1047+
Key: key + "-no-ttl",
1048+
Value: "⏱️",
1049+
Metadata: map[string]string{},
1050+
})
1051+
require.NoError(t, err)
1052+
1053+
// Request immediately
1054+
res, err := statestore.Get(context.Background(), &state.GetRequest{Key: key + "-no-ttl"})
1055+
require.NoError(t, err)
1056+
assertEquals(t, "⏱️", res)
1057+
1058+
assert.NotContains(t, res.Metadata, "ttlExpireTime")
1059+
})
1060+
1061+
t.Run("ttlExpireTime", func(t *testing.T) {
1062+
if !config.HasOperation("transaction") {
1063+
// This test is only for state stores that support transactions
1064+
return
1065+
}
1066+
1067+
unsupported := []string{
1068+
"redis.v6",
1069+
"redis.v7",
1070+
"etcd.v1",
1071+
}
1072+
1073+
for _, noSup := range unsupported {
1074+
if strings.Contains(config.ComponentName, noSup) {
1075+
t.Skipf("skipping test for unsupported state store %s", noSup)
1076+
}
1077+
}
1078+
1079+
t.Run("set and get expire time", func(t *testing.T) {
1080+
now := time.Now()
1081+
err := statestore.Set(context.Background(), &state.SetRequest{
1082+
Key: key + "-ttl-expire-time",
1083+
Value: "⏱️",
1084+
Metadata: map[string]string{
1085+
// Expire in an hour.
1086+
"ttlInSeconds": "3600",
1087+
},
1088+
})
1089+
require.NoError(t, err)
1090+
1091+
// Request immediately
1092+
res, err := statestore.Get(context.Background(), &state.GetRequest{
1093+
Key: key + "-ttl-expire-time",
1094+
})
1095+
require.NoError(t, err)
1096+
assertEquals(t, "⏱️", res)
1097+
1098+
require.Containsf(t, res.Metadata, "ttlExpireTime", "expected metadata to contain ttlExpireTime")
1099+
expireTime, err := time.Parse(time.RFC3339, res.Metadata["ttlExpireTime"])
1100+
require.NoError(t, err)
1101+
assert.InDelta(t, now.Add(time.Hour).UnixMilli(), expireTime.UnixMilli(), float64(time.Minute*10))
1102+
})
1103+
1104+
t.Run("ttl set to -1 should remove the TTL of a state store key", func(t *testing.T) {
1105+
req := func(meta map[string]string) *state.SetRequest {
1106+
return &state.SetRequest{
1107+
Key: key + "-ttl-expire-time-minus-1",
1108+
Value: "⏱️",
1109+
Metadata: meta,
1110+
}
1111+
}
1112+
1113+
require.NoError(t, statestore.Set(context.Background(), req(map[string]string{
1114+
// Expire in 2 seconds.
1115+
"ttlInSeconds": "2",
1116+
})))
1117+
1118+
// Request immediately
1119+
res, err := statestore.Get(context.Background(), &state.GetRequest{
1120+
Key: key + "-ttl-expire-time-minus-1",
1121+
})
1122+
require.NoError(t, err)
1123+
assertEquals(t, "⏱️", res)
1124+
assert.Contains(t, res.Metadata, "ttlExpireTime")
1125+
1126+
// Remove TTL by setting a value of -1.
1127+
require.NoError(t, statestore.Set(context.Background(), req(map[string]string{
1128+
"ttlInSeconds": "-1",
1129+
})))
1130+
res, err = statestore.Get(context.Background(), &state.GetRequest{
1131+
Key: key + "-ttl-expire-time-minus-1",
1132+
})
1133+
require.NoError(t, err)
1134+
assertEquals(t, "⏱️", res)
1135+
assert.NotContains(t, res.Metadata, "ttlExpireTime")
1136+
1137+
// Ensure that the key is not expired after previous TTL.
1138+
time.Sleep(3 * time.Second)
1139+
1140+
res, err = statestore.Get(context.Background(), &state.GetRequest{
1141+
Key: key + "-ttl-expire-time-minus-1",
1142+
})
1143+
require.NoError(t, err)
1144+
assertEquals(t, "⏱️", res)
1145+
1146+
// Set a new TTL.
1147+
require.NoError(t, statestore.Set(context.Background(), req(map[string]string{
1148+
"ttlInSeconds": "2",
1149+
})))
1150+
res, err = statestore.Get(context.Background(), &state.GetRequest{
1151+
Key: key + "-ttl-expire-time-minus-1",
1152+
})
1153+
require.NoError(t, err)
1154+
assertEquals(t, "⏱️", res)
1155+
assert.Contains(t, res.Metadata, "ttlExpireTime")
1156+
1157+
// Remove TTL by omitting the ttlInSeconds field.
1158+
require.NoError(t, statestore.Set(context.Background(), req(map[string]string{})))
1159+
res, err = statestore.Get(context.Background(), &state.GetRequest{
1160+
Key: key + "-ttl-expire-time-minus-1",
1161+
})
1162+
require.NoError(t, err)
1163+
assertEquals(t, "⏱️", res)
1164+
assert.NotContains(t, res.Metadata, "ttlExpireTime")
1165+
1166+
// Ensure key is not expired after previous TTL.
1167+
time.Sleep(3 * time.Second)
1168+
res, err = statestore.Get(context.Background(), &state.GetRequest{
1169+
Key: key + "-ttl-expire-time-minus-1",
1170+
})
1171+
require.NoError(t, err)
1172+
assertEquals(t, "⏱️", res)
1173+
assert.NotContains(t, res.Metadata, "ttlExpireTime")
1174+
})
1175+
1176+
t.Run("set and get expire time bulkGet", func(t *testing.T) {
1177+
now := time.Now()
1178+
require.NoError(t, statestore.Set(context.Background(), &state.SetRequest{
1179+
Key: key + "-ttl-expire-time-bulk-1",
1180+
Value: "123",
1181+
Metadata: map[string]string{"ttlInSeconds": "3600"},
1182+
}))
1183+
1184+
require.NoError(t, statestore.Set(context.Background(), &state.SetRequest{
1185+
Key: key + "-ttl-expire-time-bulk-2",
1186+
Value: "234",
1187+
Metadata: map[string]string{"ttlInSeconds": "3600"},
1188+
}))
1189+
1190+
// Request immediately
1191+
res, err := statestore.BulkGet(context.Background(), []state.GetRequest{
1192+
{Key: key + "-ttl-expire-time-bulk-1"},
1193+
{Key: key + "-ttl-expire-time-bulk-2"},
1194+
}, state.BulkGetOpts{})
1195+
require.NoError(t, err)
1196+
1197+
require.Len(t, res, 2)
1198+
sort.Slice(res, func(i, j int) bool {
1199+
return res[i].Key < res[j].Key
1200+
})
1201+
1202+
assert.Equal(t, key+"-ttl-expire-time-bulk-1", res[0].Key)
1203+
assert.Equal(t, key+"-ttl-expire-time-bulk-2", res[1].Key)
1204+
assert.Equal(t, []byte(`"123"`), res[0].Data)
1205+
assert.Equal(t, []byte(`"234"`), res[1].Data)
1206+
1207+
for i := range res {
1208+
if config.HasOperation("transaction") {
1209+
require.Containsf(t, res[i].Metadata, "ttlExpireTime", "expected metadata to contain ttlExpireTime")
1210+
expireTime, err := time.Parse(time.RFC3339, res[i].Metadata["ttlExpireTime"])
1211+
require.NoError(t, err)
1212+
// Check the expire time is returned and is in a 10 minute window. This
1213+
// window should be _more_ than enough.
1214+
assert.InDelta(t, now.Add(time.Hour).UnixMilli(), expireTime.UnixMilli(), float64(time.Minute*10))
1215+
} else {
1216+
assert.NotContains(t, res[i].Metadata, "ttlExpireTime")
1217+
}
1218+
}
1219+
})
1220+
})
10341221
}
10351222
}
10361223

0 commit comments

Comments
 (0)