{"record":{"id":"b8f62d00bf9339e2","repo":"Netflix/Hystrix","slug":"trying-to-start-null-batch-which-means-it-was-shut","errorCode":null,"errorMessage":"Trying to start null batch which means it was shutdown already.","messagePattern":"Trying to start null batch which means it was shutdown already\\.","errorType":"exception","errorClass":"IllegalStateException","httpStatus":null,"severity":"error","filePath":"hystrix-core/src/main/java/com/netflix/hystrix/collapser/RequestCollapser.java","lineNumber":111,"sourceCode":"            final Observable<ResponseType> response;\n            if (arg != null) {\n                response = b.offer(arg);\n            } else {\n                response = b.offer( (RequestArgumentType) NULL_SENTINEL);\n            }\n            // it will always get an Observable unless we hit the max batch size\n            if (response != null) {\n                return response;\n            } else {\n                // this batch can't accept requests so create a new one and set it if another thread doesn't beat us\n                createNewBatchAndExecutePreviousIfNeeded(b);\n            }\n        }\n    }\n\n    private void createNewBatchAndExecutePreviousIfNeeded(RequestBatch<BatchReturnType, ResponseType, RequestArgumentType> previousBatch) {\n        if (previousBatch == null) {\n            throw new IllegalStateException(\"Trying to start null batch which means it was shutdown already.\");\n        }\n        if (batch.compareAndSet(previousBatch, new RequestBatch<BatchReturnType, ResponseType, RequestArgumentType>(properties, commandCollapser, properties.maxRequestsInBatch().get()))) {\n            // this thread won so trigger the previous batch\n            previousBatch.executeBatchIfNotAlreadyStarted();\n        }\n    }\n\n    /**\n     * Called from RequestVariable.shutdown() to unschedule the task.\n     */\n    public void shutdown() {\n        RequestBatch<BatchReturnType, ResponseType, RequestArgumentType> currentBatch = batch.getAndSet(null);\n        if (currentBatch != null) {\n            currentBatch.shutdown();\n        }\n\n        if (timerListenerReference.get() != null) {\n            // if the timer was started we'll clear it so it stops ticking","sourceCodeStart":93,"sourceCodeEnd":129,"githubUrl":"https://github.com/Netflix/Hystrix/blob/5ce3bc58c38e7ca60ef2fe0e516e390e294ad941/hystrix-core/src/main/java/com/netflix/hystrix/collapser/RequestCollapser.java#L93-L129","documentation":"RequestCollapser.createNewBatchAndExecutePreviousIfNeeded() requires the previous batch reference to be non-null; null means shutdown() already swapped the batch reference to null via batch.getAndSet(null), so trying to start a new batch post-shutdown throws IllegalStateException('Trying to start null batch which means it was shutdown already.').","triggerScenarios":"A request arrives via offer() after RequestCollapser.shutdown() ran — the read of the batch field yielded null and code falls into createNewBatchAndExecutePreviousIfNeeded(null); a race between collapser request submission (Scope.REQUEST teardown, Hystrix.reset(), or request-context shutdown) and an in-flight command.","commonSituations":"Hystrix.reset() in test @After racing with still-executing collapsed commands; application shutdown while collapsed requests are in flight; request-scoped collapsers used after the request context was torn down; typically seen as a rare race in tests rather than production.","solutions":["Ensure HystrixRequestContext.shutdown()/Hystrix.reset() happens only after all in-flight collapsers complete (drain futures first in @After/test teardown)","Avoid submitting new collapsed requests on a thread whose context has been shut down; re-initialize the context","Retry the collapser invocation on a fresh context if the race window is inherent","Upgrade Hystrix patch level if hitting a known collapser shutdown race"],"exampleFix":"// before\n@After public void tearDown() { Hystrix.reset(); } // races in-flight collapsers\n// after\n@After public void tearDown() {\n  pendingFutures.forEach(f -> f.get(2, TimeUnit.SECONDS)); // drain first\n  Hystrix.reset();\n}","handlingStrategy":"retry","validationCode":"// verify context/collapser usability before submitting\nif (!HystrixRequestContext.isCurrentThreadInitialized()) { HystrixRequestContext.initializeContext(); /* ... */ }","typeGuard":"null","tryCatchPattern":"catch (IllegalStateException e) { if (e.getMessage().contains(\"null batch\")) { // shutdown race: re-initialize request context and retry once with a new collapser instance } }","preventionTips":["Never submit collapsed work after HystrixRequestContext.shutdown()/Hystrix.reset()","Drain in-flight collapser futures before resetting Hystrix in tests","Keep collapser submission on the thread that owns the request context"],"tags":["java","hystrix","collapser","race-condition","shutdown"],"backgroundTag":null,"analyzedSha":"5ce3bc58c38e7ca60ef2fe0e516e390e294ad941","analyzedAt":"2026-08-14T10:55:35.600Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}