{"record":{"id":"36bc3ec04b1aba13","repo":"grpc/grpc-java","slug":"can-t-cancel-resource-watch-with-active-watchers-p","errorCode":null,"errorMessage":"Can't cancel resource watch with active watchers present","messagePattern":"Can't cancel resource watch with active watchers present","errorType":"exception","errorClass":"IllegalStateException","httpStatus":null,"severity":"error","filePath":"xds/src/main/java/io/grpc/xds/client/XdsClientImpl.java","lineNumber":808,"sourceCode":"      metadata = ResourceMetadata.newResourceMetadataRequested();\n\n      if (respTimer != null) {\n        respTimer.cancel();\n      }\n      respTimer = syncContext.schedule(\n          new ResourceNotFound(), timeoutSec, TimeUnit.SECONDS, timeService);\n    }\n\n    void stopTimer() {\n      if (respTimer != null && respTimer.isPending()) {\n        respTimer.cancel();\n        respTimer = null;\n      }\n    }\n\n    void cancelResourceWatch() {\n      if (isWatched()) {\n        throw new IllegalStateException(\"Can't cancel resource watch with active watchers present\");\n      }\n      stopTimer();\n      String message = \"Unsubscribing {0} resource {1} from server {2}\";\n      XdsLogLevel logLevel = XdsLogLevel.INFO;\n      if (resourceDeletionIgnored) {\n        message += \" for which we previously ignored a deletion\";\n        logLevel = XdsLogLevel.FORCE_INFO;\n      }\n      logger.log(logLevel, message, type, resource, getTarget());\n    }\n\n    boolean isWatched() {\n      return !watchers.isEmpty();\n    }\n\n    boolean hasResult() {\n      return data != null || absent;\n    }","sourceCodeStart":790,"sourceCodeEnd":826,"githubUrl":"https://github.com/grpc/grpc-java/blob/64daddc1f3d1975670f769f3e97bde8b2ba32d25/xds/src/main/java/io/grpc/xds/client/XdsClientImpl.java#L790-L826","documentation":"XdsClientImpl throws this IllegalStateException when cancelResourceWatch() is called on a resource that still has active watchers registered. A resource watch can only be unsubscribed once every watcher has been cancelled; the internal invariant is that isWatched() must be false before teardown of the subscription and its response timer.","triggerScenarios":"Calling XdsClient.cancelResourceWatch() (or internally via watch teardown) while other components still hold watchers for the same resource name on the same XdsClient.","commonSituations":"Multiple subscribers share one XdsClient (e.g. multiple gRPC channels using xDS for the same cluster/listener resource) and one cancels without the others; lifecycle bugs where a watcher is re-registered during shutdown; races between cancel and re-watch.","solutions":["Ensure all XdsClient.Watcher instances for the resource are cancelled (via their handles) before calling cancelResourceWatch","Check for duplicate registrations that leave a stale watcher alive; only cancel once per watcher","Guard the cancel path so it only runs when your component owns the last watcher","If it appears during shutdown races, synchronize watcher registration/cancellation on a single owner"],"exampleFix":"// before\nclient.cancelResourceWatch(resourceName); // IllegalStateException: watchers still active\n// after\nwatchHandle1.cancel();\nwatchHandle2.cancel();\nclient.cancelResourceWatch(resourceName); // safe once isWatched() is false","handlingStrategy":"try-catch","validationCode":"// Track your own watcher count per resource\nif (activeWatchers.get(resourceName) > 0) { throw new IllegalStateException(\"cancel watchers first\"); }","typeGuard":"boolean safeToCancel = activeWatchers.containsKey(resourceName) && activeWatchers.get(resourceName) == 0;","tryCatchPattern":"try {\n  client.cancelResourceWatch(resourceName);\n} catch (IllegalStateException e) {\n  // a watcher is still active; cancel remaining watcher handles then retry\n}","preventionTips":["Cancel every Watcher handle before unsubscribing the resource","Keep a single owner component responsible for watch lifecycle","Avoid re-registering watchers during shutdown","Log watcher add/remove to spot leaks"],"tags":["xds","grpc","state","watcher-lifecycle"],"backgroundTag":"invalid-state-transition","analyzedSha":"64daddc1f3d1975670f769f3e97bde8b2ba32d25","analyzedAt":"2026-09-08T06:14:57.704Z","contentChangedAt":"2026-09-08T06:14:57.704Z","schemaVersion":2},"datasetVersion":"2026-09-17T15:17:12.973Z"}