diff --git a/CHANGELOG.md b/CHANGELOG.md index 815bc6c..39289d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,20 @@ All notable changes to DatabentoAPI.jl are documented here. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] +## [0.3.2] - 2026-09-10 + +### Changed + +- Require DatabentoBinaryEncoding 0.1.6 so MBO decoding cannot resolve to a + release with the pre-0.1.6 wire-layout defect. +- Clarify that live symbol-mapping callbacks execute synchronously on the + reader task and must return quickly. + +### Fixed + +- Synchronize all accesses to the live symbol-callback registry. +- Document the HTTP 2.x timeout policy accurately and pin its keyword contract + in tests (`read_idle_timeout`, `retries=0`). ## [0.3.1] - 2026-06-25 diff --git a/Project.toml b/Project.toml index cd3cb35..da3a66d 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "DatabentoAPI" uuid = "feb0aed8-3291-460b-9688-521dcb17a4bf" -version = "0.3.1" +version = "0.3.2" authors = ["Tyler Beason "] [deps] @@ -22,11 +22,11 @@ URIs = "5c2747f8-b7ea-4ff2-ba2e-563bfd36b1d4" [compat] CodecZstd = "0.8" DataFrames = "1.7" -DatabentoBinaryEncoding = "0.1.4" +DatabentoBinaryEncoding = "0.1.6" EnumX = "1.0" HTTP = "2.4" JSON3 = "1.14" -SHA = "0.7.0" +SHA = "0.7.0, 1" TOML = "1.0.3" TranscodingStreams = "0.11" URIs = "1.5" diff --git a/src/http.jl b/src/http.jl index c17cad5..ab1e2fb 100644 --- a/src/http.jl +++ b/src/http.jl @@ -164,9 +164,10 @@ end Perform an HTTP request against the Databento gateway. Maps 4xx → `BentoClientError` and 5xx → `BentoServerError`. Returns the underlying `HTTP.Messages.Response`. -Transient failures — HTTP `429` (rate limit) and `5xx`, plus connection/timeout +Transient failures — HTTP `429` (rate limit), `5xx`, and connection-phase errors — are retried up to `c.max_retries` times with full-jitter exponential backoff. A `Retry-After` response header, when present, overrides the backoff. +Read-idle timeouts are mapped to [`BentoTimeoutError`](@ref) without retrying. Once the retry budget is exhausted the final response is mapped to its error as usual, so a persistently rate-limited request still surfaces `BentoClientError(429)`. diff --git a/src/live/symbols.jl b/src/live/symbols.jl index b80e671..d9cc250 100644 --- a/src/live/symbols.jl +++ b/src/live/symbols.jl @@ -26,10 +26,10 @@ return nothing end -# Snapshot callbacks under the lock, then fire outside it so a slow callback -# can't block the reader (mirrors _fire_reconnect_callbacks). +# Snapshot callbacks under the lock, then fire outside it so callbacks may call +# symbol APIs without deadlocking. Callbacks still execute synchronously on the +# reader task and therefore must return quickly. function _fire_symbol_callbacks(c::Live, iid::UInt32, rec::DBN.SymbolMappingMsg) - isempty(c.symbol_callbacks) && return nothing cbs = @lock c.symbols_lock copy(c.symbol_callbacks) for cb in cbs try @@ -104,7 +104,9 @@ time the gateway maps (or remaps) an instrument id — useful for reacting to continuous/parent symbol rolls or persisting the mapping. Callbacks fire from the reader task; they are snapshotted under a lock and run outside it, and an exception in a callback is logged and swallowed (it won't disrupt the reader or -other callbacks). The map itself is updated before callbacks fire, so +other callbacks). Callbacks execute synchronously on the live reader task, so +they should hand off expensive or blocking work to another task. The map itself +is updated before callbacks fire, so [`symbol_for`](@ref) already reflects the new mapping inside the callback. """ function add_symbol_mapping_callback(c::Live, cb) diff --git a/test/test_http.jl b/test/test_http.jl index a4e0a59..caecea7 100644 --- a/test/test_http.jl +++ b/test/test_http.jl @@ -45,6 +45,10 @@ const _NOSLEEP = _ -> nothing @test_throws BentoClientError request(c, :GET, "/v0/foo") @test captured[].method == "GET" @test captured[].url == "https://example.test/v0/foo" + @test captured[].kwargs[:read_idle_timeout] == DEFAULT_TIMEOUT + @test captured[].kwargs[:retries] == 0 + @test !haskey(captured[].kwargs, :readtimeout) + @test !haskey(captured[].kwargs, :retry) # Authorization header present @test any(p -> first(p) == "Authorization", captured[].headers) end