Skip to content

progress: tell a cancel that was never sent from one that may have been - #158

Merged
Kiran01bm merged 3 commits into
mainfrom
kiran01bm/cancel-not-dispatched
Oct 11, 2026
Merged

Kiran01bm merged 3 commits into
mainfrom
kiran01bm/cancel-not-dispatched

Conversation

@Kiran01bm

@Kiran01bm Kiran01bm commented Oct 10, 2026 •

Copy link
Copy Markdown
Collaborator

Tracker.CancelBuild now returns ErrCancelNotDispatched, wrapped together with the caller's context error, when the caller gave up before the cancel signal was sent — and its wait for the reserved session ends when the caller's context does, not when the session's holder lets go.

Purpose

  • Context: CancelBuild may have to wait behind a progress poll or another cancel before it can send the signal.
  • Problem: if the caller gives up during that wait, it gets the same timeout error whether or not the signal was sent, so the consumer has to assume the worse case — and it gets that answer only once the holder releases the session.
  • Fix: ErrCancelNotDispatched is returned only when nothing was sent, so the consumer can tell the two cases apart; the wait itself is bounded by the caller's context, so the answer arrives when the caller gives up.
  • Consumer follow-up: consumers recognise the sentinel — before any context error — and treat a later outside cancellation as a failure to retry, not as the operator's cancel.

Why

An orchestrator that calls CancelBuild has to decide whether the operator's cancel may have reached the build. If the signal may have gone out, it must read the build's later cancelled-externally return as the operator's cancel. If nothing was sent, it must not, because an outside cancellation is then a failure to retry.

Before this change it could not decide from the error. CancelBuild takes the reserved session only after any in-flight progress poll or overlapping cancel releases it. A caller whose deadline passes during that wait gets its bare context error back, and no signal was sent. A context error can also come back after the signal was sent, for example when the signal's own deadline expires. Both look like context.DeadlineExceeded, so the consumer has to assume the worse case and record a cancel that never happened.

Example: an operator's cancel is behind a Progress poll, and the caller's deadline passes before the poll returns. The cancel sends nothing, but the orchestrator records it as possibly signalled. If someone else then cancels the backend, that cancellation is reported as the operator's.

What

  • New exported sentinel progress.ErrCancelNotDispatched. CancelBuild returns it, wrapped with ctx.Err(), from two places and no others: when the caller's context ends while it waits for the reserved session, and from the check that runs after the session is held and before the signal query. errors.Is matches both the sentinel and context.Canceled / context.DeadlineExceeded.
  • The poll gate is a one-slot channel instead of a mutex, so that wait can be interrupted. Progress, SetConcurrentBuild, StopConcurrentBuild, SetWorkSource and StopWorkSource take the gate unconditionally, exactly as they took the mutex; only CancelBuild waits under its caller's context. A caller that gives up while the build ends during the wait still gets ErrNoActiveBuild, not a stale "not sent".
  • Errors after the signal is sent are unchanged and never carry the sentinel.
  • CancelBuild's doc comment and docs/progress-report.md (new ### Cancelling a concurrent index build anchor) carry the four-class outcome table — sent / not sent / nothing delivered (ErrNoActiveBuild, ErrBuildNotRunning, ErrBuildUnobservable) / unknown — and the check order: test for the sentinel before the context error, with a three-line consumer snippet. docs/schemabot-integration.md's Cancel row points at the table. SAFETY.md's pkg/progress entry names the gate.
  • Tests: a caller that gives up behind an in-flight poll (cancelled and past deadline) and behind an overlapping cancel gets the sentinel, and no signal is sent. The wait ends with the caller — the poll is deliberately never released until the outcome is in hand. A build that ends during the wait reports ErrNoActiveBuild. The caller's context is read only once the session is held, pinned by a probe context that records the read and hooks the wait (no sleeps). A context error after the signal was sent does not carry the sentinel.

Before / after, caller's deadline passes while a progress poll holds the session:

before: cancel concurrent index build backend 4242: context deadline exceeded
        errors.Is(err, context.DeadlineExceeded) == true   // same as a timed-out signal
        returned only once the poll released the session

