Skip to content

logpuller: refactor region request worker lifecycle - #5619

Open
lidezhu wants to merge 27 commits into
masterfrom
ldz/refactor-puller04
Open

logpuller: refactor region request worker lifecycle#5619
lidezhu wants to merge 27 commits into
masterfrom
ldz/refactor-puller04

Conversation

@lidezhu

@lidezhu lidezhu commented Jul 10, 2026

Copy link
Copy Markdown
Collaborator

What problem does this PR solve?

Issue Number: close #5858

What is changed and how it works?

  1. Reworked the puller’s region request state machine by removing region_req_cache and introducing two new components: region_admission_controller for per-worker admission/window control, and region_tracker for tracking in-flight region states by subscription and region ID.
  2. Refactored the request/priority flow around that new model: region_request_worker, subscription_client, region_state, and related tests were rewritten to use admission + tracker, while region task priority was simplified to rely directly on ScanPriority instead of the older extra task-priority layer.
  3. Updated the surrounding infrastructure and config: the puller now uses OldStartTsScanLowPriorityThreshold as the low-priority lag threshold, adds a new utils/notifyqueue helper for lightweight wake-up signaling, and refreshes the related tests and edge-case handling around stream failure, deregistration, and shutdown.

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`.

Summary by CodeRabbit

New Features

  • Added priority-aware region scanning with FIFO ordering for equally prioritized work.
  • Added admission controls for scan concurrency, queue windows, cancellation, shutdown, and request tracking.
  • Added configurable maximum scan-request windows with validation and safe defaults.
  • Added a FIFO notification queue to improve worker coordination.
  • Added support for standardized scan-priority values across scheduling and retries.

Bug Fixes

  • Improved cleanup during region failures, stopped feeds, stream recovery, and request-send failures.
  • Prevented duplicate active-region processing and ensured pending requests are released correctly.
  • Improved priority handling for retries and catch-up scans.
  • Reduced the low-priority scan threshold from 30 minutes to 10 minutes.

@ti-chi-bot

ti-chi-bot Bot commented Jul 10, 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 release-note Denotes a PR that will be considered when it comes time to generate release notes. do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. labels Jul 10, 2026
@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 9ac50008-c813-4db1-95fa-e9046a5135eb

📥 Commits

Reviewing files that changed from the base of the PR and between 2973fcc and 4b0768b.

📒 Files selected for processing (4)
  • logservice/logpuller/region_event_handler.go
  • logservice/logpuller/region_request_worker.go
  • logservice/logpuller/region_request_worker_test.go
  • logservice/logpuller/subscription_client.go
💤 Files with no reviewable changes (2)
  • logservice/logpuller/region_request_worker_test.go
  • logservice/logpuller/subscription_client.go

📝 Walkthrough

Walkthrough

The puller replaces request-cache scheduling with typed scan-priority admission control. It adds bounded request windows, region tracking, worker lifecycle handling, configurable limits, and explicit deregistration queues.

Changes

Region scheduling and admission control

Layer / File(s) Summary
Scheduling configuration and notification primitives
pkg/config/debug.go, pkg/config/debug_test.go, utils/notifyqueue/*, utils/priorityqueue/priority_queue_test.go
Adds configurable request windows, validation, and FIFO notification behavior with coalesced wake-up signals.
Scan-priority model and queue ordering
logservice/logpuller/priority_task.go, logservice/logpuller/scan_priority.go, logservice/logpuller/subscription_client.go, logservice/logpuller/*priority*_test.go
Uses normalized cdcpb.ScanPriority values. High-priority tasks run first. Equal-priority tasks use submission sequence order.
Bounded region admission
logservice/logpuller/region_admission_controller.go, logservice/logpuller/region_admission_controller_test.go
Adds prioritized admission windows, idempotent leases, cancellation, closure, draining, statistics, and capacity release.
Tracked region state lifecycle
logservice/logpuller/region_tracker.go, logservice/logpuller/region_state.go, logservice/logpuller/region_event_handler.go
Adds synchronized region tracking and conditional removal. Feed states atomically finish or abort active admission leases.
Worker request and stream lifecycle
logservice/logpuller/region_request_worker.go, logservice/logpuller/region_request_worker_test.go
Moves request sending, event dispatch, deregistration, duplicate handling, stream recovery, and failure cleanup to admission- and tracker-backed worker flows.
Store admission integration
logservice/logpuller/subscription_client.go, logservice/logpuller/subscription_client_test.go
Creates per-worker admission windows, distributes region tasks through worker queues, aggregates pending and inflight counts, and broadcasts deregistration during shutdown.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant SubscriptionClient
  participant RegionAdmissionController
  participant RegionRequestWorker
  participant RegionTracker
  participant EventSink
  SubscriptionClient->>RegionAdmissionController: submit prioritized region task
  RegionAdmissionController->>RegionRequestWorker: admit region request
  RegionRequestWorker->>RegionTracker: add or remove region feed state
  RegionRequestWorker->>EventSink: publish region events and resolved timestamps
  RegionRequestWorker->>RegionAdmissionController: finish or abort admission lease
Loading

Possibly related PRs

  • pingcap/ticdc#5487: Reworks logpuller priority-task ordering and related priority-queue usage.

Suggested labels: lgtm, approved

Suggested reviewers: 3aceshowhand, hongyunyan, asddongmen

Poem

A rabbit queued scans in a neat little line,
High priority hopped to the front every time.
The tracker held states, safe and bright,
Workers sent requests through windows of light.
“Finish or abort,” said the lease with a cheer,
While FIFO kept order from burrow to ear.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description includes the issue and change summary, but it leaves the test selection, compatibility questions, documentation question, and release note unanswered. Select the applicable test type, answer both Questions items, and provide a release note or set it to None.
Docstring Coverage ⚠️ Warning Docstring coverage is 6.76% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title directly identifies the logpuller region request worker lifecycle refactor, which is the primary change.
Linked Issues check ✅ Passed The changes implement explicit request ownership, separate deregistration control flow, and stream-failure rescheduling for issue [#5858].
Out of Scope Changes check ✅ Passed All changes support the lifecycle refactor, admission and tracking model, priority flow, failure handling, shutdown, and tests; no unrelated code is evident.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ldz/refactor-puller04

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 the size/XXL Denotes a PR that changes 1000+ lines, ignoring generated files. label Jul 10, 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 region request and subscription lifecycle management in the logpuller service. It replaces the old channel-based requestCache and manual state-tracking map in regionRequestWorker with a simplified requestCache (using a new notifyqueue.Queue utility) and a dedicated regionTracker. Additionally, subscription deregistration is now broadcasted directly to workers' control queues rather than being routed through the main region task queue. There are no review comments provided, so I have no feedback to evaluate.

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.

@ti-chi-bot

ti-chi-bot Bot commented Jul 11, 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 lidezhu 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

@lidezhu
lidezhu marked this pull request as ready for review August 2, 2026 11:08
@ti-chi-bot ti-chi-bot Bot removed the do-not-merge/work-in-progress Indicates that a PR should not merge because it is a work in progress. label Aug 2, 2026
@lidezhu

lidezhu commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator Author

/gemini review

@lidezhu

lidezhu commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@lidezhu

lidezhu commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator Author

/gemini summary

@lidezhu lidezhu changed the title logpuller: make region request worker lifecycle explicit logpuller: refactor region request worker lifecycle Aug 2, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
logservice/logpuller/region_event_handler.go (1)

245-260: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Gate Report on RemoveIf success.

markRemoved() can return true independently of the tracker entry for this state, while RemoveIf returns false when another state owns that subscription/region. Reporting the failure in that case creates duplicate failure handling for a region that already has an owner.

🐛 Proposed fix
 	if stepsToRemoved {
-		worker.tracker.RemoveIf(SubscriptionID(state.requestID), state.getRegionID(), state)
-		h.failureHandler.Report(newRegionErrorInfo(state.getRegionInfo(), err))
+		if worker.tracker.RemoveIf(SubscriptionID(state.requestID), state.getRegionID(), state) {
+			h.failureHandler.Report(newRegionErrorInfo(state.getRegionInfo(), err))
+		}
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@logservice/logpuller/region_event_handler.go` around lines 245 - 260, In
handleRegionError, only call failureHandler.Report after worker.tracker.RemoveIf
succeeds; retain the existing markRemoved check and avoid reporting when
RemoveIf indicates another state owns the subscription/region.
🧹 Nitpick comments (14)
logservice/logpuller/region_request_worker_test.go (3)

