Skip to content

logpuller: split subscription runtime helpers from subscription client#5603

Draft
lidezhu wants to merge 4 commits into
masterfrom
ldz/refactor-puller01
Draft

logpuller: split subscription runtime helpers from subscription client#5603
lidezhu wants to merge 4 commits into
masterfrom
ldz/refactor-puller01

Conversation

@lidezhu

@lidezhu lidezhu commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

What problem does this PR solve?

Issue Number: close #5604

What is changed and how it works?

Check List

Tests

  • Unit test
  • Integration test
  • Manual test (add detailed scripts or steps below)
  • No code

Questions

Will it cause performance regression or break compatibility?
Do you need to update user documentation, design documentation or monitoring documentation?

Release note

Please refer to [Release Notes Language Style Guide](https://pingcap.github.io/tidb-dev-guide/contribute-to-tidb/release-notes-style-guide.html) to write a quality release note.

If you don't think this PR needs a release note then fill it with `None`.

@ti-chi-bot

ti-chi-bot Bot commented Jul 9, 2026

Copy link
Copy Markdown

Skipping CI for Draft Pull Request.
If you want CI signal for your change, please convert it to an actual PR.
You can still manually trigger a test run with /test all

@ti-chi-bot ti-chi-bot Bot added do-not-merge/needs-linked-issue do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. release-note Denotes a PR that will be considered when it comes time to generate release notes. labels Jul 9, 2026
@ti-chi-bot

ti-chi-bot Bot commented Jul 9, 2026

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by:
Once this PR has been reviewed and has the lgtm label, please assign wlwilliamx for approval. For more information see the Code Review Process.
Please ensure that each of them provides their approval before proceeding.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 32228264-9d6e-4772-8dd1-bf540ab6d5f3

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ldz/refactor-puller01

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@ti-chi-bot ti-chi-bot Bot added size/XXL Denotes a PR that changes 1000+ lines, ignoring generated files. and removed do-not-merge/needs-linked-issue labels Jul 9, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request refactors the logpuller package by modularizing subscriptionClient and extracting its responsibilities into dedicated components: regionEventSink for managing event streams, regionFailureHandler for error dispatching, and spanRegistry for tracking subscribed spans. Additionally, a pullerMemoryQuota is introduced to handle memory backpressure. Feedback on these changes highlights a missing memoryQuota field and initialization in subscriptionClient that would cause compilation errors and nil pointer dereferences. The review also recommends cleaning up unused fields and methods in errCache, and refactoring the sync.Cond synchronization in regionEventSink to a safer, channel-based signaling mechanism to prevent goroutine leaks during context cancellation.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +139 to +141
eventSink *regionEventSink
failureHandler *regionFailureHandler
spanRegistry *spanRegistry

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

critical

The subscriptionClient struct is missing the memoryQuota field, which is referenced in puller_memory_quota.go (e.g., s.memoryQuota.acquireEvent). This will cause a compilation error. Please add the memoryQuota field to the struct.

Suggested change
eventSink *regionEventSink
failureHandler *regionFailureHandler
spanRegistry *spanRegistry
eventSink *regionEventSink
failureHandler *regionFailureHandler
spanRegistry *spanRegistry
memoryQuota *pullerMemoryQuota

Comment on lines +179 to +181
subClient.spanRegistry = newSpanRegistry(subClient.pd, subClient.pdClock)
subClient.failureHandler = newRegionFailureHandler(subClient)
subClient.eventSink = newRegionEventSink(subClient.ctx, subClient)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

critical

The memoryQuota field of subscriptionClient is never initialized in NewSubscriptionClient, which will lead to nil pointer dereferences when accessing it. Please initialize it using newPullerMemoryQuota(0).

Suggested change
subClient.spanRegistry = newSpanRegistry(subClient.pd, subClient.pdClock)
subClient.failureHandler = newRegionFailureHandler(subClient)
subClient.eventSink = newRegionEventSink(subClient.ctx, subClient)
subClient.spanRegistry = newSpanRegistry(subClient.pd, subClient.pdClock)
subClient.failureHandler = newRegionFailureHandler(subClient)
subClient.eventSink = newRegionEventSink(subClient.ctx, subClient)
subClient.memoryQuota = newPullerMemoryQuota(0)

Comment on lines +194 to +199
type errCache struct {
sync.Mutex
cache []regionErrorInfo
errCh chan regionErrorInfo
notify chan struct{}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The errCh field in errCache is completely unused in the new regionFailureHandler implementation, as errors are now popped directly from the cache via popBatch and handled in Run. To improve maintainability and clean up dead code, we should remove errCh and its associated unused methods (dispatchBatch and dispatch).

type errCache struct {
	sync.Mutex
	cache  []regionErrorInfo
	notify chan struct{}
}

Comment on lines +203 to +209
func newErrCache() *errCache {
return &errCache{
cache: make([]regionErrorInfo, 0, 1024),
errCh: make(chan regionErrorInfo, 4096),
notify: make(chan struct{}, 1024),
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Since errCh is unused, we should also remove its initialization in newErrCache.

func newErrCache() *errCache {
	return &errCache{
		cache:  make([]regionErrorInfo, 0, 1024),
		notify: make(chan struct{}, 1024),
	}
}

Comment on lines +30 to +33
mu sync.Mutex
cond *sync.Cond
paused atomic.Bool
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Using sync.Cond with select and default in Push is a known anti-pattern in Go because s.cond.Wait() cannot be interrupted by context cancellation. If the context is canceled while a goroutine is blocked in s.cond.Wait(), it will remain blocked until s.cond.Broadcast() is called, potentially leaking goroutines or delaying shutdown.

We can refactor this to use a channel-based signaling mechanism (resumeCh), which is completely lock-free on the fast path, safer, and fully interruptible by context cancellation.

Suggested change
mu sync.Mutex
cond *sync.Cond
paused atomic.Bool
}
mu sync.Mutex
paused bool
resumeCh chan struct{}

Comment on lines +35 to +37
func newRegionEventSink(ctx context.Context, subClient *subscriptionClient) *regionEventSink {
sink := &regionEventSink{ctx: ctx}
sink.cond = sync.NewCond(&sink.mu)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Initialize the resumeCh channel in the constructor and close it initially since the sink is not paused by default.

Suggested change
func newRegionEventSink(ctx context.Context, subClient *subscriptionClient) *regionEventSink {
sink := &regionEventSink{ctx: ctx}
sink.cond = sync.NewCond(&sink.mu)
func newRegionEventSink(ctx context.Context, subClient *subscriptionClient) *regionEventSink {
sink := &regionEventSink{
ctx: ctx,
resumeCh: make(chan struct{}),
}
close(sink.resumeCh) // initially not paused

Comment on lines +73 to +98
func (s *regionEventSink) Push(subID SubscriptionID, event regionEvent) {
// fast path
if !s.paused.Load() {
s.ds.Push(subID, event)
return
}

ctx := s.ctx
if ctx == nil {
ctx = context.Background()
}

// slow path: wait until paused is false
s.mu.Lock()
for s.paused.Load() {
select {
case <-ctx.Done():
s.mu.Unlock()
return
default:
s.cond.Wait()
}
}
s.mu.Unlock()
s.ds.Push(subID, event)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Refactor Push to use the channel-based signaling mechanism. This is lock-free on the fast path and fully interruptible by context cancellation. We also add a defensive check for resumeCh == nil to ensure compatibility with unit tests that manually construct regionEventSink without calling the constructor.

func (s *regionEventSink) Push(subID SubscriptionID, event regionEvent) {
	s.mu.Lock()
	resumeCh := s.resumeCh
	s.mu.Unlock()

	if resumeCh == nil {
		s.ds.Push(subID, event)
		return
	}

	select {
	case <-s.ctx.Done():
		return
	case <-resumeCh:
		s.ds.Push(subID, event)
	}
}

Comment on lines +107 to +117
case dynstream.PauseArea:
s.mu.Lock()
s.paused.Store(true)
s.mu.Unlock()
log.Info("subscription client pause push region event")
case dynstream.ResumeArea:
s.mu.Lock()
s.paused.Store(false)
s.cond.Broadcast()
s.mu.Unlock()
log.Info("subscription client resume push region event")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Update the Run loop to manage the resumeCh channel when pausing and resuming.

			case dynstream.PauseArea:
				s.mu.Lock()
				if !s.paused {
					s.paused = true
					s.resumeCh = make(chan struct{})
				}
				s.mu.Unlock()
				log.Info("subscription client pause push region event")
			case dynstream.ResumeArea:
				s.mu.Lock()
				if s.paused {
					s.paused = false
					close(s.resumeCh)
				}
				s.mu.Unlock()
				log.Info("subscription client resume push region event")

Comment on lines +129 to +135
func (s *regionEventSink) Close() {
s.mu.Lock()
s.paused.Store(false)
s.cond.Broadcast()
s.mu.Unlock()
s.ds.Close()
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Update Close to safely close the resumeCh channel if currently paused.

Suggested change
func (s *regionEventSink) Close() {
s.mu.Lock()
s.paused.Store(false)
s.cond.Broadcast()
s.mu.Unlock()
s.ds.Close()
}
func (s *regionEventSink) Close() {
s.mu.Lock()
if s.paused {
s.paused = false
close(s.resumeCh)
}
s.mu.Unlock()
s.ds.Close()
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. release-note Denotes a PR that will be considered when it comes time to generate release notes. size/XXL Denotes a PR that changes 1000+ lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

logpuller: split event sink, failure handling, and span lifecycle out of subscription client

1 participant