after:  cancel concurrent index build backend 4242: the cancel signal was not sent: context deadline exceeded
        errors.Is(err, progress.ErrCancelNotDispatched) == true
        errors.Is(err, context.DeadlineExceeded)        == true
        returned when the caller's deadline passes

Outcome table

CancelBuild result Signal reached the server? Safe to retry the cancel?
nil sent, to a backend the same statement read as active not needed
ErrCancelNotDispatched (+ context error) no yes
ErrNoActiveBuild, ErrBuildNotRunning, ErrBuildUnobservable no signal delivered per each sentinel's own doc
any other error (session failure, context error without the sentinel) unknown treat as possibly sent

Verification

  • go test ./pkg/progress/ -race -count=3 — green.
  • Mutation check: a context-blind wait (plain channel send) fails TestCancelBuildReturnsWhenTheCallerGivesUpWhileWaiting, TestCancelBuildReportsNoActiveBuildWhenTheBuildEndsWhileTheCallerWaits and TestCancelBuildReadsTheCallersContextOnlyOnceItHoldsTheSession; a ctx.Err() check hoisted above the wait fails the last two.
  • SKIP_INTEGRATION=1 go test ./... — green.
  • PG_VERSION=16 go test -race ./pkg/executor/ on the concurrent-index-build tests that call CancelBuild against a real server — green.
  • make lint — 0 issues.

Veto items

  • The exported name stays ErrCancelNotDispatched while the message and docs say "not sent". Renaming to ErrCancelNotSent is cheap pre-1.0 and I will do it on request; "dispatched" was kept because the identifier describes the request leaving this process, and the message describes what the server saw.
  • A caller that gives up during the wait while the build ends in the same window gets ErrNoActiveBuild, read under the state lock without the gate. The alternative — always the sentinel once the wait is abandoned — would tell the caller to retry against a build that is gone.

🤖 Generated with Amp (Claude Opus 4.6)

CancelBuild returned the caller's bare context error when the caller gave
up before the signal went out, including while it waited for a progress
poll or an overlapping cancel to release the reserved session. A context
error that comes back after the signal was sent looks the same, so a
consumer had to treat "never sent" as "may have reached the build".

Return ErrCancelNotDispatched, wrapped together with the context error,
only when nothing was sent; errors after dispatch are unchanged.
@Kiran01bm
Kiran01bm marked this pull request as ready for review October 10, 2026 23:50
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 10, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-10T23:52:52.888717Z 228b7de Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@aparajon

Copy link
Copy Markdown
Collaborator

🤖 Review 1/2 (correctness) of 228b7de: 0 blocking, 3 non-blocking.

Inspected the full diff against the merge base ca9a8f3 (pkg/progress/progress.go, progress_test.go, docs/progress-report.md), the rest of Tracker (the pollMu/mu split, StopConcurrentBuild, Progress), and every other CancelBuild caller (pkg/executor/cic_build_integration_test.go, pkg/progress/work_source_test.go). Ran go test ./pkg/progress/ -race -count=3 and the TestCancelBuild* set with -v at the head (green, no database needed: the tests use the fake reserved session), and mutation-tested the change (table below). CI is green on all 15 checks.

The change is correct. ErrCancelNotDispatched is attached at exactly one site, the context check after pollMu is held and before the signal query, wrapped with %w: %w so errors.Is matches the sentinel and the context error. Every path after the query is issued (L333-L358) is untouched, so "sentinel present" really does mean "no signal went out", and its absence never claims the opposite. That is the fail-closed direction: a consumer that treats "no sentinel" as "the cancel may have reached the build" can only over-record an operator's cancel, never lose one. Nothing in docs/invariants.md is weakened. Disposition per entry: OC-1 (fail closed on uncertainty) upheld, since the new signal narrows an uncertainty and never rounds it toward "not sent"; OC-3 (control requests are durable operator intent) upheld and made easier to honour, since an orchestrator can now retry a cancel that provably never left; LK-2, whose text names Tracker.CancelBuild as the stop path for a caller-owned concurrent build, upheld, with the signal still running detached under cancelSignalTimeout; CO-9 untouched (cancelBuildSQL is unchanged).