165-173: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

pushCount shadows the embedded field.

countingRegionEventDynamicStream embeds mockRegionEventDynamicStream, which already declares pushCount, and then declares its own pushCount. Go resolves m.pushCount to the outer field because it has the shallower depth, so the current code is correct.

The type now carries two counters with the same name. If a later change calls the embedded Push, that call increments the inner counter and the benchmark assertion reads the outer one. Rename the field to make the two counters distinct.

♻️ Proposed refactor
 type countingRegionEventDynamicStream struct {
 	mockRegionEventDynamicStream
-	pushCount int
+	counted int
 }
 
 func (m *countingRegionEventDynamicStream) Push(_ SubscriptionID, _ regionEvent) {
-	m.pushCount++
+	m.counted++
 }

Update the benchmark assertion to read ds.counted.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@logservice/logpuller/region_request_worker_test.go` around lines 165 - 173,
Rename the outer pushCount field in countingRegionEventDynamicStream to counted,
keep Push incrementing that outer counter, and update the benchmark assertion to
read ds.counted. Leave the embedded mockRegionEventDynamicStream counter
unchanged.

69-80: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use a fixed timestamp in admitRegionRequest.

admitRegionRequest derives currentTs from time.Now(). Line 304 in the same file calls submitRegionForAdmission with the fixed value 1. If the admission controller compares currentTs against the region checkpoint to decide priority or eligibility, this helper makes those decisions depend on the wall clock and on the region fixture data.

Accept the timestamp as a parameter, or use a fixed constant, so every test that uses this helper stays deterministic.

♻️ Proposed refactor
 func admitRegionRequest(
 	t *testing.T,
 	controller *regionAdmissionController,
 	region regionInfo,
 ) *regionReq {
 	t.Helper()
-	currentTs := oracle.GoTimeToTS(time.Now())
-	submitRegionForAdmission(t, controller, region, currentTs)
+	const testCurrentTs = uint64(1)
+	submitRegionForAdmission(t, controller, region, testCurrentTs)
 	req, err := controller.pop(t.Context(), nil)
 	require.NoError(t, err)
 	return req
 }

As per coding guidelines: "Prefer focused deterministic tests; see docs/agents/testing.md before adding or changing tests."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@logservice/logpuller/region_request_worker_test.go` around lines 69 - 80,
Make admitRegionRequest deterministic by removing its time.Now()-derived
timestamp and using a caller-supplied timestamp or fixed test constant, while
preserving the existing submitRegionForAdmission and pop flow. Update all
callers, including the case currently passing timestamp 1, to use the chosen
deterministic value.

