From f4606dd63d611701012c80ba289b4f273ae08ce2 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 23 Mar 2026 11:53:34 +0000 Subject: [PATCH] Fix race condition in AbstractProducer.run() allowing uninitialized execution Add two guards at the start of run(): (1) check shouldRun to handle the case where interrupt() is called before the executor thread starts, and (2) verify state is INITIALIZED to prevent producing keys without proper initialization. This fixes a sporadic IllegalStateException in ProducerOpenCL where the OpenCL context could be null if releaseProducer() ran before the executor thread started. https://claude.ai/code/session_017zaPZjBcrkXejsmU8h5F9r --- .../AbstractProducer.java | 8 ++++ .../AbstractProducerTest.java | 39 +++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/src/main/java/net/ladenthin/bitcoinaddressfinder/AbstractProducer.java b/src/main/java/net/ladenthin/bitcoinaddressfinder/AbstractProducer.java index f68616d..58e6ea5 100644 --- a/src/main/java/net/ladenthin/bitcoinaddressfinder/AbstractProducer.java +++ b/src/main/java/net/ladenthin/bitcoinaddressfinder/AbstractProducer.java @@ -69,6 +69,14 @@ public void releaseProducer() { @Override public void run() { + if (!shouldRun.get()) { + logger.info("Producer was interrupted before it started running."); + state = ProducerState.NOT_RUNNING; + return; + } + if (state != ProducerState.INITIALIZED) { + throw new IllegalStateException("Producer not initialized. Current state: " + state); + } state = ProducerState.RUNNING; while (shouldRun.get()) { try { diff --git a/src/test/java/net/ladenthin/bitcoinaddressfinder/AbstractProducerTest.java b/src/test/java/net/ladenthin/bitcoinaddressfinder/AbstractProducerTest.java index d5c2b21..49e7060 100644 --- a/src/test/java/net/ladenthin/bitcoinaddressfinder/AbstractProducerTest.java +++ b/src/test/java/net/ladenthin/bitcoinaddressfinder/AbstractProducerTest.java @@ -236,6 +236,45 @@ static void verifyInitProducer(AbstractProducer abstractProducer) { } // + @Test(expected = IllegalStateException.class) + public void run_notInitialized_illegalStateExceptionThrown() throws IOException, InterruptedException { + // arrange + CProducer cProducer = new CProducer(); + MockConsumer mockConsumer = new MockConsumer(); + Random random = new Random(1); + MockKeyProducer mockKeyProducer = new MockKeyProducer(keyUtility, random); + AbstractProducerTestImpl abstractProducerTestImpl = new AbstractProducerTestImpl(cProducer, mockConsumer, keyUtility, mockKeyProducer, bitHelper); + + // act + abstractProducerTestImpl.run(); + } + + @Test + public void run_interruptedBeforeStarted_stateSetToNotRunning() throws IOException, InterruptedException { + // arrange + CProducer cProducer = new CProducer(); + MockConsumer mockConsumer = new MockConsumer(); + Random random = new Random(1); + MockKeyProducer mockKeyProducer = new MockKeyProducer(keyUtility, random); + AbstractProducerTestImpl abstractProducerTestImpl = new AbstractProducerTestImpl(cProducer, mockConsumer, keyUtility, mockKeyProducer, bitHelper); + + Logger logger = mock(Logger.class); + abstractProducerTestImpl.setLogger(logger); + abstractProducerTestImpl.initProducer(); + abstractProducerTestImpl.interrupt(); + + // act + abstractProducerTestImpl.run(); + + // assert + assertThat(abstractProducerTestImpl.state, is(equalTo(ProducerState.NOT_RUNNING))); + + ArgumentCaptor logCaptor = ArgumentCaptor.forClass(String.class); + verify(logger, times(2)).info(logCaptor.capture()); + List arguments = logCaptor.getAllValues(); + assertThat(arguments.get(1), is(equalTo("Producer was interrupted before it started running."))); + } + @Test public void run_exceptionInProduceKeys_exceptionCaughtAndLoggedToError() throws IOException, InterruptedException { // arrange