Skip to content

sql: measure the connect-retry budget on the monotonic clock - #37960

Open
robobun wants to merge 5 commits into
mainfrom
farm/f9f02239/sql-retry-budget-monotonic-clock
Open

robobun wants to merge 5 commits into
mainfrom
farm/f9f02239/sql-retry-budget-monotonic-clock

Conversation

@robobun

@robobun robobun commented Aug 13, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • A pool whose server accepts the TCP connection and closes it before the handshake (what a Docker port proxy does while the database inside is still starting) redials with backoff until connectionTimeout elapses. That budget is measured with Date.now(): connectStartedAt = Date.now() in #beginConnecting and Date.now() - this.connectStartedAt < connectionTimeout in #canKeepRetrying (src/js/internal/sql/shared.ts).
  • bun:test's setSystemTime() pins Date.now() at one value (and useFakeTimers() freezes it), so the budget never elapses: the pool redials once a second for as long as the test runs and connect() / the waiting queries never reject. On 1.4.0, setSystemTime(new Date("2020-01-01")) plus connectionTimeout: 0.25 against an accept-and-destroy net.Server is still pending after 4s with 8 to 12 connections dialed.
  • The other direction as well: setSystemTime() moved ahead during a connect cycle makes the next failure look like the whole budget is gone, so the pool gives up after a single attempt with the default 30s budget.
  • Capturing Date.now at module load does not help; the override lives in the engine (JSGlobalObject::overridenDateNow, consulted by Date.now() in every tier). performance.now(), process.hrtime() and Bun.nanoseconds() are frozen by useFakeTimers() too (Bun__readOriginTimer), so builtin JS had no unmockable clock to measure an internal deadline with.

