Skip to content

implement /v1/markets/status/ endpoint + integration testing pipeline#2

Merged
MarketDataDev03 merged 8 commits into
mainfrom
01_endpoint_v1_markets_status
May 12, 2026
Merged

implement /v1/markets/status/ endpoint + integration testing pipeline#2
MarketDataDev03 merged 8 commits into
mainfrom
01_endpoint_v1_markets_status

Conversation

@MarketDataDev03

@MarketDataDev03 MarketDataDev03 commented May 5, 2026

Copy link
Copy Markdown
Collaborator

feat: implement /v1/markets/status/ endpoint + transport layer + AsyncSemaphore

Branch 01_endpoint_v1_markets_statusmain · 6 commits · 38 files · +2646 / −85

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/status is the first concrete consumer of all of it.

What's included

Public API — com.marketdata.sdk.markets

  • MarketsResource (façade) reachable via client.markets(). Three overloads each in sync + async (status(), status(date), status(from, to)) per ADR-006.
  • MarketStatus (record) and DailyStatus(LocalDate, boolean) — the domain model. MarketStatus is @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 the CompletableFuture is returned, so a fan-out beyond 50 in-flight requests:

  • pins Kotlin coroutine dispatchers (Dispatchers.Default has ~cores threads — a few stuck acquire()s and the dispatcher is dead);
  • pins event loops (Vert.x / Netty / Reactor handlers — Vert.x's BlockedThreadChecker will eventually crash the JVM);
  • contaminates ForkJoinPool.commonPool() when fan-out happens via parallelStream() — workers stuck on acquire() aren't available to unrelated parallel work elsewhere in the JVM.

This PR replaces the JDK semaphore with a purpose-built AsyncSemaphore in com.marketdata.sdk.internal.http:

  • acquire() returns a CompletableFuture<Void> instead of parking the caller's thread.
  • Fast path: a permit is available → returns an already-completed future, the thenCompose chain runs synchronously in the calling thread.
  • Slow path: pool exhausted → returns a pending future and enqueues it in a FIFO 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 cancelled acquire cannot leak a permit.
  • complete(...) always runs outside the internal lock to avoid re-entrancy hazards from caller-attached callbacks.
  • ~80 LOC, package-private, internal class — no public API surface change.

HttpTransport.executeAsync is now acquire().thenCompose(unused -> dispatch(...)). The 50-permit guarantee from §12 is unchanged; what changed is "what does unavailable feel like to the caller" (park() → pending CompletableFuture).

Defensive fix — synchronous-throw permit leak (companion to the AsyncSemaphore work)

If httpClient.sendAsync(...) throws synchronously (rare: malformed request, internal NPE, OOM allocating buffers), the whenComplete(release) chain never forms and the permit was previously leaked forever. HttpTransport.dispatch now wraps the sendAsync call in a try/catch that releases the permit before propagating; Error types are rethrown unwrapped (so a real OutOfMemoryError isn't masked as NetworkError), RuntimeExceptions are surfaced as a NetworkError failed future. Without this, a long-lived process eventually deadlocks once 50 such failures accumulate.

Cross-cutting transport layer — com.marketdata.sdk.internal.http

Reusable for every future endpoint; nothing here is markets-specific.

  • HttpTransport — single point of contact between resources and the network. Owns the HttpClient, the AsyncSemaphore, the ObjectMapper, and the latest RateLimits snapshot. Async-first (executeAsync) with sync wrapper (executeSync) that unwraps CompletionException per ADR-006. A package-private secondary constructor takes an injectable HttpClient for 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 → MarketDataException subtype mapping per SDK requirements §9.1.
  • RateLimitHeaders — parses x-api-ratelimit-* headers.
  • 200 / 203 / 404 are decoded as bodies (404 with {"s":"no_data"} is the API's empty-result sentinel); other 4xx/5xx throw typed exceptions.

Wire-format adapter — com.marketdata.sdk.internal.wire.markets

  • MarketStatusDeserializer — Jackson custom deserializer for the parallel-arrays format. Normalizes unix timestamps to LocalDate in US/Eastern (SDK requirements §11.4); maps s:"no_data" to an empty list.

MarketDataClient

Refactored to delegate all HTTP-shaped concerns to the new HttpTransport. Adds client.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.
  • NEW pr-integration-on-demand.yml — triggered by commenting integrationtest (JDK 17) or integrationtestfull (matrix {17, 21, 25}) on an open PR. Gated to write+ permissions, reacts 👀, posts a result summary comment. Aggregator job Integration tests pass produces 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 if MARKETDATA_TOKEN secret is missing.
  • Gradle 9.0.0 upgrade for JDK 25 support — Gradle 8.x maxes at JDK 24 as toolchain target; Gradle 9.0.0 is the first release that supports JDK 25 for both daemon and toolchain. The daemon still runs on JDK 17 via JAVA_HOME for stability (CI workflows order setup-java's java-version so the matrix JDK is first and 17 is last); toolchain forks compile/test JDKs as needed via -PtestJdk=N.
  • build.gradle.kts-PtestJdk=N is wired to all Test tasks (test + integrationTest) via tasks.withType<Test>().configureEach, so the matrix flag works uniformly for unit and integration runs.

@codecov

codecov Bot commented May 5, 2026

Copy link
Copy Markdown

The author of this PR, MarketDataDev03, is not an activated member of this organization on Codecov.
Please activate this user on Codecov to display this PR comment.
Coverage data is still being uploaded to Codecov.io for purposes of overall coverage calculations.
Please don't hesitate to email us at support@codecov.io with any questions.

@MarketDataDev03

Copy link
Copy Markdown
Collaborator Author

/test-all

@github-actions

github-actions Bot commented May 5, 2026

Copy link
Copy Markdown
Contributor

❌ On-demand JDK matrix {21, 25} failed. View run.

@MarketDataDev03

Copy link
Copy Markdown
Collaborator Author

/test-all

@github-actions

github-actions Bot commented May 5, 2026

Copy link
Copy Markdown
Contributor

❌ On-demand JDK matrix {21, 25} failed. View run.

@MarketDataDev03

Copy link
Copy Markdown
Collaborator Author

/test-all

@github-actions

github-actions Bot commented May 6, 2026

Copy link
Copy Markdown
Contributor

✅ On-demand JDK matrix {21, 25} passed. View run.

@MarketDataDev03

Copy link
Copy Markdown
Collaborator Author

integrationtestfull

@MarketDataDev03 MarketDataDev03 marked this pull request as ready for review May 6, 2026 13:37
@MarketDataDev03 MarketDataDev03 self-assigned this May 6, 2026
@MarketDataDev03 MarketDataDev03 force-pushed the 01_endpoint_v1_markets_status branch from bba1455 to d617db2 Compare May 8, 2026 14:43
@MarketDataDev03 MarketDataDev03 merged commit 0050824 into main May 12, 2026
5 checks passed
@MarketDataDev03 MarketDataDev03 deleted the 01_endpoint_v1_markets_status branch May 12, 2026 19:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants