{"record":{"id":"98deea8a2612b71f","repo":"go-kratos/kratos","slug":"watcher-stopped","errorCode":null,"errorMessage":"watcher stopped","messagePattern":"watcher stopped","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"info","filePath":"contrib/registry/zookeeper/watcher.go","lineNumber":16,"sourceCode":"package zookeeper\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"path\"\n\t\"sync/atomic\"\n\n\t\"github.com/go-zookeeper/zk\"\n\n\t\"github.com/go-kratos/kratos/v3/registry\"\n)\n\nvar _ registry.Watcher = (*watcher)(nil)\n\nvar ErrWatcherStopped = errors.New(\"watcher stopped\")\n\ntype watcher struct {\n\tctx    context.Context\n\tevent  chan zk.Event\n\tconn   *zk.Conn\n\tcancel context.CancelFunc\n\n\tfirst atomic.Bool\n\t// prefix for ZooKeeper paths or keys (used for filtering or identifying watched nodes)\n\tprefix string\n\t// the name of the service being watched in ZooKeeper\n\tserviceName string\n}\n\nfunc newWatcher(ctx context.Context, prefix, serviceName string, conn *zk.Conn) (*watcher, error) {\n\tw := &watcher{conn: conn, event: make(chan zk.Event, 1), prefix: prefix, serviceName: serviceName}\n\tw.ctx, w.cancel = context.WithCancel(ctx)\n\tgo w.watch(w.ctx)","sourceCodeStart":1,"sourceCodeEnd":34,"githubUrl":"https://github.com/go-kratos/kratos/blob/668db92c2c001e9552594ba5a8aede8456af6d7e/contrib/registry/zookeeper/watcher.go#L1-L34","documentation":"ErrWatcherStopped (contrib/registry/zookeeper/watcher.go:16) is returned by watcher.Next when the watcher's internal context is done (the '<-w.ctx.Done()' case around line 68-72). That context is cancelled by watcher.Stop()/Close or by cancellation of the context the watcher was created with, so the error means the watch lifecycle ended and Next must not be called again - it is a termination sentinel, not a ZooKeeper session event.","triggerScenarios":"Calling w.Next() after w.Stop(); the parent context passed at watch creation being cancelled (request context timeout, app shutdown) which cancels w.ctx; kratos' registry watcher loop tearing down during server Close(). Contrast with zk session/Event errors, which arrive on the event channel rather than as this sentinel.","commonSituations":"App shutdown where the goroutine draining the watcher outlives Stop(); passing a short-lived request ctx into registry.Watch instead of the app's lifetime context; test code that stops the watcher and then performs one more Next assertion.","solutions":["Break out of the consume loop on errors.Is(err, zookeeper.ErrWatcherStopped) - it is expected during shutdown","Pass a long-lived context (app lifetime) to the Watch call, not a per-request context","Ensure exactly one owner calls Stop() and the reader goroutine exits on this sentinel"],"exampleFix":"// before\nfor {\n    ins, err := w.Next()\n    if err != nil { log.Error(\"zk watch\", err) } // spams after Stop\n}\n\n// after\nfor {\n    ins, err := w.Next()\n    if errors.Is(err, zookeeper.ErrWatcherStopped) {\n        return // watcher closed, exit loop\n    }\n    if err != nil { return err }\n    update(ins)\n}","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"ins, err := w.Next()\nif err != nil {\n    if errors.Is(err, zookeeper.ErrWatcherStopped) {\n        return nil // watcher stopped: exit loop, do not restart\n    }\n    return err\n}","preventionTips":["Pass the application-lifetime context to registry.Watch, never a request context","Exit the consume loop on the sentinel instead of logging errors","Call Stop() exactly once and ensure the Next() reader has drained before teardown completes"],"tags":["registry","zookeeper","watcher","sentinel-error","lifecycle"],"backgroundTag":null,"analyzedSha":"668db92c2c001e9552594ba5a8aede8456af6d7e","analyzedAt":"2026-08-16T02:07:20.704Z","schemaVersion":2},"datasetVersion":"2026-08-16T03:17:38.424Z"}