Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 11 additions & 11 deletions internal/clients/state/v3/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,22 +166,22 @@ func (c *Client) SetKey(ctx context.Context, key string, value string) error {
c.mutex.Lock()
defer c.mutex.Unlock()

scopes, pending := c.changes[key]
if c.mem[key].value == value {
// the pending change summarizes this caller's resources too
if pending && c.checkpoints != nil {
scopes.add(checkpoint.FromContext(ctx))
changed := c.mem[key].value != value
if changed {
c.mem[key] = versionedValue{
value: value,
version: c.mem[key].version + 1,
}
return nil // don't update if the value is the same
}
c.mem[key] = versionedValue{
value: value,
version: c.mem[key].version + 1,
}
if c.checkpoints == nil {
c.changes[key] = nil
if changed {
c.changes[key] = nil
}
return nil
}
// with checkpoints, even an unchanged value covers the caller's resources, so later checkpoints of the key must
// follow them
scopes, pending := c.changes[key]
if !pending {
scopes = newScopeSet()
c.changes[key] = scopes
Expand Down
56 changes: 56 additions & 0 deletions scheduler/checkpoint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -936,3 +936,59 @@ func TestCheckpointsDroppedOnCancelAreAborted(t *testing.T) {
require.True(t, sent.Aborted())
})
}

// gatedScope holds the checkpoints passed to it until released, as a table resolver's scope holds them until the rows
// it produced are sent.
type gatedScope struct {
mu sync.Mutex
held []message.SyncMessage
}

func (g *gatedScope) Checkpoint(_ context.Context, msg message.SyncMessage) error {
g.mu.Lock()
defer g.mu.Unlock()
g.held = append(g.held, msg)
return nil
}

func (g *gatedScope) release(ctx context.Context, res chan<- message.SyncMessage) error {
g.mu.Lock()
held := g.held
g.held = nil
g.mu.Unlock()
for _, msg := range held {
if err := checkpoint.Deliver(ctx, res, msg); err != nil {
return err
}
}
return nil
}

func TestUnchangedStateValueOrdersLaterCheckpointsAfterTheCallersResources(t *testing.T) {
ctx := context.Background()
res := make(chan message.SyncMessage, 10)
stateClient := newCheckpointStateClient(t, res)
sent := func() []string {
var values []string
for len(res) > 0 {
values = append(values, stateValues(t, message.SyncMessages{<-res})["cursor"])
}
return values
}

require.NoError(t, stateClient.SetKey(ctx, "cursor", "1"))
require.NoError(t, stateClient.Flush(ctx))
require.Equal(t, []string{"1"}, sent())

// the same value, set by a resolver whose rows are not sent yet: it covers them too
unsent := &gatedScope{}
resolverCtx := checkpoint.WithScope(ctx, unsent)
require.NoError(t, stateClient.SetKey(resolverCtx, "cursor", "1"))
require.NoError(t, stateClient.Flush(resolverCtx))
require.NoError(t, stateClient.SetKey(ctx, "cursor", "2"))
require.NoError(t, stateClient.Flush(ctx))
require.Empty(t, sent(), "a later value must not overtake the rows the unchanged value covers")

require.NoError(t, unsent.release(ctx, res))
require.Equal(t, []string{"1", "2"}, sent())
}
Loading