Skip to content

Commit 79c0e2d

Browse files
committed
cluster_link: record the SR-sync task start time
totals_since_task_start.start_time is documented as the time the Schema Registry sync task started, but the task only ever stamped current_sync.summary.start_time -- the cumulative summary's start_time was left unset, so the admin API and rpk reported it as empty. Stamp it once per task instance on the first run. A fresh instance after a leadership change re-stamps it, so the field reflects the current leader's task. A mirroring_task unit test asserts the field is set after a full sync, and the e2e asserts it is present after a sync and carries a later value on the post-bounce instance.
1 parent 12b20e5 commit 79c0e2d

3 files changed

Lines changed: 27 additions & 12 deletions

File tree

src/v/cluster_link/schema_registry_sync/mirroring_task.cc

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,13 @@ ss::future<task::state_transition> mirroring_task::full_source_sync(
424424

425425
ss::future<task::state_transition>
426426
mirroring_task::run_impl(ss::abort_source& as) {
427+
// Stamp the cumulative summary's start time once per task instance (the
428+
// proto documents totals_since_task_start.start_time as the task start).
429+
// A fresh instance after a leadership change re-stamps it on its first run.
430+
if (!_status.totals_since_task_start.start_time.has_value()) {
431+
_status.totals_since_task_start.start_time = ::model::timestamp::now();
432+
}
433+
427434
// Consume the config-changed flag before any co_await so a concurrent
428435
// update_config during this run is not lost (it re-arms for the next run).
429436
const bool config_changed = std::exchange(_config_changed, false);

src/v/cluster_link/schema_registry_sync/tests/mirroring_task_test.cc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,9 @@ TEST_F(mirroring_task_test, full_sync_imports_and_reports) {
212212
EXPECT_EQ(status->last_full_sync->subject_versions_changed, 3);
213213
EXPECT_EQ(status->last_full_sync->errors, 0);
214214
EXPECT_EQ(status->totals_since_task_start.subject_versions_changed, 3);
215+
// The cumulative summary's start time is stamped once on the task's first
216+
// run; it was previously left unset (only current_sync carried a start).
217+
EXPECT_TRUE(status->totals_since_task_start.start_time.has_value());
215218

216219
// Create-only replication imports schema versions but never touches
217220
// compatibility configs, subject modes, or unsupported-feature handling,

tests/rptest/tests/cluster_linking_schema_registry_sync_test.py

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -388,7 +388,10 @@ def counters_ready() -> bool:
388388

389389
# Mapped-but-unimplemented counters must be zero (flagged above).
390390
totals = sr.totals_since_task_start
391-
# The cumulative summary is open-ended, so it carries no finish time.
391+
# The cumulative summary carries the task start time (stamped once on the
392+
# task's first run) and, being open-ended, has no finish time.
393+
assert totals.HasField("start_time"), totals
394+
assert totals.start_time.seconds > 0, totals
392395
assert not totals.HasField("finish_time"), totals
393396
for name in self.UNPOPULATED_COUNTERS:
394397
assert getattr(totals, name) == 0, (
@@ -718,6 +721,9 @@ def first_synced() -> bool:
718721
backoff_sec=1,
719722
err_msg="first task instance did not fully sync",
720723
)
724+
before_start = (
725+
self._admin_sr_status().totals_since_task_start.start_time.ToNanoseconds()
726+
)
721727

722728
# Move _schemas/0 leadership to another destination broker. The sync
723729
# task follows leadership, so a fresh instance takes over there.
@@ -738,22 +744,21 @@ def first_synced() -> bool:
738744
)
739745

740746
# The new instance resets its cumulative counters (documented on
741-
# totals_since_task_start), re-derives the destination inventory from
742-
# the replicated _schemas log, and completes a full sync that imports
743-
# nothing -- everything is already present. The first instance had
744-
# imported every version, so a cumulative subject_versions_changed of 0
745-
# marks the reset new instance; last_full_sync == 0 confirms the sync
746-
# was a no-op. This relies on the destination scan syncing the local
747-
# store first: without it, the freshly-elected leader could see a
748-
# stale view of its own store, spuriously re-import already-present
749-
# versions, and permanently inflate subject_versions_changed for this
750-
# instance (it only ever accumulates, so a transient race here would
751-
# never recover).
747+
# totals_since_task_start): a fresh, later start_time and
748+
# subject_versions_changed back at 0. It re-derives the destination
749+
# inventory from the replicated _schemas log and completes a full sync
750+
# that imports nothing (last_full_sync == 0) -- everything is already
751+
# present. This relies on the destination scan syncing the local store
752+
# first: without it, the freshly-elected leader could see a stale view
753+
# of its own store, spuriously re-import already-present versions, and
754+
# permanently inflate subject_versions_changed for this instance (it
755+
# only ever accumulates, so a transient race here would never recover).
752756
def re_derived() -> bool:
753757
sr = self._admin_sr_status()
754758
totals = sr.totals_since_task_start
755759
return (
756760
sr.HasField("last_full_sync")
761+
and totals.start_time.ToNanoseconds() > before_start
757762
and totals.subject_versions_changed == 0
758763
and totals.errors == 0
759764
and sr.last_full_sync.subject_versions_changed == 0

0 commit comments

Comments
 (0)