Repository navigation
Add HTTP API concurrency regression test #186
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,10 +1,19 @@ | ||
| package uk.co.compendiumdev.thingifier.api.http.requests; | ||
|
|
||
| import java.util.HashMap; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
| import java.util.concurrent.CountDownLatch; | ||
| import java.util.concurrent.ExecutorService; | ||
| import java.util.concurrent.Executors; | ||
| import java.util.concurrent.Future; | ||
| import java.util.concurrent.TimeUnit; | ||
| import java.util.concurrent.atomic.AtomicBoolean; | ||
| import java.util.concurrent.atomic.AtomicInteger; | ||
| import org.junit.jupiter.api.Assertions; | ||
| import org.junit.jupiter.api.Test; | ||
| import uk.co.compendiumdev.thingifier.Thingifier; | ||
| import uk.co.compendiumdev.thingifier.adapter.http.messagehooks.HttpApiRequestHook; | ||
| import uk.co.compendiumdev.thingifier.api.http.HttpApiRequest; | ||
| import uk.co.compendiumdev.thingifier.api.http.HttpApiResponse; | ||
| import uk.co.compendiumdev.thingifier.api.http.ThingifierHttpApi; | ||
|
|
@@ -141,6 +150,73 @@ public void aPostRequestWillCreateNewSessionWithDatabase() { | |
| .getDefinitionWithSingularOrPluralNamed("thing"))); | ||
| } | ||
|
|
||
| @Test | ||
| public void generatedHttpApiRequestsAreSerializedOnTheThingifierModel() throws Exception { | ||
| // TODO: currently we synchronise at HttpAPI boundary to prevent GET happening before POST | ||
| // work is complete - this should only really be necessary for inmemory persistence - need | ||
| // to evaluate and push this lower when trying to scale because this might slow and impact | ||
| // the disk and database based persistence backed APIs | ||
| Thingifier thingifier = getTestThingifier(); | ||
| CountDownLatch firstRequestEnteredHook = new CountDownLatch(1); | ||
| CountDownLatch releaseFirstRequest = new CountDownLatch(1); | ||
| CountDownLatch secondRequestEnteredHook = new CountDownLatch(1); | ||
| AtomicInteger hookCallCount = new AtomicInteger(); | ||
| AtomicBoolean hooksRanInsideThingifierLock = new AtomicBoolean(true); | ||
|
|
||
| HttpApiRequestHook blockingHook = | ||
| (request, config) -> { | ||
| if (!Thread.holdsLock(thingifier)) { | ||
| hooksRanInsideThingifierLock.set(false); | ||
| } | ||
|
|
||
| int callNumber = hookCallCount.incrementAndGet(); | ||
| if (callNumber == 1) { | ||
| firstRequestEnteredHook.countDown(); | ||
| waitForLatch(releaseFirstRequest, "Timed out waiting to release POST"); | ||
| } else if (callNumber == 2) { | ||
| secondRequestEnteredHook.countDown(); | ||
| } | ||
|
|
||
| return null; | ||
| }; | ||
| ThingifierHttpApi api = new ThingifierHttpApi(thingifier, List.of(blockingHook), null); | ||
| ExecutorService executor = Executors.newFixedThreadPool(2); | ||
|
|
||
| try { | ||
| Future<HttpApiResponse> postResponse = | ||
| executor.submit( | ||
| () -> api.post(jsonRequest("/things", "{\"title\":\"thing1\"}"))); | ||
|
|
||
| Assertions.assertTrue( | ||
| firstRequestEnteredHook.await(1, TimeUnit.SECONDS), | ||
| "POST did not enter the HTTP API hook"); | ||
|
|
||
| Future<HttpApiResponse> getResponse = | ||
| executor.submit(() -> api.get(new HttpApiRequest("/things"))); | ||
|
|
||
| Assertions.assertFalse( | ||
| secondRequestEnteredHook.await(200, TimeUnit.MILLISECONDS), | ||
| "GET entered generated API processing before POST completed"); | ||
|
|
||
| releaseFirstRequest.countDown(); | ||
|
|
||
| Assertions.assertEquals(201, postResponse.get(1, TimeUnit.SECONDS).getStatusCode()); | ||
| HttpApiResponse actualGetResponse = getResponse.get(1, TimeUnit.SECONDS); | ||
|
|
||
| Assertions.assertTrue( | ||
| secondRequestEnteredHook.await(1, TimeUnit.SECONDS), | ||
| "GET did not enter generated API processing after POST completed"); | ||
| Assertions.assertEquals(200, actualGetResponse.getStatusCode()); | ||
| Assertions.assertTrue(actualGetResponse.getBody().contains("thing1")); | ||
| Assertions.assertTrue( | ||
| hooksRanInsideThingifierLock.get(), | ||
| "Generated API hook ran outside the Thingifier model lock"); | ||
|
Comment on lines
+211
to
+213
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This assertion adds a distinct implementation-level rule to a test that already verifies request ordering and POST-to-GET data visibility. Serialization could remain correct after moving the lock lower, as the TODO proposes, or after replacing the intrinsic monitor, yet this assertion would fail; conversely, a failure does not identify which behavior regressed. Keep the domain-level serialization scenario focused and test exact monitor ownership separately only if it is an intentional contract. AGENTS.md reference: AGENTS.md:L6-L8 Useful? React with 👍 / 👎. |
||
| } finally { | ||
| releaseFirstRequest.countDown(); | ||
| executor.shutdownNow(); | ||
| } | ||
| } | ||
|
|
||
| @Test | ||
| public void strictPostRejectsNumericValueForStringField() { | ||
| Thingifier thingifier = strictTypedThingifier(); | ||
|
|
@@ -400,4 +476,15 @@ private HttpApiRequest patchRequest( | |
| .setHeaders(Map.of("content-type", contentType)) | ||
| .setBody(body); | ||
| } | ||
|
|
||
| private void waitForLatch(final CountDownLatch latch, final String timeoutMessage) { | ||
| try { | ||
| if (!latch.await(2, TimeUnit.SECONDS)) { | ||
| throw new AssertionError(timeoutMessage); | ||
| } | ||
| } catch (InterruptedException e) { | ||
| Thread.currentThread().interrupt(); | ||
| throw new AssertionError(timeoutMessage, e); | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the executor's second worker is not scheduled during this 200 ms window, the latch remains closed even after synchronization is removed; the test then releases the POST, the delayed GET runs, and every later assertion can pass. Ensure the GET worker has actually attempted the API call—for example, by capturing it in the expected blocked state—before treating failure to enter the hook as evidence of serialization.
AGENTS.md reference: AGENTS.md:L3-L4
Useful? React with 👍 / 👎.