Source: Coding guidelines


397-404: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Both stream-recovery tests duplicate handleStreamFailure instead of calling it. handleStreamFailure is a closure declared inside Run in logservice/logpuller/region_request_worker.go, so no test can invoke it. Both tests work around this by copying parts of its body, which means neither test protects the production recovery path. Extract handleStreamFailure into a method on regionRequestWorker and call it from both tests.

  • logservice/logpuller/region_request_worker_test.go#L397-L404: replace the copied tracker-drain and event-push loop with a call to the extracted method, then keep the assertions on admission.stats().inflight, req.abort(), and ds.pushCount.
  • logservice/logpuller/region_request_worker_test.go#L451-L453: replace the copied admission-drain and onRegionFail loop with a call to the extracted method, then keep the assertions on admission.stats().pending and the failure-handler cache.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@logservice/logpuller/region_request_worker_test.go` around lines 397 - 404,
Extract the closure handleStreamFailure from regionRequestWorker.Run into a
regionRequestWorker method, preserving its recovery behavior. In
logservice/logpuller/region_request_worker_test.go:397-404, replace the
duplicated tracker-drain and event-push logic with that method call while
retaining the inflight, abort, and pushCount assertions; at 451-453, replace the
duplicated admission-drain and onRegionFail loop similarly while retaining the
pending and failure-handler-cache assertions.

Source: Coding guidelines

logservice/logpuller/subscription_client_test.go (2)

642-651: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add coverage for a deregistration that matches a pending admission task.

The test submits a task for subscription 2 and deregisters subscription 1, so the two never interact. The interesting case is a deregistration for the subscription that owns the pending task.

sendDeregisterRequest in region_request_worker.go clears only tracker entries, so a queued admission task for a deregistered subscription survives and is sent later. It then fails the subscribedSpan.stopped check in sendRegionRequest. Add a case that pins this behaviour.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@logservice/logpuller/subscription_client_test.go` around lines 642 - 651,
Update the deregistration test around NewRegionPriorityTask and
client.broadcastDeregister to submit and deregister the same subscription, then
assert the pending admission task remains queued and is later processed through
the existing request path, covering the stopped subscribedSpan behavior in
sendDeregisterRequest and sendRegionRequest.

404-449: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add a case for the unspecified scan priority.

The table covers SCAN_PRIORITY_HIGH and SCAN_PRIORITY_LOW only. cdcpb.ScanPriority_SCAN_PRIORITY_UNSPECIFIED is the zero value, and region_request_worker.go calls normalizeScanPriority on the way out, which implies the unset value reaches this code in practice.

Add a case with the zero value to pin down the retry behaviour for an unset priority.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@logservice/logpuller/subscription_client_test.go` around lines 404 - 449,
Extend the table-driven tests around the existing high/low cases to cover
cdcpb.ScanPriority_SCAN_PRIORITY_UNSPECIFIED (the zero value), including its
retry input and expected priority. Use the test’s existing fields and naming
pattern to pin down normalizeScanPriority behavior for an unset priority.
logservice/logpuller/region_request_worker.go (2)

