dapr / dapr/go-sdk

Actor state manager does not invalidate tracker after `SetWithTTL`

Open
#406 3 comments 0 reactions 0 assignees View on GitHub
bug
Dominant language
Go
Stars
479
Forks
187
PR merge metrics
No merged PRs in 30d

Description

**Describe the bug**

In the actor state store, `Get`/`Contains` return invalid data after saving a state with `SetWithTTL` and waiting for TTL time.

`t.GetStateManager().SetWithTTL` introduced by #383 is able to save the actual data into the state store with the correct TTL.

Here is the screenshot from my Redis DB showing a TTL on the right side.
image

However, the internal state manager state is likely corrupted. On calling `Contains` or `Get` after the TTL has expired, it still returns the old key. This might be possibly due to the fact that in actor/state/state_manager.go, we update the `stateChangeTracker` when `SetWithTTL` is called, and in the `Get` or `Contains` method, we only check for `Remove`d state, and never look at the TTL. This cache is not invalidated when the TTL expires.

**To Reproduce**

Here is the test plan
```go
func (t *TestActor) TestTTL(ctx context.Context, stateKey string) error {
fmt.Println("test ttl with key = ", stateKey)

stateValue := "foobar"

// 1. Set state with TTL 15 seconds
if err := t.GetStateManager().SetWithTTL(ctx, stateKey, stateValue, time.Second*15); err != nil {
fmt.Println("[ERROR] STEP 1: state manager set get with key " + stateKey + " and state value " + stateValue + "err = " + err.Error())
return err
}

if err := t.GetStateManager().Save(ctx); err != nil {
fmt.Println("[ERROR] STEP 1: state manager save err = " + err.Error())
return err
}

// 2. Get state with key, it should exist and value should be stateValue
if exist, err := t.GetStateManager().Contains(ctx, stateKey); err != nil {
fmt.Println("[ERROR] STEP 2: state manager call contains with key " + stateKey + " err = " + err.Error())
return err
} else if !exist {
fmt.Println("[ERROR] STEP 2: state manager call contains with key " + stateKey + " does not exist")
return fmt.Errorf("state with key %s should exist", stateKey)
} else {
var stateData string
if err := t.GetStateManager().Get(ctx, stateKey, &stateData); err != nil {
fmt.Println("[ERROR] STEP 2: state manager call get with key " + stateKey + " err = " + err.Error())
return err
}
if stateData != stateValue {
fmt.Println("[ERROR] STEP 2: state manager call get with key " + stateKey + " value = " + stateData + " does not match expected value = " + stateValue)
return fmt.Errorf("state with key %s should have value %s, but got %s", stateKey, stateValue, stateData)
} else {
fmt.Println("[INFO] STEP 2: state manager call get with key " + stateKey + " value = " + stateData + " matches expected value = " + stateValue)
}
}

// 3. Wait for 20 seconds, state should be expired
fmt.Println("[INFO] STEP 3: wait for 20 seconds")
time.Sleep(time.Second * 20)

// 4. Get state with key, it should not exist
if exist, err := t.GetStateManager().Contains(ctx, stateKey); err != nil {
fmt.Println("[ERROR] STEP 4: state manager call contains with key " + stateKey + " err = " + err.Error())
return err
} else if exist {
fmt.Println("[ERROR] STEP 4: state manager call contains with key " + stateKey + " exist")
return fmt.Errorf("state with key %s should not exist", stateKey)
} else {
fmt.Println("[INFO] STEP 4: state manager call contains with key " + stateKey + " does not exist")
}

return nil
}
```

Output:
```
== APP == test ttl with key = testStateKey
== APP == [INFO] STEP 2: state manager call get with key testStateKey value = foobar matches expected value = foobar
== APP == [INFO] STEP 3: wait for 20 seconds
== APP == [ERROR] STEP 4: state manager call contains with key testStateKey exist
```

**Expected behavior**

`Get`/`Contains` should not return the data if its TTL has expired.

Contributor guide

Open the contributing guide

Research direction

Start in actor/state/state_manager.go and follow how SetWithTTL updates the stateChangeTracker, then reproduce the issue's SetWithTTL, Save, wait, and Contains sequence. Done means Get and Contains no longer report the state after its TTL has expired, while they still return it before expiration.

Written by the indexing model from the issue text.

Assessment

Tech stack
go
Domain
backend, distributed-systems
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.