Repository navigation
Conversation
Signed-off-by: cxhello <caixiaohuichn@gmail.com>
…proto Signed-off-by: cxhello <caixiaohuichn@gmail.com>
Signed-off-by: cxhello <caixiaohuichn@gmail.com>
…-listener support Replace the by-value cacheData stored in cache.ConcurrentMap with a pointer-based configCacheHolder (clients/config_client/config_cache_holder.go), fixing the long-standing bug where a second ListenConfig call on the same dataId/group/tenant silently dropped the first listener instead of appending to it. - cacheData is now always referenced by pointer and carries its own mutex plus a listeners []*listenerWrap slice, so multiple independent listeners can be registered on the same key. - ListenConfig now does get-or-create + an atomic reviveAndAddListener; an existing entry is revived (discard=false) rather than replaced, with the revive and the listener append happening in a single critical section so a concurrent CancelListenConfig can't interleave between them and leave the entry discard==true with a non-empty listeners slice. CancelListenConfig marks the entry discarded instead of removing it outright, and is a no-op on unknown keys. - configCacheHolder.removeIfDiscarded re-checks discard && no-listeners under lock before deleting, so a concurrent revive/addListener can't race a removal. - executeConfigListen/buildListenTask/refreshContentAndCheck and config_connection_event_listener.go/config_proxy.go are adapted to read from the holder; their single-batch listen=true behavior and per-listener notify-on-md5-change semantics are kept equivalent to today's behavior. Cancel-batch/listen=false executor semantics and real per-listener watermark delivery are left for follow-up tasks. - Removed the dead ConfigClient.localConfigs field. Signed-off-by: cxhello <caixiaohuichn@gmail.com>
…oup#629) executeConfigListen now partitions each round into two independent batches: discarded entries are sent as a Listen=false batch (grouped by taskId) before any Listen=true batch, and are only reaped from the holder via removeIfDiscarded once that batch gets a successful response; a transport error or non-success response leaves them in place for the next round to retry. This fixes CancelListenConfig never actually telling the server to stop pushing a key. Listen=true batches keep the existing changedConfigs handling (refreshContentAndCheck) but now mark isSyncWithServer based on the batch's own caches instead of the full holder snapshot, avoiding cross-taskId contamination between concurrent batches. Also make the config-change push handler ack cache misses with a success response instead of returning nil, matching the documented "push handler only marks isSyncWithServer=false and rings the bell" contract. Signed-off-by: cxhello <caixiaohuichn@gmail.com>
Add cacheData.notifyListeners(chain): snapshots md5/content/encryptedDataKey and not-yet-caught-up listener wraps under cData.mu, then releases the lock before running the filter chain and invoking callbacks. Each callback is recover-wrapped so a panicking listener neither crashes the executor goroutine nor blocks delivery to other listeners on the same key, and its watermark only advances to the snapshotted md5 on a normal return -- a panic leaves it unchanged so the content is replayed on the next round. A filter chain error skips the whole round without advancing any watermark. Replace the Task 3 interim notifyListenersIfChanged path (whole-entry, fire-and-forget goroutines) with this per-listener delivery and rewire refreshContentAndCheck to call cData.notifyListeners directly. Signed-off-by: cxhello <caixiaohuichn@gmail.com>
ListenConfig previously registered a listener even after CloseClient had already torn down the client's context and rpc connection, leaving the caller with a listener that would never be served again. Check isClosed under client.mutex at ListenConfig entry (same nacos-group#904 semantics used for naming) and return an error instead. Also add regression coverage for CancelListenConfig and double CloseClient staying panic-free after close. Signed-off-by: cxhello <caixiaohuichn@gmail.com>
… coverage
Add TestIntegrationConfig{MultiListener,CancelStopsPush,CasPublish,
ListenSurvivesRestart} covering: two independent listeners on the same
key both firing on publish; CancelListenConfig actually stopping
further pushes (nacos-group#629); PublishConfig's CasMd5 rejecting stale writes
while leaving content untouched and accepting a correct cas (nacos-group#727);
and a listener surviving a full server restart (nacos-group#694). The restart
test is skipped unless NACOS_CONTAINER_NAME is set and shells out to
plain `docker restart`, matching reconnect_test.go's existing pattern.
Verified locally against both probe-nacos3 (3.2.0, port 8848) and
probe-nacos2 (2.5.2, port 8858) with -race -count=1.
Signed-off-by: cxhello <caixiaohuichn@gmail.com>
ListenConfig checked isClosed under client.mutex but committed the new listener (getOrCreate + reviveAndAddListener) outside that lock, so a CloseClient landing in between registered a live listener on an already-shut-down client while still returning nil. Re-check isClosed under client.mutex right after the commit and roll back via cData.markDiscard() when a close won the race. Export ErrConfigClientClosed (mirroring naming's ErrFuzzyWatchClientClosed) and return it from both ListenConfig closed paths so callers can errors.Is instead of matching error text. Fix asyncNotifyListenConfig leaking a goroutine per call once the client is closed: it blocked forever sending on listenExecute after the listen executor loop had already exited via ctx.Done(). Select the send against ctx.Done() too. Signed-off-by: cxhello <caixiaohuichn@gmail.com>
TestListenConfigRaceWithCloseClientNeverLeavesLiveListenerOnClosedClient asserted a <=10% threshold on how often a live listener survived a nil-error ListenConfig racing CloseClient. That "benign occurrence" rate is machine-dependent and flaked in CI (52/500 = 10.4% on GitHub runners), so drop the percentage assertion entirely. The ghost-listener semantics are already pinned deterministically by TestListenConfigCommitRollsBackWhenCloseWinsRace. This test now keeps only the concurrent ListenConfig/CloseClient hammer loop as a -race exerciser for the Finding 1 locking, with scheduling-independent invariants: the entry must never be observed with discard==true and non-empty listeners, and once CloseClient has definitely returned, a further ListenConfig call must fail with errors.Is(err, ErrConfigClientClosed) and must not grow the listener count. Signed-off-by: cxhello <caixiaohuichn@gmail.com>
|
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## v3.x-dev #914 +/- ##
===========================================
Coverage ? 40.72%
===========================================
Files ? 102
Lines ? 7206
Branches ? 0
===========================================
Hits ? 2935
Misses ? 4095
Partials ? 176 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
这里建议合并前修复一个 P1 回归:不要在配置监听主循环中同步执行用户回调。
针对当前 head
在 base 建议保留异步回调,并用每个 listener 的 in-flight 状态避免重复并发投递,成功后再推进 |
The listen executor is the single goroutine driving queries, notifications and cancel batches for every config; delivering callbacks inline on it was a regression from the pre-v3 behavior (which launched every callback with go), letting one slow callback stall all other configs and piling up one blocked bell goroutine per pending notification. Callbacks now run on their own delivery goroutine, gated per listener by an inFlight flag so a single listener is never invoked concurrently with itself and a slow listener holds exactly one goroutine. The watermark advances only on normal return, in the same critical section that clears the flag; a wrap left trailing the entry's md5 (newer change during the callback, or a panic) rings the bell and the executor's round-entry sweep re-notifies promptly. The bell itself is now a 1-buffered coalescing channel with a non-blocking send, removing the goroutine-per-notification pile-up entirely. Signed-off-by: cxhello <caixiaohuichn@gmail.com>
|
Verified and fixed — this was a real regression, not a style choice. The base implementation launched every callback with Fix (commit 9dc559b), along the lines you suggested:
Regression tests drive the real
Full unit suite and both-version integration (2.5.2 / 3.2.0, |
|
异步回调这部分修复已复核:相关回归测试本地 还有一个 P2 建议一并修复:持续 panic 的 listener 会触发无间隔重试。
本地用真实 建议采用最小修改:保留异步投递、 // 在 c.mu 内计算,在锁外调用 wake。
shouldWake := delivered && md5 != c.md5 && !c.discard
这与 Java SDK 的失败处理思路一致:CacheData 捕获异常后只清理执行状态,不立即唤醒,后续由 ClientWorker 的监听循环再次检查。没有其他事件时通常等约 5 秒;其他推送仍可能提前触发重试,这不是严格的失败冷却时间,也无需为此新增复杂的重试队列或退避机制。 建议补两个真实监听循环测试:持续 panic 且没有新事件时不会高速自重试;失败回调恢复后,即使服务端配置未再变化,也能在后续轮询中补发成功。现有“成功回调完成后追上新版本”的测试继续保留。 |
…elf-waking A failed delivery leaves its watermark lagging by construction, and waking on that lag redelivers the same content immediately -- which panics again, a self-sustaining busy loop the coalescing bell cannot damp (measured 12k callback invocations in 600ms). Wake now fires only when a delivery succeeded and the entry's md5 moved past the delivered snapshot meanwhile; failed deliveries are retried by the regular poll round's re-notify sweep, matching the Java client's CacheData/ClientWorker failure handling. Signed-off-by: cxhello <caixiaohuichn@gmail.com>
|
Confirmed and fixed in 48f3294. Reproduced first: the new busy-loop regression test (real The fix is your minimal change, applied verbatim: shouldWake := delivered && md5 != c.md5 && !c.discardcomputed inside Both requested tests drive the real
Full unit suite and both-version integration (2.5.2 / 3.2.0, |
What is this PR for
PR5a of the v3 roadmap (#879, sub-issue #913): migrates the Config module's wire types to nacos-sdk-proto and structurally rebuilds the config listen state machine. Config-side FuzzyWatch (#859) follows separately as PR5b on top of this branch.
Proto migration (same adapter pattern as PR2/PR4)
ConfigQueryRequest,ConfigPublishRequest,ConfigRemoveRequest,ConfigBatchListenRequest) implementProtoMessage()and encode throughPayloadCodec; a wire-parity test pins the protojson key set against the legacy JSON body (guarding thejson_namedefect class found in PR4).ConfigQueryResponse,ConfigPublishResponse,ConfigRemoveResponse,ConfigChangeBatchListenResponse) and theConfigChangeNotifyRequestserver push decode throughproto_dispatchadapters back into the legacy structs, so downstream code is unchanged.successis derived fromresultCode(proto has no such field), matching the PR2 rule.ConfigQueryRequest, which this PR migrates).ConfigQueryResponse.Tagis a deadboolfield (proto'stagis a string); the adapter intentionally does not map it.Listen state machine rebuild (Java
ClientWorker/CacheDataparity)The by-value
cacheDatacopies stored incache.ConcurrentMap(copy-mutate-set races, single silently-overwritten listener) are replaced by a pointer-basedconfigCacheHolderwith per-entry locking:cacheData.listenersis a list of wraps, each with its ownlastCallMd5watermark. Delivery snapshots state under the entry lock, releases it, then per listener: filter-chain decrypt →recover-wrapped callback → watermark advances only on normal return. A panicking listener is replayed next round and never blocks its siblings; a filter-chain error skips the round without advancing any watermark.CancelListenConfigmarks the entrydiscardand rings the bell; the single executor goroutine sendsConfigBatchListenRequest{listen=false}batches (grouped by taskId, before the listen batches) and only removes an entry after the server confirmed — re-checking under lock that no listener re-registered while the cancel was in flight. This also fixes the corner where cancelling the only listened config sent nothing at all.ListenConfigre-checksisClosedunder the client mutex after committing its listener and rolls back if a concurrentCloseClientwon the race; the closed path returns the exported sentinelErrConfigClientClosed. The bell send selects against the client context so post-close notifications cannot leak goroutines.Issues
listen=falserequest stream; server behavior (push stops afterlisten=false) was probe-verified against Nacos 2.5.2 and 3.2.0; an integration test pins the user-visible contract (no callbacks after cancel). Note the integration test alone cannot prove server-side stop — cancel clears local listeners synchronously — hence the unit + probe layers.docker restartprobes against 2.5.2 and 3.2.0 show listen callbacks resume after a server restart (the connection-event listener synced from master already re-syncs). A restart integration regression test pins it.v3.x-dev: both server versions reject a stalecasMd5with resultCode 500 andPublishConfigpropagates(false, error). A dual-version CAS integration regression test pins it. No typed sentinel is introduced: the only discriminator is the server's message string, which is too fragile to key on.Behavior changes vs v2 (please review deliberately)
ListenConfigon the same key now appends listeners (Java parity). v2 silently dropped every callback after the first. Code that calledListenConfigrepeatedly as an idempotent re-registration (e.g. in reconnect loops) will now accumulate listeners and receive duplicate callbacks — this is the most user-visible change in the PR.CancelListenConfignow notifies the server and removes all listeners of the key (same key-level granularity as v2's local removal). Watcher-scoped cancellation (handle-based, likeFuzzyWatchHandle) is deliberately deferred — proposed forSubscribe/ListenConfigjointly in Repeated subscriptions result in multiple callbacks #655.ListenConfigon a closed client returnsErrConfigClientClosedinstead of silently registering into a dead client.Known inherited limitation
A server push that lands between an executor round's response and its sync-flag update can be overwritten and only recovered by the 3-minute full resync (the window is microseconds; the previous code had a strictly larger version of the same window across all task shards). Kept as-is, documented here.
Testing
-race -count=1green, including targeted concurrency tests (listen/cancel hammer with invariant checks, cancel-during-drain, revive-during-cancel-in-flight, panic replay, watermark skip).-tags=integration -race -count=1) green against both Nacos 2.5.2 and 3.2.0, including the new multi-listener / cancel / CAS / server-restart tests.