247-254: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

The deferred log loses the underlying stream error.

err is the named return value. Line 247 assigns the g.Wait() result to it, but line 252 overwrites it with &storeStreamErr{} before the deferred log at line 214 runs. The deferred log therefore always reports storeStreamErr and never the concrete cause from Recv or Send.

Log the original error before you replace it.

♻️ Proposed refactor to keep the original cause
 	err = g.Wait()
 	if err != nil {
+		log.Warn("region request worker stream failed",
+			zap.Uint64("workerID", s.workerID),
+			zap.String("addr", s.store.storeAddr),
+			zap.Error(err))
 		if ctx.Err() != nil {
 			return ctx.Err()
 		}
 		return &storeStreamErr{}
 	}
 	return nil

Confirm the level and field set against docs/agents/logging.md before you merge. As per coding guidelines: "Logs are operational signals; see docs/agents/logging.md before adding, removing, or rewriting logs."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@logservice/logpuller/region_request_worker.go` around lines 247 - 254,
Preserve the original g.Wait() error in the return path of the region request
worker before replacing it with storeStreamErr, so the deferred log records the
concrete Recv or Send failure. Keep the existing context-cancellation handling,
and verify any logging changes against docs/agents/logging.md for the required
level and fields.

Source: Coding guidelines


417-420: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Consider batching the deregistration events.

This loop pushes one regionEvent per state. A subscription over a large table can hold thousands of tracked regions, so a single deregistration produces thousands of sink pushes on the send goroutine. dispatchResolvedTsEvent already batches states at 1024 per push for the same reason.

Batch these states the same way to bound the push count and keep the send loop responsive to the control queue.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@logservice/logpuller/region_request_worker.go` around lines 417 - 420, The
deregistration loop should batch stopped states instead of pushing one
regionEvent per state. Update the flow around tracker.TakeSubscription and
eventSink.Push to accumulate at most 1024 states per event, push each full
batch, and push any remaining states after the loop while preserving markStopped
behavior.
pkg/config/debug.go (1)

102-116: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider suppressing the warning for unset fields.

PullerConfig fields default to zero when a user omits them from the TOML file. In that case both branches log a warning on every startup, even though no misconfiguration exists. Log at warn level only when the value is negative, and apply the default silently when the value is zero.

