implement /v1/markets/status/ endpoint + integration testing pipeline#2
Merged
Merged
Conversation
|
The author of this PR, MarketDataDev03, is not an activated member of this organization on Codecov. |
Collaborator
Author
|
/test-all |
Contributor
|
❌ On-demand JDK matrix |
Collaborator
Author
|
/test-all |
Contributor
|
❌ On-demand JDK matrix |
Collaborator
Author
|
/test-all |
Contributor
|
✅ On-demand JDK matrix |
Collaborator
Author
|
integrationtestfull |
bba1455 to
d617db2
Compare
d617db2 to
63fc0a6
Compare
MarketDataDev01
approved these changes
May 12, 2026
This file contains hidden or 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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
feat: implement /v1/markets/status/ endpoint + transport layer + AsyncSemaphore
First endpoint of the SDK. Lays down the HTTP transport layer, the wire-format adapter pattern, the integration-testing pipeline, and a custom async-safe concurrency limiter that every subsequent endpoint will reuse —
markets/statusis the first concrete consumer of all of it.What's included
Public API —
com.marketdata.sdk.marketsMarketsResource(façade) reachable viaclient.markets(). Three overloads each in sync + async (status(),status(date),status(from, to)) per ADR-006.MarketStatus(record) andDailyStatus(LocalDate, boolean)— the domain model.MarketStatusis@JsonDeserialize(using = MarketStatusDeserializer.class), expanding the API's parallel-arrays JSON into idiomatic typed objects (SDK requirements §11.1).Concurrency layer —
AsyncSemaphore(the headline architectural change)The 50-permit pool from SDK requirements §12 was originally backed by
java.util.concurrent.Semaphore. That works for sync callers but silently breaks the async contract:Semaphore.acquire()parks the calling thread before theCompletableFutureis returned, so a fan-out beyond 50 in-flight requests:Dispatchers.Defaulthas ~coresthreads — a few stuckacquire()s and the dispatcher is dead);BlockedThreadCheckerwill eventually crash the JVM);ForkJoinPool.commonPool()when fan-out happens viaparallelStream()— workers stuck onacquire()aren't available to unrelated parallel work elsewhere in the JVM.This PR replaces the JDK semaphore with a purpose-built
AsyncSemaphoreincom.marketdata.sdk.internal.http:acquire()returns aCompletableFuture<Void>instead of parking the caller's thread.thenComposechain runs synchronously in the calling thread.Deque. The caller's thread is never parked; it returns to its dispatcher / event loop immediately.release()either transfers the permit directly to the next live waiter (completing its future) or increments the counter when the queue is empty. Cancelled or timed-out waiters are skipped on release so a cancelledacquirecannot leak a permit.complete(...)always runs outside the internal lock to avoid re-entrancy hazards from caller-attached callbacks.HttpTransport.executeAsyncis nowacquire().thenCompose(unused -> dispatch(...)). The 50-permit guarantee from §12 is unchanged; what changed is "what does unavailable feel like to the caller" (park()→ pendingCompletableFuture).Defensive fix — synchronous-throw permit leak (companion to the AsyncSemaphore work)
If
httpClient.sendAsync(...)throws synchronously (rare: malformed request, internal NPE, OOM allocating buffers), thewhenComplete(release)chain never forms and the permit was previously leaked forever.HttpTransport.dispatchnow wraps thesendAsynccall in a try/catch that releases the permit before propagating;Errortypes are rethrown unwrapped (so a realOutOfMemoryErrorisn't masked asNetworkError),RuntimeExceptions are surfaced as aNetworkErrorfailed future. Without this, a long-lived process eventually deadlocks once 50 such failures accumulate.Cross-cutting transport layer —
com.marketdata.sdk.internal.httpReusable for every future endpoint; nothing here is markets-specific.
HttpTransport— single point of contact between resources and the network. Owns theHttpClient, theAsyncSemaphore, theObjectMapper, and the latestRateLimitssnapshot. Async-first (executeAsync) with sync wrapper (executeSync) that unwrapsCompletionExceptionper ADR-006. A package-private secondary constructor takes an injectableHttpClientfor testing (used by the stub-based unit tests; the public API surface is unchanged).RequestSpec— declarative request description (path + ordered query params).HttpStatusMapper— HTTP status →MarketDataExceptionsubtype mapping per SDK requirements §9.1.RateLimitHeaders— parsesx-api-ratelimit-*headers.{"s":"no_data"}is the API's empty-result sentinel); other 4xx/5xx throw typed exceptions.Wire-format adapter —
com.marketdata.sdk.internal.wire.marketsMarketStatusDeserializer— Jackson custom deserializer for the parallel-arrays format. Normalizes unix timestamps toLocalDatein US/Eastern (SDK requirements §11.4); mapss:"no_data"to an empty list.MarketDataClient
Refactored to delegate all HTTP-shaped concerns to the new
HttpTransport. Addsclient.markets().getRateLimits()now reads from the transport's snapshot.CI changes
pull-request.yml— runs unit tests + Spotless + JaCoCo + Codecov on JDK 17 only. Removed the auto-running integration-tests job — IT is opt-in on PRs now.pr-integration-on-demand.yml— triggered by commentingintegrationtest(JDK 17) orintegrationtestfull(matrix{17, 21, 25}) on an open PR. Gated towrite+permissions, reacts 👀, posts a result summary comment. Aggregator jobIntegration tests passproduces a single check name that branch protection should require — covers both modes uniformly.main.yml— integration-tests job now runs the full matrix{17, 21, 25}mandatorily on every push to main. Fails the pipeline ifMARKETDATA_TOKENsecret is missing.JAVA_HOMEfor stability (CI workflows ordersetup-java'sjava-versionso the matrix JDK is first and 17 is last); toolchain forks compile/test JDKs as needed via-PtestJdk=N.build.gradle.kts—-PtestJdk=Nis wired to allTesttasks (test+integrationTest) viatasks.withType<Test>().configureEach, so the matrix flag works uniformly for unit and integration runs.