-
Notifications
You must be signed in to change notification settings - Fork 53
eventstore: fix checkpoint update race #5019
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -117,7 +117,7 @@ type dispatcherStat struct { | |||||
|
|
||||||
| resolvedTs atomic.Uint64 | ||||||
| // the max ts of events which is not needed by this dispatcher | ||||||
| checkpointTs uint64 | ||||||
| checkpointTs atomic.Uint64 | ||||||
| // the difference between `subStat`, `pendingSubStat` and `removingSubStat`: | ||||||
| // 1) if there is no existing subscriptions which can be reused, | ||||||
| // or there is a existing subscription with exact span match, | ||||||
|
|
@@ -500,8 +500,8 @@ func (e *eventStore) RegisterDispatcher( | |||||
| stat := &dispatcherStat{ | ||||||
| dispatcherID: dispatcherID, | ||||||
| tableSpan: dispatcherSpan, | ||||||
| checkpointTs: startTs, | ||||||
| } | ||||||
| stat.checkpointTs.Store(startTs) | ||||||
| stat.resolvedTs.Store(startTs) | ||||||
|
|
||||||
| wrappedNotifier := func(resolvedTs uint64, latestCommitTs uint64) { | ||||||
|
|
@@ -712,7 +712,7 @@ func (e *eventStore) UpdateDispatcherCheckpointTs( | |||||
| if !ok { | ||||||
| return | ||||||
| } | ||||||
| dispatcherStat.checkpointTs = checkpointTs | ||||||
| dispatcherStat.checkpointTs.Store(checkpointTs) | ||||||
|
|
||||||
| updateSubStatCheckpoint := func(subStat *subscriptionStat) { | ||||||
| if subStat == nil { | ||||||
|
|
@@ -732,8 +732,9 @@ func (e *eventStore) UpdateDispatcherCheckpointTs( | |||||
| continue | ||||||
| } | ||||||
|
|
||||||
| if newCheckpointTs == 0 || dispatcherStat.checkpointTs < newCheckpointTs { | ||||||
| newCheckpointTs = dispatcherStat.checkpointTs | ||||||
| dispatcherCheckpointTs := dispatcherStat.checkpointTs.Load() | ||||||
| if newCheckpointTs == 0 || dispatcherCheckpointTs < newCheckpointTs { | ||||||
| newCheckpointTs = dispatcherCheckpointTs | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
|
|
@@ -746,44 +747,39 @@ func (e *eventStore) UpdateDispatcherCheckpointTs( | |||||
| if newCheckpointTs == 0 { | ||||||
| return | ||||||
| } | ||||||
| oldCheckpointTs := subStat.checkpointTs.Load() | ||||||
| if newCheckpointTs == oldCheckpointTs { | ||||||
| return | ||||||
| } | ||||||
| if newCheckpointTs < oldCheckpointTs { | ||||||
| log.Panic("should not happen", | ||||||
| zap.Uint64("newCheckpointTs", newCheckpointTs), | ||||||
| zap.Uint64("oldCheckpointTs", oldCheckpointTs)) | ||||||
| } | ||||||
| // If there is no dml event after old checkpoint ts, then there is no data to be deleted. | ||||||
| // So we can skip adding gc item. | ||||||
| lastReceiveDMLTime := subStat.lastReceiveDMLTime.Load() | ||||||
| if lastReceiveDMLTime > 0 { | ||||||
| oldCheckpointPhysicalTime := oracle.GetTimeFromTS(oldCheckpointTs) | ||||||
| if lastReceiveDMLTime >= oldCheckpointPhysicalTime.UnixMilli() { | ||||||
| e.gcManager.addGCItem( | ||||||
| subStat.dbIndex, | ||||||
| uint64(subStat.subID), | ||||||
| subStat.tableSpan.TableID, | ||||||
| oldCheckpointTs, | ||||||
| newCheckpointTs, | ||||||
| ) | ||||||
| for { | ||||||
| oldCheckpointTs := subStat.checkpointTs.Load() | ||||||
| if newCheckpointTs == oldCheckpointTs { | ||||||
| return | ||||||
| } | ||||||
| } | ||||||
| e.subscriptionChangeCh.In() <- SubscriptionChange{ | ||||||
| ChangeType: SubscriptionChangeTypeUpdate, | ||||||
| SubID: uint64(subStat.subID), | ||||||
| Span: subStat.tableSpan, | ||||||
| CheckpointTs: newCheckpointTs, | ||||||
| ResolvedTs: subStat.resolvedTs.Load(), | ||||||
| } | ||||||
| subStat.checkpointTs.Store(newCheckpointTs) | ||||||
| if log.GetLevel() <= zap.DebugLevel { | ||||||
| log.Debug("update checkpoint ts", | ||||||
| zap.Any("dispatcherID", dispatcherID), | ||||||
| zap.Uint64("subscriptionID", uint64(subStat.subID)), | ||||||
| zap.Uint64("newCheckpointTs", newCheckpointTs), | ||||||
| zap.Uint64("oldCheckpointTs", oldCheckpointTs)) | ||||||
| if newCheckpointTs < oldCheckpointTs { | ||||||
| return | ||||||
| } | ||||||
| if !subStat.checkpointTs.CompareAndSwap(oldCheckpointTs, newCheckpointTs) { | ||||||
| continue | ||||||
| } | ||||||
| e.gcManager.addGCItem( | ||||||
| subStat.dbIndex, | ||||||
| uint64(subStat.subID), | ||||||
| subStat.tableSpan.TableID, | ||||||
| oldCheckpointTs, | ||||||
| newCheckpointTs, | ||||||
| ) | ||||||
| e.subscriptionChangeCh.In() <- SubscriptionChange{ | ||||||
| ChangeType: SubscriptionChangeTypeUpdate, | ||||||
| SubID: uint64(subStat.subID), | ||||||
| Span: subStat.tableSpan, | ||||||
| CheckpointTs: newCheckpointTs, | ||||||
| ResolvedTs: subStat.resolvedTs.Load(), | ||||||
| } | ||||||
| if log.GetLevel() <= zap.DebugLevel { | ||||||
| log.Debug("update checkpoint ts", | ||||||
| zap.Any("dispatcherID", dispatcherID), | ||||||
| zap.Uint64("subscriptionID", uint64(subStat.subID)), | ||||||
| zap.Uint64("newCheckpointTs", newCheckpointTs), | ||||||
| zap.Uint64("oldCheckpointTs", oldCheckpointTs)) | ||||||
| } | ||||||
| return | ||||||
| } | ||||||
| } | ||||||
| updateSubStatCheckpoint(dispatcherStat.subStat) | ||||||
|
|
@@ -1506,7 +1502,7 @@ func (iter *eventStoreIter) Next() (*common.RawKVEntry, bool) { | |||||
| } | ||||||
| comparableKey := common.ToComparableKey(rawKV.Key) | ||||||
| if bytes.Compare(comparableKey, iter.tableSpan.StartKey) >= 0 && | ||||||
| bytes.Compare(comparableKey, iter.tableSpan.EndKey) <= 0 { | ||||||
| bytes.Compare(comparableKey, iter.tableSpan.EndKey) < 0 { | ||||||
| break | ||||||
| } | ||||||
| log.Debug("event store iter skip kv not in table span", | ||||||
|
|
@@ -1650,17 +1646,21 @@ func (e *eventStore) uploadStatePeriodically(ctx context.Context) error { | |||||
| log.Warn("cannot find subscription state", zap.Uint64("subscriptionID", change.SubID)) | ||||||
| continue | ||||||
| } | ||||||
| if change.CheckpointTs < tableState.Subscriptions[targetIndex].CheckpointTs || | ||||||
| change.ResolvedTs < tableState.Subscriptions[targetIndex].ResolvedTs { | ||||||
| log.Panic("should not happen", | ||||||
| subState := tableState.Subscriptions[targetIndex] | ||||||
| if change.CheckpointTs < subState.CheckpointTs || change.ResolvedTs < subState.ResolvedTs { | ||||||
| log.Warn("ignore stale subscription state update", | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The log message "ignore stale subscription state update" is slightly misleading because the code does not ignore the update. It proceeds to update any timestamps that are advancing monotonically. A more accurate log message would be "received stale subscription state update", which correctly indicates that a stale value was detected while still processing the valid parts of the update. This would be clearer for anyone debugging potential message ordering issues.
Suggested change
|
||||||
| zap.Uint64("subscriptionID", change.SubID), | ||||||
| zap.Uint64("oldCheckpointTs", tableState.Subscriptions[targetIndex].CheckpointTs), | ||||||
| zap.Uint64("oldResolvedTs", tableState.Subscriptions[targetIndex].ResolvedTs), | ||||||
| zap.Uint64("oldCheckpointTs", subState.CheckpointTs), | ||||||
| zap.Uint64("oldResolvedTs", subState.ResolvedTs), | ||||||
| zap.Uint64("newCheckpointTs", change.CheckpointTs), | ||||||
| zap.Uint64("newResolvedTs", change.ResolvedTs)) | ||||||
| } | ||||||
| tableState.Subscriptions[targetIndex].CheckpointTs = change.CheckpointTs | ||||||
| tableState.Subscriptions[targetIndex].ResolvedTs = change.ResolvedTs | ||||||
| if change.CheckpointTs > subState.CheckpointTs { | ||||||
| subState.CheckpointTs = change.CheckpointTs | ||||||
| } | ||||||
| if change.ResolvedTs > subState.ResolvedTs { | ||||||
| subState.ResolvedTs = change.ResolvedTs | ||||||
| } | ||||||
| default: | ||||||
| log.Panic("invalid subscription change type", zap.Int("changeType", int(change.ChangeType))) | ||||||
| } | ||||||
|
|
||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Keep the per-dispatcher checkpoint monotonic too.
Line 715 can still overwrite a newer dispatcher checkpoint with a stale one. After that, the min recomputation can stay pinned below the real progress, so the subscription checkpoint and GC stop advancing until this dispatcher reports again.
Suggested fix
📝 Committable suggestion
🤖 Prompt for AI Agents