♻️ Proposed adjustment
-	if c.PendingRegionRequestQueueSize <= 0 {
-		log.Warn("pending region request queue size must be positive, use default value",
-			zap.Int("value", c.PendingRegionRequestQueueSize),
-			zap.Int("default", defaultCfg.PendingRegionRequestQueueSize))
+	if c.PendingRegionRequestQueueSize <= 0 {
+		if c.PendingRegionRequestQueueSize < 0 {
+			log.Warn("pending region request queue size must be positive, use default value",
+				zap.Int("value", c.PendingRegionRequestQueueSize),
+				zap.Int("default", defaultCfg.PendingRegionRequestQueueSize))
+		}
 		c.PendingRegionRequestQueueSize = defaultCfg.PendingRegionRequestQueueSize
 	}

As per coding guidelines: "Logs are operational signals; see docs/agents/logging.md before adding, removing, or rewriting logs."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/config/debug.go` around lines 102 - 116, Update
PullerConfig.ValidateAndAdjust so negative queue-size or window-multiplier
values log warnings and are replaced with defaults, while zero values are
treated as unset and silently replaced without warning. Preserve the existing
default assignments and warning details for negative inputs.

Source: Coding guidelines

logservice/logpuller/subscription_client.go (2)

427-437: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

The lock around the worker list creation has no effect.

Line 427 writes rs.requestWorkers.s before the Lock call on Line 429, and rs is not published to s.stores until Line 437. No other goroutine can observe rs during this window, so the Lock/Unlock pair only adds noise and suggests a synchronization requirement that does not exist here. Remove the pair, or move the slice allocation inside it for consistency.

♻️ Proposed cleanup
 		rs = &requestedStore{storeAddr: storeAddr}
 		rs.requestWorkers.s = make([]*regionRequestWorker, 0, workerCount)
-
-		rs.requestWorkers.Lock()
 		for i := 0; i < workerCount; i++ {
 			requestWorker := newRegionRequestWorker(s, rs, workerWindow, maxWindowMultiplier)
 			rs.requestWorkers.s = append(rs.requestWorkers.s, requestWorker)
 		}
-		rs.requestWorkers.Unlock()
-
 		// Publish the store only after its immutable worker list is complete.
 		s.stores.Store(storeAddr, rs)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@logservice/logpuller/subscription_client.go` around lines 427 - 437, Remove
the unnecessary requestWorkers lock and unlock around worker list construction
in the store initialization flow, since rs remains unpublished until
s.stores.Store. Keep the slice allocation and worker appends unchanged, then
publish the completed store as before.

642-659: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the duplicated priority log field.

Line 642 assigns region.scanPriority = priority. resolve returns only SCAN_PRIORITY_HIGH or SCAN_PRIORITY_LOW, so normalizeScanPriority(priority) on Line 652 equals region.scanPriority on Line 653. The log record carries the same value under two keys.

♻️ Proposed cleanup
-				zap.String("priority", normalizeScanPriority(priority).String()),
 				zap.String("scanPriority", region.scanPriority.String()),

As per coding guidelines: "Logs are operational signals; see docs/agents/logging.md before adding, removing, or rewriting logs."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@logservice/logpuller/subscription_client.go` around lines 642 - 659, In the
cdc region scan task log within the region lock handling flow, remove the
redundant priority field so the record reports the priority only once. Keep
region.scanPriority assignment and its existing log field unchanged, and delete
the normalizeScanPriority(priority) field from the log call.

Source: Coding guidelines

logservice/logpuller/priority_task.go (1)

45-70: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Keep the priority queue ordering key stable during submission.

handleRegions pops a task and assigns a refreshed region to regionTask.regionInfo, but LessThan, priority, and canUseMaxWindow still read pt.regionInfo.scanPriority. A queued low-priority task can be re-prioritized through that live field before the same task is submitted again, so keep the normalized priority in an immutable field set at construction. Store only the resolved RPC context/task metadata in regionInfo while enqueueing.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@logservice/logpuller/priority_task.go` around lines 45 - 70, The priority
queue ordering currently depends on mutable regionInfo.scanPriority, allowing
queued tasks to change order after submission. Update regionPriorityTask to
store the normalized scan priority in an immutable construction-time field, and
make priority, canUseMaxWindow, and LessThan use that field; keep regionInfo
limited to refreshed RPC context and task metadata.
logservice/logpuller/region_admission_controller_test.go (2)

52-61: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove or use the unused currentTs parameter.

submitRegionForAdmission takes currentTs uint64 but never uses it; NewRegionPriorityTask is called with only region and region.verID.GetID(). Every call site threads a currentTs value through for no effect. This also means none of these tests actually exercise priority derivation from checkpoint staleness (checkpointTs vs currentTs); priority is always set explicitly via region.scanPriority.

Either remove the unused parameter, or wire it into task creation if staleness-based priority is meant to be covered here.

♻️ Proposed fix to drop the unused parameter
 func submitRegionForAdmission(
 	t *testing.T,
 	controller *regionAdmissionController,
 	region regionInfo,
-	currentTs uint64,
 ) {
 	t.Helper()
 	task := NewRegionPriorityTask(region, region.verID.GetID())
 	require.True(t, controller.submit(task))
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@logservice/logpuller/region_admission_controller_test.go` around lines 52 -
61, Remove the unused currentTs parameter from submitRegionForAdmission and
update every call site to stop passing it. Leave NewRegionPriorityTask and the
existing explicit region.scanPriority behavior unchanged.

88-152: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consolidate near-duplicate high-priority tests.

TestRegionAdmissionControllerHighPriorityUsesMaxWindow (lines 88-122) and TestRegionAdmissionControllerPrioritizesHighPriorityRegion (lines 124-152) exercise the same scenario: submit two normal-priority regions plus one high-priority region, and verify the high-priority region is admitted first via the max window. The second test omits only the interrupt/inflight check present in the first. Merge them into one test to reduce duplication.

As per path instructions, "Prefer focused deterministic tests; see docs/agents/testing.md before adding or changing tests."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@logservice/logpuller/region_admission_controller_test.go` around lines 88 -
152, Consolidate TestRegionAdmissionControllerPrioritizesHighPriorityRegion into
TestRegionAdmissionControllerHighPriorityUsesMaxWindow, retaining the max-window
admission assertions plus the interrupt/inflight verification and cleanup for
all admitted requests. Remove the duplicate test and preserve coverage that the
high-priority region is admitted before the remaining normal-priority region.

Source: Path instructions

logservice/logpuller/region_admission_controller.go (1)

42-61: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Avoid unconditional mutex acquisition for a debug-only log field.

r.controller.stats() locks c.mu on every call to finish(), even though it is only used to populate a zap.Int field on a log.Debug call. Go does not defer argument evaluation for logging calls, so this lock is acquired on every completed admission lease regardless of the configured log level. Under high region churn, this adds avoidable lock contention on the admission controller's mutex.

Consider dropping the inflightCount field from this debug log, or gating it behind an explicit level check before calling stats().

As per path instructions, "Logs are operational signals; see docs/agents/logging.md before adding, removing, or rewriting logs." Please confirm this new log field is intentional per that guidance.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@logservice/logpuller/region_admission_controller.go` around lines 42 - 61,
Remove the inflightCount field and its unconditional r.controller.stats() call
from the debug log in regionReq.finish. Preserve the remaining region request
diagnostic fields and metric behavior; do not add or rewrite logging beyond
eliminating this mutex acquisition.

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@logservice/logpuller/region_admission_controller.go`:
- Around line 203-233: Update requestedStore.close() to drain each
regionAdmissionController’s pending tasks before closing its worker, and
reschedule or otherwise return those tasks through the existing failure/retry
path such as onRegionFail(). Ensure shutdown removes all pending admissions
instead of leaving them in a closed controller; use
regionAdmissionController.drain() and close() while preserving normal worker
shutdown behavior.
- Around line 143-173: The regionAdmissionController.pop method currently
returns raw context cancellation errors instead of repository-defined errors.
Replace the context.Canceled and ctx.Err() returns in pop with the appropriate
TiCDC cancellation error, wrapping the underlying context error at this
boundary, and update the related test’s require.ErrorIs predicate to assert the
repository error.

In `@logservice/logpuller/region_request_worker.go`:
- Around line 217-231: Move errgroup.WithContext(ctx) in the worker flow so it
is created only after Connect succeeds, while preserving the existing gctx usage
for subsequent operations; alternatively, explicitly cancel the derived context
before every early return from the Connect failure path.
- Around line 436-453: Update sendRegionRequest so an inactive req returns nil
instead of &storeStreamErr{}, matching the stopped subscribedSpan branch and
allowing the worker stream to continue without reconnecting or draining pending
requests.

In `@logservice/logpuller/subscription_client.go`:
- Around line 339-342: Move the stopped-subscription guard into the
region-handling path, specifically around handleRequest and before it submits a
region or calls getStore() for rt.subID. Ensure pending handleRegions tasks
recheck the subscription’s stopped state after setTableStopped and skip creating
or using a worker when stopped, while preserving normal processing for active
subscriptions.
- Around line 467-485: Update the failed `store.submit(regionTask)` path in
`handleRegions` so the region task is rescheduled rather than dropped,
preserving its range lock and allowing retry when admission is unavailable.
Before returning, check `ctx.Done()` and return `ctx.Err()` only during client
shutdown; otherwise propagate the repository’s predefined admission/store error
instead of the raw `context.Canceled` sentinel. Keep successful submissions and
logging unchanged.

In `@utils/notifyqueue/notify_queue.go`:
- Around line 1-12: Both notifyqueue files have incomplete Apache license
headers. In utils/notifyqueue/notify_queue.go lines 1-12 and
utils/notifyqueue/notify_queue_test.go lines 1-12, add the standard “WITHOUT
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.” license line
immediately after the AS IS BASIS line.

---

Outside diff comments:
In `@logservice/logpuller/region_event_handler.go`:
- Around line 245-260: In handleRegionError, only call failureHandler.Report
after worker.tracker.RemoveIf succeeds; retain the existing markRemoved check
and avoid reporting when RemoveIf indicates another state owns the
subscription/region.

---

Nitpick comments:
In `@logservice/logpuller/priority_task.go`:
- Around line 45-70: The priority queue ordering currently depends on mutable
regionInfo.scanPriority, allowing queued tasks to change order after submission.
Update regionPriorityTask to store the normalized scan priority in an immutable
construction-time field, and make priority, canUseMaxWindow, and LessThan use
that field; keep regionInfo limited to refreshed RPC context and task metadata.

In `@logservice/logpuller/region_admission_controller_test.go`:
- Around line 52-61: Remove the unused currentTs parameter from
submitRegionForAdmission and update every call site to stop passing it. Leave
NewRegionPriorityTask and the existing explicit region.scanPriority behavior
unchanged.
- Around line 88-152: Consolidate
TestRegionAdmissionControllerPrioritizesHighPriorityRegion into
TestRegionAdmissionControllerHighPriorityUsesMaxWindow, retaining the max-window
admission assertions plus the interrupt/inflight verification and cleanup for
all admitted requests. Remove the duplicate test and preserve coverage that the
high-priority region is admitted before the remaining normal-priority region.

In `@logservice/logpuller/region_admission_controller.go`:
- Around line 42-61: Remove the inflightCount field and its unconditional
r.controller.stats() call from the debug log in regionReq.finish. Preserve the
remaining region request diagnostic fields and metric behavior; do not add or
rewrite logging beyond eliminating this mutex acquisition.

In `@logservice/logpuller/region_request_worker_test.go`:
- Around line 165-173: Rename the outer pushCount field in
countingRegionEventDynamicStream to counted, keep Push incrementing that outer
counter, and update the benchmark assertion to read ds.counted. Leave the
embedded mockRegionEventDynamicStream counter unchanged.
- Around line 69-80: Make admitRegionRequest deterministic by removing its
time.Now()-derived timestamp and using a caller-supplied timestamp or fixed test
constant, while preserving the existing submitRegionForAdmission and pop flow.
Update all callers, including the case currently passing timestamp 1, to use the
chosen deterministic value.
- Around line 397-404: Extract the closure handleStreamFailure from
regionRequestWorker.Run into a regionRequestWorker method, preserving its
recovery behavior. In
logservice/logpuller/region_request_worker_test.go:397-404, replace the
duplicated tracker-drain and event-push logic with that method call while
retaining the inflight, abort, and pushCount assertions; at 451-453, replace the
duplicated admission-drain and onRegionFail loop similarly while retaining the
pending and failure-handler-cache assertions.

In `@logservice/logpuller/region_request_worker.go`:
- Around line 247-254: Preserve the original g.Wait() error in the return path
of the region request worker before replacing it with storeStreamErr, so the
deferred log records the concrete Recv or Send failure. Keep the existing
context-cancellation handling, and verify any logging changes against
docs/agents/logging.md for the required level and fields.
- Around line 417-420: The deregistration loop should batch stopped states
instead of pushing one regionEvent per state. Update the flow around
tracker.TakeSubscription and eventSink.Push to accumulate at most 1024 states
per event, push each full batch, and push any remaining states after the loop
while preserving markStopped behavior.

In `@logservice/logpuller/subscription_client_test.go`:
- Around line 642-651: Update the deregistration test around
NewRegionPriorityTask and client.broadcastDeregister to submit and deregister
the same subscription, then assert the pending admission task remains queued and
is later processed through the existing request path, covering the stopped
subscribedSpan behavior in sendDeregisterRequest and sendRegionRequest.
- Around line 404-449: Extend the table-driven tests around the existing
high/low cases to cover cdcpb.ScanPriority_SCAN_PRIORITY_UNSPECIFIED (the zero
value), including its retry input and expected priority. Use the test’s existing
fields and naming pattern to pin down normalizeScanPriority behavior for an
unset priority.

In `@logservice/logpuller/subscription_client.go`:
- Around line 427-437: Remove the unnecessary requestWorkers lock and unlock
around worker list construction in the store initialization flow, since rs
remains unpublished until s.stores.Store. Keep the slice allocation and worker
appends unchanged, then publish the completed store as before.
- Around line 642-659: In the cdc region scan task log within the region lock
handling flow, remove the redundant priority field so the record reports the
priority only once. Keep region.scanPriority assignment and its existing log
field unchanged, and delete the normalizeScanPriority(priority) field from the
log call.

In `@pkg/config/debug.go`:
- Around line 102-116: Update PullerConfig.ValidateAndAdjust so negative
queue-size or window-multiplier values log warnings and are replaced with
defaults, while zero values are treated as unset and silently replaced without
warning. Preserve the existing default assignments and warning details for
negative inputs.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 1ce0378a-a0fe-402d-adb4-e84d3a2f438f

📥 Commits

Reviewing files that changed from the base of the PR and between af33cc1 and 23b085b.

📒 Files selected for processing (22)
  • logservice/logpuller/priority_task.go
  • logservice/logpuller/priority_task_test.go
  • logservice/logpuller/region_admission_controller.go
  • logservice/logpuller/region_admission_controller_test.go
  • logservice/logpuller/region_event_handler.go
  • logservice/logpuller/region_event_handler_test.go
  • logservice/logpuller/region_failure_handler.go
  • logservice/logpuller/region_req_cache.go
  • logservice/logpuller/region_req_cache_test.go
  • logservice/logpuller/region_request_worker.go
  • logservice/logpuller/region_request_worker_test.go
  • logservice/logpuller/region_state.go
  • logservice/logpuller/region_tracker.go
  • logservice/logpuller/region_tracker_test.go
  • logservice/logpuller/scan_priority.go
  • logservice/logpuller/scan_priority_test.go
  • logservice/logpuller/subscription_client.go
  • logservice/logpuller/subscription_client_test.go
  • pkg/config/debug.go
  • pkg/config/debug_test.go
  • utils/notifyqueue/notify_queue.go
  • utils/notifyqueue/notify_queue_test.go
💤 Files with no reviewable changes (2)
  • logservice/logpuller/region_req_cache.go
  • logservice/logpuller/region_req_cache_test.go

Comment thread logservice/logpuller/region_admission_controller.go
Comment thread logservice/logpuller/region_admission_controller.go
Comment thread logservice/logpuller/region_request_worker.go Outdated
Comment thread logservice/logpuller/region_request_worker.go
Comment thread logservice/logpuller/subscription_client.go
Comment thread logservice/logpuller/subscription_client.go
Comment thread utils/notifyqueue/notify_queue.go

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
logservice/logpuller/subscription_client_test.go (1)

858-866: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert the count after the abort.

Line 865 aborts the in-flight request but asserts nothing afterwards. The test therefore proves only that the total is 3. It does not prove that the in-flight request is the counted item. Add a second assertion after the abort to close that gap.

As per coding guidelines: "Prefer focused deterministic tests".

♻️ Proposed change
 	require.Equal(t, 3, store.requestedRegionCount())
 	require.True(t, req.abort())
+	require.Equal(t, 2, store.requestedRegionCount())
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@logservice/logpuller/subscription_client_test.go` around lines 858 - 866, In
the test around worker admission and request counting, add an assertion
immediately after req.abort() to verify the store’s requestedRegionCount
reflects removal of the aborted in-flight request. Keep the existing pre-abort
count assertion and use the existing requestedRegionCount helper.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@logservice/logpuller/subscription_client_test.go`:
- Around line 452-467: Await and assert the errgroup completion in both
handleRegions tests: logservice/logpuller/subscription_client_test.go lines
452-467 and 527-547. After canceling the context and receiving the handleRegions
error, call eg.Wait() and assert its expected result before each test returns.

In `@pkg/config/debug.go`:
- Line 27: Restore DefaultOldStartTsScanLowPriorityThreshold to 30 minutes so
scan-priority classification and region request admission behavior remain
unchanged.

In `@utils/priorityqueue/priority_queue_test.go`:
- Around line 96-108: Replace the scheduler-sensitive 10-millisecond
early-return select in the priority-queue Pop test with a direct wait for the
expected context.DeadlineExceeded result and nil task, avoiding timing-based
assertions. Apply the same change to the corresponding test block around the
second referenced section; add synchronization only if the test must explicitly
verify that Pop blocked.

---

Nitpick comments:
In `@logservice/logpuller/subscription_client_test.go`:
- Around line 858-866: In the test around worker admission and request counting,
add an assertion immediately after req.abort() to verify the store’s
requestedRegionCount reflects removal of the aborted in-flight request. Keep the
existing pre-abort count assertion and use the existing requestedRegionCount
helper.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 6455fcbf-27fe-4fa2-b2c1-c5a863e0d0a9

📥 Commits

Reviewing files that changed from the base of the PR and between 23b085b and 79d2f9a.

📒 Files selected for processing (12)
  • logservice/logpuller/priority_task.go
  • logservice/logpuller/priority_task_test.go
  • logservice/logpuller/region_admission_controller_test.go
  • logservice/logpuller/region_request_worker.go
  • logservice/logpuller/region_request_worker_test.go
  • logservice/logpuller/region_tracker.go
  • logservice/logpuller/subscription_client.go
  • logservice/logpuller/subscription_client_test.go
  • pkg/config/debug.go
  • utils/notifyqueue/notify_queue.go
  • utils/notifyqueue/notify_queue_test.go
  • utils/priorityqueue/priority_queue_test.go
🚧 Files skipped from review as they are similar to previous changes (8)
  • utils/notifyqueue/notify_queue_test.go
  • logservice/logpuller/region_admission_controller_test.go
  • logservice/logpuller/region_tracker.go
  • utils/notifyqueue/notify_queue.go
  • logservice/logpuller/priority_task.go
  • logservice/logpuller/priority_task_test.go
  • logservice/logpuller/region_request_worker.go
  • logservice/logpuller/subscription_client.go

Comment thread logservice/logpuller/subscription_client_test.go
Comment thread pkg/config/debug.go
Comment thread utils/priorityqueue/priority_queue_test.go Outdated
@lidezhu

lidezhu commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator Author

/test all

@lidezhu

lidezhu commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator Author

/retest

@lidezhu

lidezhu commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator Author

/test all

@lidezhu

lidezhu commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator Author

/test pull-cdc-mysql-integration-light

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

Labels

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: simplify region request worker lifecycle

1 participant