{"record":{"id":"7cf30523a8668893","repo":"apache/druid","slug":"cache-reference-is-null","errorCode":null,"errorMessage":"Cache reference is null","messagePattern":"Cache reference is null","errorType":"exception","errorClass":"IllegalStateException","httpStatus":null,"severity":"error","filePath":"extensions-core/lookups-cached-single/src/main/java/org/apache/druid/server/lookup/PollingLookup.java","lineNumber":119,"sourceCode":"        scheduledExecutorService.shutdown();\n      }\n      CacheRefKeeper cacheRefKeeper = refOfCacheKeeper.getAndSet(null);\n      if (cacheRefKeeper != null) {\n        cacheRefKeeper.doneWithIt();\n      }\n    }\n  }\n\n  @Override\n  @Nullable\n  public String apply(@Nullable String key)\n  {\n    if (key == null) {\n      return null;\n    }\n    final CacheRefKeeper cacheRefKeeper = refOfCacheKeeper.get();\n    if (cacheRefKeeper == null) {\n      throw new ISE(\"Cache reference is null\");\n    }\n    final PollingCache cache = cacheRefKeeper.getAndIncrementRef();\n    try {\n      if (cache == null) {\n        // it must've been closed after swapping while I was getting it.  Try again.\n        return this.apply(key);\n      }\n      return (String) cache.get(key);\n    }\n    finally {\n      if (cache != null) {\n        cacheRefKeeper.doneWithIt();\n      }\n    }\n  }\n\n  @Override\n  public List<String> unapply(@Nullable final String value)","sourceCodeStart":101,"sourceCodeEnd":137,"githubUrl":"https://github.com/apache/druid/blob/9b90983fd291f26935af934383ce360473179e4d/extensions-core/lookups-cached-single/src/main/java/org/apache/druid/server/lookup/PollingLookup.java#L101-L137","documentation":"PollingLookup.apply() reads from an internal AtomicReference<CacheRefKeeper> that holds the current cache. When the lookup has been closed (or not yet initialized), the reference is null and the method throws IllegalStateException instead of silently returning null. This is an internal invariant: a live lookup must always have a cache reference.","triggerScenarios":"Calling apply(key) after PollingLookup.close() has been called, or applying a lookup while it is being swapped/replaced during a lookup manager refresh race.","commonSituations":"A query references a lookup that was just removed or updated through the lookup configuration API; stale query plans holding old lookup objects; concurrent close-and-query during cluster config propagation.","solutions":["Refresh lookup configuration so all tasks/brokers have the lookup defined, then retry the query","Check for concurrent use of the lookup after close() in your code; stop using the lookup once closed","Upgrade Druid if you hit this during normal lookup swaps (known race windows were fixed over time)"],"exampleFix":"// before\nString val = pollingLookup.apply(key);\n// after\nif (!pollingLookup.isOpen()) { return null; }\nString val = pollingLookup.apply(key);","handlingStrategy":"try-catch","validationCode":"if (lookup == null || !lookup.isOpen()) { /* skip or refresh lookup config */ }","typeGuard":"boolean usable(PollingLookup l) { return l != null && l.isOpen(); }","tryCatchPattern":"try { return lookup.apply(key); } catch (IllegalStateException e) { if (e.getMessage().contains(\"Cache reference is null\")) { return fallbackValue; } throw e; }","preventionTips":["Don't hold PollingLookup references across lookup config updates","Check lookup existence in the lookup manager before issuing queries that use it","Re-fetch lookups from the LookupReferencesManager instead of caching instances"],"tags":["lookup","state","closed-resource","race-condition"],"backgroundTag":"internal-invariant-violation","analyzedSha":"9b90983fd291f26935af934383ce360473179e4d","analyzedAt":"2026-09-07T13:32:30.957Z","contentChangedAt":"2026-09-07T13:32:30.957Z","schemaVersion":2},"datasetVersion":"2026-09-14T11:17:12.474Z"}