Blocking

None.

Non-blocking

1. The wait for the session ignores the caller's context, so the sentinel arrives only after the wait ends (progress.go#L321-L322). CancelBuild blocks on t.pollMu.Lock(), which cannot be interrupted. A caller whose deadline passes behind an in-flight poll or cancel does not get a timely return; it gets ErrCancelNotDispatched only once the holder lets go, which is bounded by the holder, not by the caller. This is pre-existing (same lock on the base), and the PR's doc comment is accurate about it ("waits ... before it reads the caller's ctx"), so it is not a regression. It matters because the PR's stated use is an orchestrator that wants a bounded answer to "did my cancel go out", and the operator-facing stop path is the one place an unbounded wait is least welcome. A context-aware acquisition (a one-slot channel semaphore in place of the mutex, or TryLock plus a wait on ctx.Done()) would let the sentinel mean "gave up while waiting" the instant it happens. If that is out of scope here, saying so in the doc comment is enough.

Test case: CancelBuild returns promptly once the caller's deadline passes while a poll holds the session
package progress_test

import (
	"context"
	"sync"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
)

func TestCancelBuildReturnsWhenTheCallerGivesUpWhileWaiting(t *testing.T) {
	blocking := newBlockingSession()
	close(blocking.releaseSignal)
	tracker := runningTrackerWithBuild(t, blocking.session())

	var workers sync.WaitGroup
	defer workers.Wait()
	workers.Go(func() {
		_, err := tracker.Progress(t.Context())
		assert.NoError(t, err)
	})
	<-blocking.pollEntered
	defer close(blocking.releasePoll)

	callerCtx, cancel := context.WithTimeout(t.Context(), 50*time.Millisecond)
	defer cancel()
	outcome := make(chan error, 1)
	workers.Go(func() { outcome <- tracker.CancelBuild(callerCtx) })

	select {
	case err := <-outcome:
		t.Logf("returned while the poll still held the session: %v", err)
	case <-time.After(time.Second):
		t.Fatal("CancelBuild is still waiting 950ms after the caller's 50ms deadline passed")
	}
}

--- FAIL: TestCancelBuildReturnsWhenTheCallerGivesUpWhileWaiting (1.00s) on 228b7de (and on the merge base, since the lock is unchanged). Uses the PR's own blockingSession helper; passes once the wait honours the context.

2. The headline scenario, the context ending during the wait, is not pinned. The three subtests in TestCancelBuildReportsNotDispatchedWhenTheCallerGivesUpWaitingForTheSession end the caller's context (L491, L517, L542) without any synchronisation that CancelBuild has reached the lock. The test comment says as much ("whether or not CancelBuild has started waiting yet"), and in practice the context is usually already ended when CancelBuild is entered. So the tests pass for a check hoisted above pollMu.Lock(), which is the behaviour the PR exists to move away from: it would read the context before the wait, find it live, then send the signal after the caller has given up. Mutant M6 below survives all of the PR's tests. A context that records when Err() is read pins it.

Test case: the caller's context is read only after the session is held
package progress_test

