{"record":{"id":"897ec98e9067122e","repo":"redis/jedis","slug":"controller-closed","errorCode":null,"errorMessage":"controller closed","messagePattern":"controller closed","errorType":"exception","errorClass":"RejectedExecutionException","httpStatus":null,"severity":"error","filePath":"src/main/java/redis/clients/jedis/MaintenanceEventController.java","lineNumber":84,"sourceCode":"\n  /**\n   * Test seam: an explicit marking scheduler (pre-populates the lazy field). The controller owns it\n   * and shuts it down on {@link #close()}.\n   */\n  static MaintenanceEventController from(MaintenanceNotificationsConfig cfg,\n      ScheduledExecutorService scheduler) {\n    return new MaintenanceEventController(cfg, scheduler);\n  }\n\n  /**\n   * The marking scheduler, created on first use.\n   */\n  private ScheduledExecutorService scheduler() {\n    ScheduledExecutorService s = scheduler;\n    if (s == null) {\n      synchronized (schedulerLock) {\n        if (closed) {\n          throw new RejectedExecutionException(\"controller closed\");\n        }\n        s = scheduler;\n        if (s == null) {\n          scheduler = s = newMaintenanceScheduler();\n        }\n      }\n    }\n    return s;\n  }\n\n  private static final AtomicInteger MAINTENANCE_THREAD_SEQ = new AtomicInteger();\n\n  private static ScheduledExecutorService newMaintenanceScheduler() {\n    String name = \"jedis-maintenance-\" + MAINTENANCE_THREAD_SEQ.incrementAndGet();\n    return Executors.newSingleThreadScheduledExecutor(r -> {\n      Thread t = new Thread(r, name);\n      t.setDaemon(true);\n      return t;","sourceCodeStart":66,"sourceCodeEnd":102,"githubUrl":"https://github.com/redis/jedis/blob/6dac31d4c224fb3257c216f3985340c6f500cdcb/src/main/java/redis/clients/jedis/MaintenanceEventController.java#L66-L102","documentation":"MaintenanceEventController lazily creates its ScheduledExecutorService. If scheduling is requested after the controller was closed, it throws RejectedExecutionException(\"controller closed\") rather than resurrecting a scheduler on a closed controller.","triggerScenarios":"Calling any controller method that schedules maintenance polling/rebind work after close() has been called on the controller (or after the owning client/pool was closed and shut the controller down).","commonSituations":"Background threads or push-event handlers still referencing the controller after the client/pool was closed; a race where a scheduled task fires during shutdown and tries to reschedule; reusing a closed client.","solutions":["Stop using the controller/client after close(); check the closed state before scheduling","Ensure push consumers and background tasks are cancelled before closing the controller","Create a new client/controller instance if you need to continue scheduling after a close"],"exampleFix":"// before\ncontroller.close();\ncontroller.schedulePolling(...); // RejectedExecutionException\n// after\nif (!controller.isClosed()) {\n  controller.schedulePolling(...);\n}","handlingStrategy":"try-catch","validationCode":"if (controller == null || controller.isClosed()) {\n  throw new IllegalStateException(\"Controller already closed\");\n}","typeGuard":null,"tryCatchPattern":"try {\n  controller.schedule(task, delay, unit);\n} catch (RejectedExecutionException e) {\n  logger.debug(\"Controller closed; skipping scheduling\", e);\n}","preventionTips":["Cancel push consumers and background jobs before closing the controller/client","Never reuse a client instance after close(); create a new one","Make shutdown ordering explicit: stop producers of scheduling requests first"],"tags":["redis","lifecycle","closed-resource","scheduler"],"backgroundTag":"invalid-state-transition","analyzedSha":"6dac31d4c224fb3257c216f3985340c6f500cdcb","analyzedAt":"2026-09-08T04:55:01.204Z","contentChangedAt":"2026-09-08T04:55:01.204Z","schemaVersion":2},"datasetVersion":"2026-09-16T04:17:20.429Z"}