Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
11 changes: 11 additions & 0 deletions docs/modules/components/pages/inputs/aws_dynamodb_cdc.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ input:
checkpoint_table: redpanda_dynamodb_checkpoints
checkpoint_namespace: ""
start_from: trim_horizon
auto_replay_nacks: true
snapshot_mode: none
```

Expand All @@ -69,6 +70,7 @@ input:
start_from: trim_horizon
checkpoint_limit: 1000
max_tracked_shards: 10000
auto_replay_nacks: true
throttle_backoff: 100ms
snapshot_mode: none
snapshot_segments: 1
Expand Down Expand Up @@ -412,6 +414,15 @@ Maximum number of shards to track simultaneously. Prevents memory issues with ex

*Default*: `10000`

=== `auto_replay_nacks`

Whether messages that are rejected (nacked) at the output level should be automatically replayed indefinitely, eventually resulting in back pressure if the cause of the rejections is persistent. If set to `false` these messages will instead be deleted. Disabling auto replays can greatly improve memory efficiency of high throughput streams as the original shape of the data can be discarded immediately upon consumption and mutation.


*Type*: `bool`

*Default*: `true`

=== `throttle_backoff`

Time to wait when applying backpressure due to too many in-flight messages.
Expand Down
26 changes: 26 additions & 0 deletions internal/impl/aws/dynamodb/checkpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,32 @@ func (c *Checkpointer) CheckpointLimit() int {
return c.checkpointLimit
}

// HasAnyState reports whether any checkpoint state (shard checkpoints or
// snapshot progress) exists under this pipeline's namespace and stream/table
// key. Used to scope start_from to genuinely fresh pipelines: shards that
// appear once state exists are stream-rotation children whose backlog must
// not be skipped.
func (c *Checkpointer) HasAnyState(ctx context.Context) (bool, error) {
result, err := c.svc.Query(ctx, &dynamodb.QueryInput{

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HasAnyState is now the sole gate for honoring start_from: latest, but Query defaults to eventually consistent reads.

Failure scenario: a pipeline crashes and is restarted within a second or two (the common restart case). The checkpoint rows written moments before the crash are not yet visible to this probe, so HasAnyState returns falsehonorStartFrom.Store(true) → shards whose per-shard checkpoint read (also eventually consistent, Get) is stale for the same reason fall into the default branch and are re-positioned at LATEST — silently skipping their backlog, which is exactly the hole this change closes.

Since the probe is Limit: 1, adding ConsistentRead: true makes the decision deterministic for a negligible extra RCU.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in bea7052 — the probe now sets ConsistentRead: true. (The per-shard Get staleness alone is safe: with honorStartFrom false a missing checkpoint falls through to TRIM_HORIZON, so the probe was the only decision that needed the strong read.)

TableName: aws.String(c.tableName),
KeyConditionExpression: aws.String("#hk = :hv"),
ExpressionAttributeNames: map[string]string{
"#hk": c.hashAttrName(),
},
ExpressionAttributeValues: map[string]types.AttributeValue{
":hv": &types.AttributeValueMemberS{Value: c.hashKeyValue()},
},
Limit: aws.Int32(1),
})
if err != nil {
if _, ok := errors.AsType[*types.ResourceNotFoundException](err); ok {
return false, nil
}
return false, fmt.Errorf("probing checkpoint state for table=%s key=%s: %w", c.tableName, c.hashKeyValue(), err)
}
return len(result.Items) > 0, nil
}

type resumeMode int

const (
Expand Down
43 changes: 43 additions & 0 deletions internal/impl/aws/dynamodb/checkpoint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,49 @@ func shardRow(streamArn, shardID, seq, ts string) map[string]types.AttributeValu
return row
}

func TestHasAnyState(t *testing.T) {
t.Run("false on an empty partition", func(t *testing.T) {
c := globalCheckpointerWithPartition(t, "arn:A", nil)
has, err := c.HasAnyState(context.Background())
require.NoError(t, err)
require.False(t, has)
})

t.Run("true when any row exists", func(t *testing.T) {
c := globalCheckpointerWithPartition(t, "arn:A", []map[string]types.AttributeValue{
shardRow("arn:A", "shard-1", "seq-9", ""),
})
has, err := c.HasAnyState(context.Background())
require.NoError(t, err)
require.True(t, has)
})

t.Run("queries the namespaced hash key", func(t *testing.T) {
var queryIn *dynamodb.QueryInput
api := &fakeCheckpointAPI{
describeTable: func(context.Context, *dynamodb.DescribeTableInput, ...func(*dynamodb.Options)) (*dynamodb.DescribeTableOutput, error) {
return &dynamodb.DescribeTableOutput{Table: &types.TableDescription{TableStatus: types.TableStatusActive}}, nil
},
query: func(_ context.Context, in *dynamodb.QueryInput, _ ...func(*dynamodb.Options)) (*dynamodb.QueryOutput, error) {
queryIn = in
return &dynamodb.QueryOutput{}, nil
},
}
c, err := NewCheckpointer(context.Background(), api, CheckpointerConfig{
TableName: "cps", SourceTable: "t", StreamArn: "arn:A",
Namespace: "dev", CheckpointLimit: 1, Region: "us-east-1",
}, checkpointTestLogger())
require.NoError(t, err)

_, err = c.HasAnyState(context.Background())
require.NoError(t, err)
require.NotNil(t, queryIn)
hv, ok := queryIn.ExpressionAttributeValues[":hv"].(*types.AttributeValueMemberS)
require.True(t, ok)
require.Equal(t, "dev#arn:A", hv.Value)
})
}

func TestCDCCheckpointProbeNeeded(t *testing.T) {
// Only an exact, same-region resume can be a stale checkpoint: its sequence
// number belongs to this stream, so a failed iterator means trimmed data.
Expand Down
Loading