import (
	"context"
	"sync"
	"sync/atomic"
	"testing"
	"time"

	"github.com/block/pg-sprite/pkg/progress"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

// errProbeCtx reports a context that ends exactly when the poll releases the
// session, and records whether CancelBuild read it before that.
type errProbeCtx struct {
	context.Context
	released  *atomic.Bool
	readEarly atomic.Bool
}

func (c *errProbeCtx) Err() error {
	if !c.released.Load() {
		c.readEarly.Store(true)
		return nil
	}
	return context.Canceled
}

func TestCancelBuildReadsTheCallersContextOnlyOnceItHoldsTheSession(t *testing.T) {
	blocking := newBlockingSession()
	close(blocking.releaseSignal)
	tracker := runningTrackerWithBuild(t, blocking.session())

	var workers sync.WaitGroup
	defer workers.Wait()
	workers.Go(func() {
		_, err := tracker.Progress(t.Context())
		assert.NoError(t, err)
	})
	<-blocking.pollEntered

	var released atomic.Bool
	caller := &errProbeCtx{Context: t.Context(), released: &released}
	outcome := make(chan error, 1)
	workers.Go(func() { outcome <- tracker.CancelBuild(caller) })

	// Give CancelBuild every chance to read the context while the poll still
	// holds the session.
	time.Sleep(200 * time.Millisecond)
	released.Store(true)
	close(blocking.releasePoll)

	err := awaitCancelOutcome(t, outcome)
	require.ErrorIs(t, err, progress.ErrCancelNotDispatched)
	assert.False(t, caller.readEarly.Load(), "the caller's context must be read only once the session is held")
	assert.Equal(t, int32(0), blocking.signals.Load())
}

Passes on 228b7de. With the ctx.Err() check moved above t.pollMu.Lock(), the PR's tests all pass and this one fails: --- FAIL: TestCancelBuildReadsTheCallersContextOnlyOnceItHoldsTheSession (0.20s). It uses a bounded settle wait, which is one-sided (it can only miss, never false-fail); if the repo's no-sleep test rule applies to it, gate it on a hook instead.

3. The doc comment over-claims in the other direction (progress.go#L309-L313, mirrored in docs/progress-report.md#L217-L220). "Any other error, a context error included, may have come back after the signal was sent" is not true of ErrNoActiveBuild (returned before any query), ErrBuildNotRunning and ErrBuildUnobservable (the statement signals only a backend it read as active, so on those outcomes pg_cancel_backend did not run or returned false). Those three also mean "nothing was delivered". The PR body is careful ("returned only when nothing was sent"); the comment should be too, so a consumer does not read the three older sentinels as ambiguous. See comment 2/2 for the consumer-facing table this feeds.

Verified

  • Sentinel appears at one site only; git grep ErrCancelNotDispatched shows no other producer. Errors after the signal query (session failure, ErrBuildNotRunning, ErrBuildUnobservable) never carry it, and TestCancelBuildWrapsASessionFailure / TestCancelBuildDoesNotReportNotDispatchedOnceTheSignalIsSent assert that.
  • ErrNoActiveBuild still takes precedence over the sentinel (the pid == 0 || session == nil check at L326-L328 precedes it), so a build that finished during the wait reports that, not a stale "not sent".
  • The error text reads ...backend 4242: the cancel signal was not sent: context deadline exceeded, matching the PR body.
  • Existing integration callers (pkg/executor/cic_build_integration_test.go, container-backed, not run here) use NoError / ErrorIs(ErrNoActiveBuild) / ErrorIs(ErrBuildNotRunning) and are unaffected by a wrap that only fires on an ended context.
Mutant (against 228b7de) Caught by
Drop the sentinel from the %w: %w wrap ...WhenTheCallerGivesUpWaitingForTheSession, ...RunsTheSignalDetachedFromTheCaller
Sentinel only, drop the context error same two (ErrorIs context.Canceled)
Sentinel on every post-signal scan error ...DoesNotReportNotDispatchedOnceTheSignalIsSent, ...WrapsASessionFailure
Sentinel plus context error on every post-signal scan error same two
Delete the context check ...WhenTheCallerGivesUpWaitingForTheSession, ...RunsTheSignalDetachedFromTheCaller
Sentinel on ErrBuildNotRunning (idle backend) ...ReportsAStatementNotRunning
M6: context check hoisted above pollMu.Lock() survives (finding 2)

This review was generated by Claude Code (claude-sonnet-5-5).

@aparajon

Copy link
Copy Markdown
Collaborator

🤖 Review 2/2 (OSS adoption and integration ease) of 228b7de: 0 blocking, 4 non-blocking.

Read the change the way someone embedding pkg/progress would: through the exported surface (go doc-level comments on ErrCancelNotDispatched and CancelBuild), docs/progress-report.md, the neighbouring sentinels, and the test helpers a downstream test would copy. The correctness findings are in comment 1/2.

The change is a good fit for an importer. It is additive, it fits the existing errors.Is vocabulary, %w: %w lets a consumer keep its current context handling, and the PR body states the consumer decision (read a later cancelled-externally as the operator's cancel only when the cancel may have gone out). The gaps below are all about making that decision easy to get right the first time.

Blocking

None.

Non-blocking

1. The consumer's decision is spread across three places and not stated as a table. ErrCancelNotDispatched (progress.go#L255-L262), the CancelBuild comment (L309-L313) and the prose in progress-report.md#L217-L220 each describe one slice. An importer wiring a cancel verb has one question, "may this cancel have reached the build?", and the answer now has four classes, only two of which are named in the new text:

CancelBuild result Signal reached the server? Safe to retry the cancel?
nil sent, accepted not needed
ErrCancelNotDispatched (plus a context error) no yes
ErrNoActiveBuild, ErrBuildNotRunning, ErrBuildUnobservable no signal delivered per each sentinel's own doc
any other error (session failure, a context error without the sentinel) unknown treat as possibly sent

A short table like this in docs/progress-report.md next to the existing ErrBuildNotRunning / ErrBuildUnobservable paragraph would make the contract checkable. It also forces the over-broad sentence in the doc comment (comment 1/2, finding 3) to be fixed, which matters here because a consumer following it literally would treat the three older sentinels as "may have been sent".

2. Check order is part of the contract and is not documented. Because the error matches both the sentinel and the context error, a consumer that already has case errors.Is(err, context.DeadlineExceeded): treat as possibly sent ahead of the new check will keep today's behaviour and silently ignore the new signal. One sentence in the sentinel's comment ("test for this sentinel before testing for the context error") and a three-line consumer snippet in the progress doc would prevent the most likely integration mistake. The existing tests assert both ErrorIs directions, so the double match itself is pinned; only the ordering advice is missing.

3. docs/schemabot-integration.md does not mention the stop path at all. The control-verb table there lists Cancel as "abort and guarantee logical-slot + shadow-table cleanup" and never mentions Tracker.CancelBuild or its outcomes. That is the document a schema change orchestrator reads when mapping its own cancel verb onto pg-sprite. Not a regression of this PR, but this is the first change that gives the mapping a decision to make, so a pointer to the progress doc's outcome table would close the loop.

4. Naming and vocabulary are slightly off from the neighbours. The three existing sentinels read as states of the build (ErrNoActiveBuild, ErrBuildNotRunning, ErrBuildUnobservable); the new one is about the request. That is the right subject, and the doc comment explains it, but the identifier says "dispatched" while the message and every doc sentence say "sent". Using one word in both would help grep-ability for adopters (ErrCancelNotSent, or "dispatched" throughout). Purely cosmetic and not worth blocking a green PR on; mentioned because exported names are the hardest thing to change after a release (the module is pre-1.0, tags v0.3.x, and this is an additive export, so a rename now is cheap).

What works well for adopters

  • Additive and source compatible: existing errors.Is(err, context.Canceled) / DeadlineExceeded checks on CancelBuild keep matching.
  • The wrapped message reads cleanly in logs: cancel concurrent index build backend 4242: the cancel signal was not sent: context deadline exceeded.
  • The test helper blockingSession is a reusable, well-commented way for a downstream test to hold the reserved session; if more tests like these arrive, promoting it to a small test-support file would avoid copies.
  • The PR body's before/after example carries real error text, which makes the behaviour change easy to review.

This review was generated by Claude Code (claude-sonnet-5-5).

@aparajon aparajon left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🤖 Stamping with comments: 0 blocking, 7 non-blocking (see the two review comments above).

This stamp was left by Claude Code (claude-sonnet-5-5).

…ntext

The poll gate is a one-slot channel instead of a mutex, so CancelBuild can
stop waiting for an in-flight poll or another cancel the instant its
caller's context ends, and report ErrCancelNotDispatched then rather than
when the holder lets go. A caller that gives up while the build ends
during the wait is told ErrNoActiveBuild, as before. The observers and
the handoff fences take the gate unconditionally, as they did the mutex.

CancelBuild's doc comment and docs/progress-report.md carry the four-class
outcome table (sent / not sent / nothing delivered / unknown) and name the
check order a consumer needs: the sentinel before the context error. The
earlier text claimed every error other than the sentinel might follow a
sent signal, which was not true of ErrNoActiveBuild, ErrBuildNotRunning
or ErrBuildUnobservable. docs/schemabot-integration.md's Cancel row points
at the table.

Tests pin the wait ending with the caller (the poll is never released
until the outcome is in hand), the build ending during the wait, and the
context being read only once the session is held, through a probe context
that records the read.
@Kiran01bm

Copy link
Copy Markdown
Collaborator Author

🤖 Adversarial review response — created by Kiran's code review agent (Amp, Claude Opus 4.6) — pull/158, follow-up commit

The wait for the reserved session is now bounded by the caller's context (one-slot channel gate in place of the mutex), the context is read only once the session is held and that is pinned, and the consumer-facing contract is one four-class outcome table with its check order, linked from the integration doc.

# Finding Status Explanation
C1-N1 pollMu.Lock() ignores the caller's context; the sentinel arrives only when the holder lets go fixed The poll gate is a one-slot channel; CancelBuild selects on it against ctx.Done(), so a caller that gives up behind a poll or another cancel returns then, with ErrCancelNotDispatched and its context error, and nothing is sent. The other five takers (Progress, the four handoff fences) take the gate unconditionally, as they took the mutex. The suggested TestCancelBuildReturnsWhenTheCallerGivesUpWhileWaiting is in, reshaped to the repo's no-timer rule: the poll is never released until the outcome is in hand, so a context-blind wait only returns at the outcome deadline. A build that ends during the abandoned wait reports ErrNoActiveBuild, not a stale "not sent" (TestCancelBuildReportsNoActiveBuildWhenTheBuildEndsWhileTheCallerWaits).
C1-N2 The headline scenario is not pinned; a check hoisted above the wait survives every test fixed TestCancelBuildReadsTheCallersContextOnlyOnceItHoldsTheSession is in, with the 200 ms settle replaced by a hook: the probe context's Done reports that CancelBuild has started waiting on it, and Err records any read before the poll's release. Mutant M6 (the hoisted check) now fails it and the build-ends-during-wait test.
C1-N3 Doc comment over-claims: the three older sentinels also mean nothing was delivered fixed CancelBuild's comment carries the four classes (nil / sentinel + context error / ErrNoActiveBuild, ErrBuildNotRunning, ErrBuildUnobservable as "nothing delivered" / any other error as "may have reached the build"); the sentinel's comment points at it. docs/progress-report.md is corrected to match.
C1 (verified) Single producer site, precedence of ErrNoActiveBuild, error text, integration callers no action Agreed. There are now two producer sites — the abandoned wait and the post-acquire check — and both precede the signal query; ErrNoActiveBuild keeps precedence at both.
C2-N1 State the consumer's decision as a table fixed The table is in docs/progress-report.md under a new ### Cancelling a concurrent index build anchor, in the form proposed, and in CancelBuild's comment as a list.
C2-N2 Check order is part of the contract and undocumented fixed The sentinel's comment says "test for this sentinel before testing for the context error"; the progress doc explains why and carries the three-line switch snippet.
C2-N3 docs/schemabot-integration.md never mentions the stop path fixed The Cancel row names progress.Tracker.CancelBuild, links the outcome table, and states the check order and when to record the operator's cancel as possibly delivered.
C2-N4 ErrCancelNotDispatched vs "sent" in the message and docs no action Kept as a veto item in the body: the identifier describes the request leaving this process, the message describes what the server saw. A rename to ErrCancelNotSent is a one-line change if the author prefers one word throughout.
C2 (works well) Additive, log text, reusable blockingSession no action Agreed; blockingSession stays in the test file until a second package needs it.
R1 Approval, no findings no action —

Source: block/pg-sprite#158, review comments 6103553458 and 6103554024, review 5481260631 at head 228b7de; fixes in the follow-up commit.

@Kiran01bm
Kiran01bm enabled auto-merge (squash) October 11, 2026 05:45
@Kiran01bm
Kiran01bm merged commit 122d344 into main Oct 11, 2026
16 checks passed
@Kiran01bm
Kiran01bm deleted the kiran01bm/cancel-not-dispatched branch October 11, 2026 05:52
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