-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[DCJ-507] Stairway with work queue enabled should be context-aware (#152
) * [DCJ-507] Flights queued in GCP Pub-Sub include calling thread context Context-aware Stairway previously assumed that the calling thread would have mapped diagnostic context (MDC) to persist to the child threads spawned for flight execution. This was not the case when Stairway is configured with a work queue enabled (e.g. GCP Pub-Sub). Here, flight information was written to the pub-sub topic without the calling thread's context, so when a message was processed and deserialized any logs emitted from that flight would be missing context set by the original calling thread (e.g. request ID). - Expanded QueueMessageReady to store calling thread context on construction - QueueMessageReady.process sets the MDC using its stored calling thread context, which then makes the flight context-aware - Expanded unit tests * Sonar: JUnit 5 test classes, methods should have default visibility * PR feedback MdcUtils: - Added public utility method callWithContext for running and returning a callable with MDC's context map temporarily overwritten - Remove public modifier on overwriteContext in favor of the above QueueMessageReady: - Use MdcUtils.callWithContext to simplify process definition - Revert process return strategy to return booleans directly rather than setting a boolean to return later - Remove unnecessary callingThreadContext setter (left existing setters untouched even though they can likely be removed: OOS) - Left public callingThreadContext getter unchanged: it is required to be public for serde operations Added unit test coverage for all changes. * Remove debugging print statement errantly checked in
- Loading branch information
1 parent
1410310
commit 6f74895
Showing
6 changed files
with
210 additions
and
37 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
114 changes: 95 additions & 19 deletions
114
stairway/src/test/java/bio/terra/stairway/queue/QueueMessageTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,32 +1,108 @@ | ||
package bio.terra.stairway.queue; | ||
|
||
import static org.hamcrest.CoreMatchers.equalTo; | ||
import static org.hamcrest.CoreMatchers.instanceOf; | ||
import static org.hamcrest.MatcherAssert.assertThat; | ||
import static org.junit.jupiter.api.Assertions.fail; | ||
import static org.mockito.Mockito.doThrow; | ||
import static org.mockito.Mockito.when; | ||
|
||
import bio.terra.stairway.exception.DatabaseOperationException; | ||
import bio.terra.stairway.impl.MdcUtils; | ||
import bio.terra.stairway.impl.StairwayImpl; | ||
import java.util.Map; | ||
import java.util.stream.Stream; | ||
import org.junit.jupiter.api.BeforeEach; | ||
import org.junit.jupiter.api.Tag; | ||
import org.junit.jupiter.api.Test; | ||
import org.junit.jupiter.api.extension.ExtendWith; | ||
import org.junit.jupiter.params.ParameterizedTest; | ||
import org.junit.jupiter.params.provider.MethodSource; | ||
import org.junit.jupiter.params.provider.ValueSource; | ||
import org.mockito.Mock; | ||
import org.mockito.junit.jupiter.MockitoExtension; | ||
import org.mockito.stubbing.Answer; | ||
import org.slf4j.MDC; | ||
|
||
@Tag("unit") | ||
public class QueueMessageTest { | ||
@ExtendWith(MockitoExtension.class) | ||
class QueueMessageTest { | ||
|
||
@Mock private StairwayImpl stairway; | ||
private static final String FLIGHT_ID = "flight-abc"; | ||
private static final Map<String, String> CALLING_THREAD_CONTEXT = | ||
Map.of("requestId", "request-abc"); | ||
|
||
@BeforeEach | ||
void beforeEach() { | ||
MDC.clear(); | ||
} | ||
|
||
private QueueMessageReady createQueueMessageWithContext(Map<String, String> expectedMdc) | ||
throws InterruptedException { | ||
return MdcUtils.callWithContext(expectedMdc, () -> new QueueMessageReady(FLIGHT_ID)); | ||
} | ||
|
||
private static Stream<Map<String, String>> message_serde() { | ||
return Stream.of(null, CALLING_THREAD_CONTEXT); | ||
} | ||
|
||
@ParameterizedTest | ||
@MethodSource | ||
void message_serde(Map<String, String> expectedMdc) throws InterruptedException { | ||
QueueMessageReady messageReady = createQueueMessageWithContext(expectedMdc); | ||
WorkQueueProcessor workQueueProcessor = new WorkQueueProcessor(stairway); | ||
|
||
// Now we add something else to the MDC, but it won't show up in our deserialized queue message. | ||
MDC.put("another-key", "another-value"); | ||
|
||
String serialized = workQueueProcessor.serialize(messageReady); | ||
QueueMessage deserialized = workQueueProcessor.deserialize(serialized); | ||
assertThat(deserialized, instanceOf(QueueMessageReady.class)); | ||
|
||
QueueMessageReady messageReadyCopy = (QueueMessageReady) deserialized; | ||
assertThat(messageReadyCopy.getFlightId(), equalTo(messageReady.getFlightId())); | ||
assertThat( | ||
messageReadyCopy.getType().getMessageEnum(), | ||
equalTo(messageReady.getType().getMessageEnum())); | ||
assertThat( | ||
messageReadyCopy.getType().getVersion(), equalTo(messageReady.getType().getVersion())); | ||
assertThat(messageReadyCopy.getFlightId(), equalTo(messageReady.getFlightId())); | ||
assertThat(messageReadyCopy.getCallingThreadContext(), equalTo(expectedMdc)); | ||
} | ||
|
||
@ParameterizedTest | ||
@ValueSource(booleans = {true, false}) | ||
void process(boolean resumeAnswer) throws InterruptedException { | ||
QueueMessageReady messageReady = createQueueMessageWithContext(CALLING_THREAD_CONTEXT); | ||
|
||
when(stairway.resume(FLIGHT_ID)) | ||
.thenAnswer( | ||
(Answer<Boolean>) | ||
invocation -> { | ||
assertThat( | ||
"MDC is set during processing", | ||
MDC.getCopyOfContextMap(), | ||
equalTo(CALLING_THREAD_CONTEXT)); | ||
return resumeAnswer; | ||
}); | ||
|
||
assertThat( | ||
"Message is considered processed when stairway.resume returns " + resumeAnswer, | ||
messageReady.process(stairway), | ||
equalTo(true)); | ||
assertThat("MDC is reverted after processing", MDC.getCopyOfContextMap(), equalTo(null)); | ||
} | ||
|
||
@Test | ||
public void messageTest() throws Exception { | ||
WorkQueueProcessor queueProcessor = new WorkQueueProcessor(null); | ||
QueueMessageReady messageReady = new QueueMessageReady("abcde"); | ||
String serialized = queueProcessor.serialize(messageReady); | ||
QueueMessage deserialized = queueProcessor.deserialize(serialized); | ||
if (deserialized instanceof QueueMessageReady) { | ||
QueueMessageReady messageReadyCopy = (QueueMessageReady) deserialized; | ||
assertThat(messageReadyCopy.getFlightId(), equalTo(messageReady.getFlightId())); | ||
assertThat( | ||
messageReadyCopy.getType().getMessageEnum(), | ||
equalTo(messageReady.getType().getMessageEnum())); | ||
assertThat( | ||
messageReadyCopy.getType().getVersion(), equalTo(messageReady.getType().getVersion())); | ||
assertThat(messageReadyCopy.getFlightId(), equalTo(messageReady.getFlightId())); | ||
} else { | ||
fail(); | ||
} | ||
void process_DatabaseOperationException() throws InterruptedException { | ||
QueueMessageReady messageReady = createQueueMessageWithContext(CALLING_THREAD_CONTEXT); | ||
|
||
doThrow(DatabaseOperationException.class).when(stairway).resume(FLIGHT_ID); | ||
|
||
assertThat( | ||
"Message is left on the queue when stairway.resume throws", | ||
messageReady.process(stairway), | ||
equalTo(false)); | ||
assertThat(MDC.getCopyOfContextMap(), equalTo(null)); | ||
} | ||
} |