Fix

  • src/runtime/timer/Timer.rs: new internal_bindings.monotonicNowMs() returning Timespec::now(ForceRealTime).ms(), next to the existing timerClockMs binding (which reads the mockable clock on purpose, for ported Node timer tests). node:http: measure the keep-alive idle period on the monotonic clock #37974 (the node:http keep-alive side of the same problem) adds the identical hunk, so either PR can land second without a rebase.
  • sql/shared.ts binds it with $newRustFunction where it is used (the way postgres.ts / mysql.ts bind their natives) and records / compares connectStartedAt with it. Sentinel (0 = not connecting), backoff and budget are unchanged. An earlier revision routed this through a require("internal/timers"), which would have pulled internal/timers, validators, internal/shared and primordials into every Bun.SQL load for one function; the direct binding keeps the SQL module graph as it is.
  • Why this is the right clock: connectionTimeout is a real-time budget (how long to wait for the server to come up), the same thing the native connect timeout measures, and the backoff timers that drive the retries run on the real clock under setSystemTime(). ForceRealTime is the clock the event loop drains the real timer heap with, so the budget and the timers now agree. This is also what Node users get: Node's internals read the DateNow primordial, which jest/sinon fake timers (they replace the global Date) cannot reach; bun:test mocks inside the engine, so the internal clock has to be explicit.
  • Behavior change to be aware of: mocked time (setSystemTime(), advanceTimersByTime()) no longer expires or extends the connect-retry budget; it elapses in real time, the same call bun:test: keep runtime-internal timeouts out of the fake timer heap #37946 made for the native connection timeouts. Nothing in the tree relied on the old coupling (no test combines Bun.SQL with mocked time, and the docs describe the retry window only in terms of connectionTimeout).
  • Left alone on purpose: the retry setTimeout in shared.ts is still a regular JS timer, so under useFakeTimers() it lands in the fake heap (a real-time timer for builtin JS is a separate change), and _http_server.ts's keep-alive bookkeeping is node:http: measure the keep-alive idle period on the monotonic clock #37974.
  • Tests, test/js/sql/sql-connect-error-reporting.test.ts (4 new, postgres and mysql each since both adapters share BasePooledConnection): with the clock pinned, connect() still rejects with ERR_*_CONNECTION_FAILED once the budget elapses; with the clock jumped a day ahead while the first failure is being reported, the pool still retries and connects on the second attempt. On the unfixed build the pinned tests hang until the test timeout and the jump tests reject after one attempt.
  • Tests, test/js/bun/test/fake-timers/fake-timers.test.ts (1 new, through a timerInternals.monotonicNowMs export in bun:internal-for-testing): under useFakeTimers() + advanceTimersByTime(1h) the mockable timerClockMs() and performance.now() read exactly 1h while monotonicNowMs() neither restarted from 0 nor moved by the hour. setSystemTime() never touches the Rust mock clock, so this is the one test that pins ForceRealTime: with the binding switched to AllowMockedTime it fails (restarted: true, advancedByTheHour: true), and on the released binary it fails because the export does not exist.
  • Both files pass on the debug build (67 tests; the SQL file also 8 repeated runs plus 4 concurrent runs). Also run: sql-close-pending-connection, postgres-failed-connection-resurrection, sql-onconnect-onclose-throw, sql-mysql-duplicate-auth-switch, test/js/web/timers/setTimeout.test.js (the timerClockMs test passes; its three leak fixtures fail locally on any debug+ASAN build because the fixture detects ASAN by binary name, which test: surface ASAN status to leak fixtures via bunEnv #35081 addresses), test-timers-ordering.js, and a real connect + select 1 against a local postgres.

Background

  • Connect-retry budget (sql: retry connect failures while queries are waiting #32028): on a pre-handshake close, BasePooledConnection.handleClose keeps the slot pending and redials after 40ms, 80ms, ... (capped at 1s) while queries are waiting, until connectionTimeout (default 30s) has passed since connectStartedAt; both handleClose and the backoff timer callback re-check the budget through #canKeepRetrying.
  • setSystemTime(d) sets overridenDateNow on the global object; from then on Date.now() returns exactly d until it is reset, it does not advance, and nothing on the Rust side changes. useFakeTimers() additionally installs the Rust mock clock (read by Timespec::now(AllowMockedTime), performance.now() and timerClockMs(), all restarting from 0) and routes setTimeout into a heap that only jest.advanceTimersByTime() and friends drain.
  • Timespec::now(mode) is bun's monotonic clock (CLOCK_MONOTONIC, QPC on Windows). AllowMockedTime returns the mock clock while one is installed; ForceRealTime never does.
  • $newRustFunction(file, symbol, argc) is how builtin JS modules call into Rust: codegen emits one thunk per distinct symbol that calls the named function directly, so several modules may bind the same function, and a missing or misnamed Rust function is a compile error. bun:internal-for-testing is the test-only module for exposing such bindings to tests.

BasePooledConnection bounded connect retries with Date.now(), which
bun:test's setSystemTime() pins (and useFakeTimers() freezes), so a pool
pointed at a server that is not accepting connections redialed forever
instead of failing after connectionTimeout, and a clock moved ahead ended
the budget after one attempt.

Add a monotonicNowMs() binding (Timespec::now(ForceRealTime)) exported
from internal/timers and use it for connectStartedAt and the budget check.
@coderabbitai

coderabbitai Bot commented Aug 13, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@robobun, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 3 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 8a2cc51d-5ecc-4265-a402-766d82b2246a

📥 Commits

Reviewing files that changed from the base of the PR and between 5b83fd4 and b3995f2.

📒 Files selected for processing (5)
  • src/js/internal-for-testing.ts
  • src/js/internal/sql/shared.ts
  • src/runtime/timer/Timer.rs
  • test/js/bun/test/fake-timers/fake-timers.test.ts
  • test/js/sql/sql-connect-error-reporting.test.ts

Comment @coderabbitai help to get the list of available commands.

@robobun

robobun commented Aug 13, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 5:17 AM PT - Aug 13th, 2026

✅ @robobun, your commit b3995f231cddcfe0761a236c0d2044820445d2d9 passed in Build #94285! 🎉


🧪   To try this PR locally:

bunx bun-pr 37960

That installs a local version of the PR into your bun-37960 executable, so you can run:

bun-37960 --bun

@robobun

robobun commented Aug 13, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status: fix pushed (b3995f2), waiting on CI.

Reproduced on the released 1.4.0 binary with test/js/sql/sql-connect-error-reporting.test.ts from this branch (USE_SYSTEM_BUN=1 bun test ...): the two "pinned by setSystemTime()" tests hang until the test timeout (the pool keeps redialing the accept-and-destroy server), the two "jumping ahead" tests reject with ERR_POSTGRES_CONNECTION_FAILED / ERR_MYSQL_CONNECTION_FAILED after a single attempt. The new fake-timers.test.ts case fails there because the test export does not exist, and fails on a build with the binding switched to the mockable clock. All of these pass with the debug build of this branch (67 tests across the two files).

CI: the earlier heads were green on every lane that ran (179 of 181 on fc80581, 177 on 2d69e81 with the macOS aarch64 jobs queued behind a backlogged lane). b3995f2 is the revision to judge: it rebinds the clock directly in shared.ts, adds the binding-level test, and merges current main.

Review: automated reviews found nothing to change (the latest one covers b3995f2); the three comment-length threads are answered and resolved. #37974 shares the Timer.rs hunk byte for byte, so either PR can land second without a rebase.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — focused clock swap with a matching binding and tests for both directions.

Checked: the new monotonic_now_ms binding mirrors the existing timer_clock_ms shape and $newRustFunction call site; the two Date.now() sites in BasePooledConnection are the only ones on the retry-budget path and the 0 sentinel still holds. The added mysqlHandshakeV10/mysqlOkPacket/mysqlReadPackets imports exist in wire-frames.ts, and every new test resets setSystemTime() in finally. The top-level require("internal/timers") in sql/shared.ts is a leaf module (no cycle).

Extended reasoning...

Overview

Swaps the SQL pool's connect-retry budget from Date.now() to a new unmockable monotonic clock so bun:test's setSystemTime() cannot freeze or skip it. Touches four files: a 12-line Rust host function (monotonic_now_ms) alongside the existing timer_clock_ms in src/runtime/timer/Timer.rs; an export in src/js/internal/timers.ts; two call sites plus a comment in src/js/internal/sql/shared.ts; and four new tests plus a MySQL greet helper in test/js/sql/sql-connect-error-reporting.test.ts.

Security risks

None. The new binding is internal-only ($newRustFunction codegen, not user-visible), takes no arguments, and just reads Timespec::now(ForceRealTime).ms(). No user input reaches it. The test file's new MySQL helper reuses existing wire-frames.ts builders against a local net.Server.

Level of scrutiny

Low-to-moderate. The Rust side is a near-copy of the sibling timer_clock_ms binding with only the mock-mode flag changed — no allocation, no JS re-entry, no unsafe. The JS change is a two-call-site clock substitution that preserves the connectStartedAt === 0 sentinel and all backoff/budget arithmetic. internal/timers only requires internal/validators, so the new top-level require in sql/shared.ts introduces no cycle.

Other factors

Tests cover both failure modes (pinned clock → budget still elapses; forward jump → still retries) for both adapters that share BasePooledConnection, and each restores setSystemTime() in finally. The PR description documents that the pinned tests hang and the jump tests fail after one attempt on the unfixed build, and that the full 19-test file plus adjacent SQL/fake-timer suites pass on the debug build. No prior reviewer comments to address.

@robobun

robobun commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator Author

#37974 (the node:http keep-alive side of this) adds the same monotonicNowMs binding and export. Its src/runtime/timer/Timer.rs and src/js/internal/timers.ts hunks are byte-identical to the ones here, so whichever of the two lands second merges without a rebase.

Comment thread src/js/internal/sql/shared.ts Outdated
Comment thread src/js/internal/timers.ts Outdated
Comment thread src/runtime/timer/Timer.rs

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reviewed this and didn't find any bugs — the Date.now() → monotonicNowMs() swap is correct and well-tested. Deferring to a human because it adds a new native binding and changes user-observable behavior (mocked time no longer affects the SQL connect-retry budget), and the comment-cop feedback on the doc comments is a style call for a maintainer.

What was reviewed:

  • monotonic_now_ms mirrors the adjacent timer_clock_ms binding with ForceRealTime; the $newRustFunction name/argc match the existing pattern.
  • The connectStartedAt === 0 sentinel remains safe — CLOCK_MONOTONIC/QPC won't return 0 in practice.
  • New test imports (mysqlHandshakeV10, mysqlOkPacket, mysqlReadPackets) exist in wire-frames.ts; setSystemTime() is reset in every finally.
Extended reasoning...

Overview

Replaces Date.now() with a new unmockable monotonic clock (monotonicNowMs) for measuring the SQL connect-retry budget in BasePooledConnection. Four files: a ~10-line Rust host function in src/runtime/timer/Timer.rs (sibling to the existing timer_clock_ms, differing only in TimespecMockMode::ForceRealTime vs AllowMockedTime), a one-line $newRustFunction export in src/js/internal/timers.ts, two call-site swaps in src/js/internal/sql/shared.ts, and four new regression tests in test/js/sql/sql-connect-error-reporting.test.ts.

Security risks

None. The new binding reads a clock and returns a number; no user input is parsed, no allocations sized from external data, no auth/TLS/permission paths touched.

Level of scrutiny

Moderate. The mechanical change is small and follows an established pattern exactly (verified against the neighboring timerClockMs binding in internal-for-testing.ts). However, it (1) adds a new native→JS binding surface that #37974 already plans to reuse, (2) is a deliberate behavior change — setSystemTime() / advanceTimersByTime() no longer influence the SQL retry budget — which the PR description argues for convincingly but is still a maintainer-level decision, and (3) has outstanding comment-cop bot feedback on three doc comments. Those comments read to me as legitimate "why this clock and not Date.now()" documentation rather than workaround justification, but that's a repo style judgment.

Other factors

  • CI showed a ❌ on fc80581 followed by a ci: retrigger commit; current status is not confirmed green from the timeline.
  • Verified the 0 sentinel for connectStartedAt is still safe with the monotonic clock (starts at boot on all platforms, not at epoch).
  • Verified the newly-imported mysqlHandshakeV10 / mysqlOkPacket / mysqlReadPackets helpers exist in test/js/sql/wire-frames.ts.
  • Tests correctly restore process-global state (setSystemTime() with no args) in finally blocks and cover both failure directions (pinned clock → never elapses; jumped clock → elapses too early) for both adapters sharing BasePooledConnection.
  • require("internal/timers") from sql/shared.ts introduces no circular dependency (it only pulls in internal/validators).

@robobun

robobun commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator Author

Confirmed from this side: as of 2d69e81 the src/runtime/timer/Timer.rs and src/js/internal/timers.ts blobs here are still identical to the ones in #37974 (91e0edc and 886c629), and the review feedback on those two comments was answered the same way on both PRs so they stay that way. The only file changed since is src/js/internal/sql/shared.ts, which #37974 does not touch.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reviewed this PR and didn't find any bugs. The clock swap itself is small and correct, but since it introduces a new internal binding (monotonicNowMs, also picked up by #37974) and fixes a semantics decision — the connect-retry budget now ignores mocked time — a human sign-off would still be worthwhile. Two of the three comment-cop flags (on the timers.ts and Timer.rs doc comments) are also still open; they read as API docs rather than workaround justifications, so likely false positives, but worth a look.

What was reviewed:

  • monotonic_now_ms mirrors the adjacent timer_clock_ms binding with ForceRealTime instead of AllowMockedTime; the $newRustFunction symbol name matches the existing camelCase→snake_case convention.
  • The 0 sentinel on connectStartedAt is unchanged and still safe with the monotonic clock; both call sites (#beginConnecting, #canKeepRetrying) now read the same clock.
  • New tests restore setSystemTime() in finally on every path; the imported mysqlHandshakeV10/mysqlOkPacket/mysqlReadPackets helpers exist in wire-frames.ts.
Extended reasoning...

Overview

Swaps Date.now() for a new monotonicNowMs() binding in BasePooledConnection's connect-retry budget (src/js/internal/sql/shared.ts), so bun:test's setSystemTime() / useFakeTimers() can no longer freeze or prematurely exhaust it. Adds the Rust host function in src/runtime/timer/Timer.rs (a one-line variant of the existing timer_clock_ms using TimespecMockMode::ForceRealTime), exports it from src/js/internal/timers.ts, and adds four tests (postgres × mysql × pinned/jumped clock) to test/js/sql/sql-connect-error-reporting.test.ts.

Security risks

None. No user-controlled input reaches the new binding; it takes zero arguments and returns a number. No auth/crypto/permissions surface touched.

Level of scrutiny

Medium. The implementation is mechanical (two call sites swap Date.now() → monotonicNowMs(); the Rust binding is a near-copy of its neighbor), but this is a deliberate semantics decision: the connect-retry budget is now decoupled from mocked time. The PR argues convincingly that this aligns with the native connect timeout and the real-clock backoff timers, and #37974 will reuse the same binding for node:http — so the shape of this internal API is worth a human confirming once.

Other factors

  • Two comment-cop flags remain on the doc comments in timers.ts:7-12 and Timer.rs:600-605 (the third, on shared.ts, was addressed in 2d69e81). These look like false positives — they document why the API exists, not why a workaround is acceptable — but they're technically unresolved.
  • CI on fc805815 reported failures, followed by a retrigger commit; status on the latest head isn't visible in the timeline.
  • Tests cover both failure directions (budget never elapses under a pinned clock; budget ends early under a forward jump) for both adapters, and restore process-global setSystemTime() state in finally. The 0 sentinel for "not connecting" is preserved and remains safe with a monotonic clock.

…ding

Requiring internal/timers from internal/sql/shared pulled validators,
internal/shared and primordials into every Bun.SQL load for one native
function; bind it where it is used instead, like the adapters do.

Expose the binding through bun:internal-for-testing and assert that
useFakeTimers() neither restarts nor advances it, which is the only thing
separating it from the mockable timerClockMs next to it.
@robobun

robobun commented Aug 13, 2026 •

Copy link
Copy Markdown
Collaborator Author

Update after a self-review pass (b3995f2): sql/shared.ts now binds monotonicNowMs directly with $newRustFunction instead of requiring internal/timers (that require would have added four modules to every Bun.SQL load), so this PR no longer touches src/js/internal/timers.ts; the Timer.rs hunk is unchanged and still identical to the one in #37974. The binding is also exported from bun:internal-for-testing now, with a test in fake-timers.test.ts that fails if it is switched to the mockable clock, since the SQL tests alone (setSystemTime() only) could not tell the two apart. The branch also has current main merged in (#37946 landed).

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — targeted swap of Date.now() for an unmockable monotonic clock in the SQL connect-retry budget.

What was reviewed:

  • The new monotonic_now_ms binding mirrors the sibling timer_clock_ms exactly, only swapping AllowMockedTime → ForceRealTime; the $newRustFunction symbol path matches the existing camelCase→snake_case convention.
  • Checked the connectStartedAt === 0 sentinel still works with a monotonic clock (CLOCK_MONOTONIC/QPC ms since boot; effectively never 0 in practice, and the previous Date.now() had the same theoretical edge).
  • The four new SQL tests reset setSystemTime() in finally, cover both pinned and jumped-ahead directions for both adapters, and reuse the existing wire-frames.ts helpers; the fake-timers test asserts the mockable clocks do move while monotonicNowMs() does not.
Extended reasoning...

Overview

Replaces Date.now() with a new monotonicNowMs() Rust binding (Timespec::now(ForceRealTime).ms()) at the two sites in src/js/internal/sql/shared.ts that measure the connect-retry budget (#beginConnecting records the start, #canKeepRetrying compares against it). Adds the binding in src/runtime/timer/Timer.rs alongside the existing timer_clock_ms, exposes it for tests via internal-for-testing.ts, and adds one binding-level test in fake-timers.test.ts plus four end-to-end tests (postgres/mysql × pinned/jumped) in sql-connect-error-reporting.test.ts.

Security risks

None. The change reads a monotonic clock instead of wall-clock time for an internal deadline; no user input flows into the new binding, and the binding takes no arguments.

Level of scrutiny

Low-to-medium. The Rust hunk is a near-verbatim copy of the neighbouring timer_clock_ms with one enum variant flipped; the JS change is a two-line clock substitution that leaves the sentinel, backoff, and budget arithmetic unchanged. The behavioural change (mocked time no longer influences the retry budget) is intentional, well-argued in the description, and consistent with #37946's treatment of the native connection timeouts.

Other factors

  • The $newRustFunction symbol name follows the same camelCase→snake_case mapping as the existing timerClockMs → timer_clock_ms, so codegen will resolve it.
  • The 0 sentinel for connectStartedAt is safe with a monotonic clock: the value only reads 0 within the first millisecond after boot, no worse than the previous Date.now() edge at the epoch.
  • Tests are hermetic (local TCP servers, setSystemTime() reset in finally), cover both failure directions for both drivers, and the author verified they fail on the released binary and pass on the debug build.
  • All three comment-cop threads are resolved: the shared.ts field comment was shortened in 2d69e81, and the remaining doc comments on the new binding were justified and kept. No prior reviews from me on this PR.

@robobun

robobun commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator Author

Follow-up to the note above: review on #37974 pointed out two things in the shared doc comments, so they changed there and the hunks are no longer byte-identical (the code lines still are, so any conflict is comment-only). In case you want to line them up again, this is the text #37974 now carries:

src/js/internal/timers.ts, above the const:

/**
 * The real monotonic clock in whole milliseconds, for deadlines the runtime's
 * own JS keeps (a mark recorded now and compared against when a timer fires).
 * No JS-visible clock will do: bun:test's `setSystemTime()` overrides
 * `Date.now()` inside the engine, and `useFakeTimers()` also overrides
 * `performance.now()` and `process.hrtime()`, so a deadline measured with any
 * of them freezes or jumps along with the mock.
 */

src/runtime/timer/Timer.rs, on monotonic_now_ms:

    /// `require("internal/timers").monotonicNowMs()`, documented there. Always
    /// the real clock, i.e. the one the real timer heap is drained against,
    /// whatever bun:test has mocked.

The two corrections behind it: setSystemTime() on its own pins only Date.now() (performance.now() keeps running; useFakeTimers() is what freezes that one, along with process.hrtime() and Bun.nanoseconds()), and naming a specific caller in the Rust doc goes stale depending on which PR lands first.

@robobun

robobun commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator Author

Saw that this PR now binds the function from sql/shared.ts directly and no longer touches internal/timers.ts, so disregard the internal/timers.ts part of the comment above. The only remaining overlap with #37974 is the Timer.rs doc comment on the (otherwise identical) binding; whichever lands second keeps its own wording, no coordination needed.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant