Skip to content

Clean up staged outputs when mmdbconvert is interrupted (STF-1809) - #116

Merged
horgh merged 17 commits into
mainfrom
greg/mmdbconvert-cancellation
Sep 29, 2026
Merged

horgh merged 17 commits into
mainfrom
greg/mmdbconvert-cancellation

Conversation

@oschwald

@oschwald oschwald commented Sep 23, 2026 •

Copy link
Copy Markdown
Member

Tracking issue: STF-1809.

Interrupting a conversion currently skips deferred cleanup and can leave large staged CSV, Parquet, or MMDB files beside the output. Add cooperative cancellation that removes unpublished files and preserves existing output before publication begins. Once publication starts, finish the commit sequence.

This follows #115 and STF-1560 and is based on main. The library changes Run to require a context without installing signal handlers. Existing Go callers must pass a context, such as context.Background() when cancellation is not needed. The CLI preserves inherited ignored SIGINT/SIGHUP, identifies handled signals, allows a second signal to terminate blocked cleanup, and restores native interruption behavior after cleanup on Unix and Windows.

Update mmdbwriter/v2 to cc514d224474 and adapt Uint128 conversions to its new value representation. Nested values in CSV and Parquet string columns encode as exact decimal JSON numbers. Low-word scalar conversions avoid big-integer allocations while preserving overflow checks and high-word float rounding.

mmdbwriter #272 tracks adding upstream Uint128 JSON marshaling and then replacing this converter’s custom normalization with ordinary json.Marshal.

Acceptance criteria:

  • Change the library API to Run(ctx context.Context, opts Options) error. This is a breaking change; callers must pass a context. Library calls must not install signal handlers.
  • Cancellation before publication preserves existing outputs and removes staged files for all formats, including split CSV/Parquet outputs. Preserve both conversion and cleanup errors.
  • Handle SIGINT/SIGTERM on Unix and Windows, and SIGHUP on Unix, identifying the handled signal in diagnostics. Preserve inherited ignored SIGHUP under nohup and SIGINT in background jobs. Do not subscribe to any candidate signal Go reports as ignored. Other platforms retain native signal handling without installing handlers; existing builds remain supported.
  • Signal warnings use the structured error field and identify successful publication when conversion completes. Nonzero results do not imply that outputs were unpublished; unrelated parent cancellation is reported separately only when the callback is unsuccessful.
  • Restore default handling after the first handled signal so a second handled signal can terminate blocked work immediately. Do not debounce or delay the second signal.
  • Finish the commit sequence once publication starts. After cleanup, re-raise the handled signal on Unix even if publication completed, so interrupted shell loops stop. If re-raising fails or the process remains alive after one second, log the condition and fall back to 128 + signal number.
  • On Windows, use the native console termination status STATUS_CONTROL_C_EXIT (0xC000013A, signed -1073741510) after cleanup.
  • Document cooperative cancellation, inherited ignored signals, exit behavior, and that forced termination can leave temporary files. Explain that duplicate delivery by signal-forwarding wrappers can force termination from a single user action, and that Windows limits cleanup time for console close, logoff, and shutdown events.
  • Cover cancellation during merging, the final accumulator flush, writing, and format serialization through the production writer setup; preserve optional range-writing behavior in test wrappers. Cover cleanup failures, publication completion, queued signals at shutdown, Unix signal behavior, and the Windows exit status.
  • Unix signal tests tolerate inherited ignored signals, and shell-dependent tests skip when the required shell is unavailable.
  • Tests and lint pass on Linux, macOS, and Windows.

Validation:

  • go test -race -count=1 ./... and scoped precious lint pass at faa374b.
  • Focused signal and shell-helper cleanup tests pass with -race -count=10. Inherited ignored SIGINT/SIGHUP checks pass; Bash-dependent tests skip when Bash is unavailable.
  • CLI test compilation passes for Windows, macOS, Plan 9, js/wasm, and wasip1/wasm.
  • New regressions cover a received signal after parent cancellation, callback success/failure diagnostics, and cleanup when Bash exits while a helper still holds stderr open.
  • A source-overlay mutation removing the write cancellation check makes the existing two-write adapter test fail. The real-serializer cancellation and final-accumulator-flush tests remain in place; earlier mutation checks confirmed their production wiring and pre-serialization cancellation boundary.
  • Native Linux, macOS, and Windows tests, CI lint, and the dependency freshness check pass at faa374b.

Summary by CodeRabbit

  • New Features
    • Conversions can be canceled through the Go API or by sending a termination signal to the command-line tool. Go callers provide a context to Run; use a background context when cancellation is not needed.
    • Cancellation before publishing preserves existing outputs and cleans up temporary files. If publishing has started, it completes before exit.
  • Bug Fixes
    • Nested 128-bit integers in maps and arrays are serialized as exact JSON numbers in CSV and Parquet string columns.
  • Documentation
    • Added guidance on cancellation, signal handling, and output behavior during shutdown.

@coderabbitai

coderabbitai Bot commented Sep 23, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 9a1cd77b-f002-4ce4-be81-6b50c50ba9d8

📥 Commits

Reviewing files that changed from the base of the PR and between 29f223d and faa374b.

📒 Files selected for processing (8)
  • CHANGELOG.md
  • cancellation_test.go
  • cmd/mmdbconvert/main.go
  • cmd/mmdbconvert/signals.go
  • cmd/mmdbconvert/signals_test.go
  • cmd/mmdbconvert/signals_unix_test.go
  • docs/config.md
  • mmdbconvert.go

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

The conversion API and merger now accept contexts. Cancellation checks cover conversion, output preparation, writes, and publication. The CLI handles termination signals with platform-specific behavior. CSV and Parquet writers and format detection now handle Uint128 values.

Changes

Conversion cancellation

Layer / File(s) Summary
Context-aware conversion and merging
mmdbconvert.go, internal/merger/*, mmdbconvert_test.go, variables_test.go, format_integration_test.go, geoip_csv_test.go, internal/writer/integration_test.go, CHANGELOG.md
Run and Merger.Merge accept contexts. Conversion passes the context through merger processing, and callers and tests use the updated signatures.
Output cancellation and publication
output.go, output_test.go, cancellation_test.go
Output preparation and writes check for cancellation. Cancellation before publication prevents committing staged outputs; publication already in progress finishes all commits. Tests check cancellation, cleanup errors, and preservation of existing outputs.
CLI signal handling
cmd/mmdbconvert/*, docs/config.md, CHANGELOG.md
The CLI cancels conversion on handled signals. Unix handling covers non-ignored SIGINT, SIGTERM, and SIGHUP; Windows has separate signal exit behavior. Tests cover signal cancellation, repeated signals, and CLI error output.

Uint128 writer conversion

Layer / File(s) Summary
Uint128 writer conversions
internal/writer/csv.go, internal/writer/csv_test.go, internal/writer/json.go, internal/writer/json_test.go, internal/writer/format.go, internal/writer/format_test.go, internal/writer/parquet.go, internal/writer/parquet_test.go, go.mod
CSV and Parquet conversions, JSON normalization, and format detection now handle mmdbtype.Uint128 values. Tests cover string output, exact JSON numbers, and numeric conversion boundaries. The mmdbwriter requirement was updated.

Priority: ➖ Normal

Estimated code review effort: 4 (Complex) | ~45 minutes

Change: Bug fix

Sequence Diagram(s)

sequenceDiagram
  participant CLI as runCLI
  participant Signals as runWithSignalChannel
  participant Run as mmdbconvert.Run
  participant Merger as Merger.Merge
  participant Output as flushAndCommit
  CLI->>Signals: Start conversion callback
  Signals->>Run: Provide cancel-cause context
  Run->>Merger: Pass context to Merge
  Signals->>Run: Cancel context on first handled signal
  Run->>Output: Pass context to publication
  Signals->>Signals: Exit immediately on a second handled signal
Loading

Suggested reviewers: horgh

Merge Risk: ⚪ Minimal · up to faa37

The change adds context cancellation and signal-driven cleanup with tests and documentation. No actionable merge-blocking issue remains in the supplied evidence.

Security Architecture Review

Security architecture risk: 🔵 Low · up to faa37

Staged publication reduces the chance that an interrupted conversion replaces existing output with incomplete data. The review found no demonstrated new security exposure, but the public API and interruption behavior warrant design review, and some deployment-specific effects remain unverified.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The directly affected assets are the configured local output files and their staged counterparts. The reviewed caller passes a process-signal-driven context; no network entrypoint or additional privileged caller is established by this evidence.

Trust Boundaries and Controls

  • observed — A caller can request cancellation but cannot use that context to skip the pre-publication check or selectively stop commits once publication has begun. The CLI keeps signal handling outside the library.

Resilience and Maintainability Implications

  • observed — A second handled signal can terminate blocked CLI cleanup immediately. Unix subprocess tests exercise that escape path, so cleanup completion is not guaranteed after a second signal.

Hardening Proposals

  • proposed — If consumers require IPv4 and IPv6 outputs to form one security-sensitive snapshot, document the existing nontransactional pair or publish versioned pairs through a single generation pointer.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.07% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 112 functions across 32 files. (2 skipped… Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the primary change: cleaning up staged outputs when mmdbconvert is interrupted. It is concise and directly related to the pull request objectives.
Full details: Docstring Coverage

Explanation

Docstring coverage is 16.07% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 112 functions across 32 files. (2 skipped: 2 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

A rabbit taps the context bell,
The maps keep numbers whole and well.
Through signals, rows, and files that flow,
Old outputs wait till commits go.
Then carrots crunch; the tests all pass,
A hop of joy across the grass!

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

@oschwald
oschwald force-pushed the greg/mmdbconvert-cancellation branch from cecdf38 to 0c98293 Compare September 23, 2026 20:06
@oschwald
oschwald force-pushed the greg/stf-1560 branch 2 times, most recently from 7106b73 to 2ed86bc Compare September 23, 2026 20:16
@oschwald
oschwald force-pushed the greg/mmdbconvert-cancellation branch 2 times, most recently from 85c2be9 to d822ddd Compare September 24, 2026 15:13
@oschwald
oschwald force-pushed the greg/stf-1560 branch 2 times, most recently from 32a851e to 3788e22 Compare September 24, 2026 15:44
@oschwald
oschwald force-pushed the greg/mmdbconvert-cancellation branch 2 times, most recently from 9c83bf7 to 4c9c4f7 Compare September 24, 2026 16:26
@oschwald
oschwald force-pushed the greg/mmdbconvert-cancellation branch 2 times, most recently from 155a93f to 78cf7fb Compare September 24, 2026 18:04
@oschwald
oschwald force-pushed the greg/mmdbconvert-cancellation branch from 78cf7fb to a736943 Compare September 24, 2026 18:47
Base automatically changed from greg/stf-1560 to main September 25, 2026 22:11
@oschwald
oschwald force-pushed the greg/mmdbconvert-cancellation branch from 42e38b7 to 90d94cf Compare September 28, 2026 20:26
@oschwald
oschwald force-pushed the greg/mmdbconvert-cancellation branch from a5476d6 to 3d3e38e Compare September 28, 2026 21:03
@oschwald
oschwald marked this pull request as ready for review September 28, 2026 21:13
@oschwald oschwald changed the title Clean up staged outputs when mmdbconvert is interrupted Clean up staged outputs when mmdbconvert is interrupted (STF-1809) Sep 28, 2026
Require a context in Run and keep library calls free of signal handlers.
Check cancellation through merging, output writes, and serialization; clean
unpublished files and preserve cleanup errors. Finish publication once it starts.

Update existing callers for the breaking API change. Cover all formats and
split outputs, production writer capabilities, final accumulator flush
cancellation, cleanup failures, and publication completion.
Cancel conversion on handled signals while preserving inherited ignored SIGINT and SIGHUP. Drain queued signals at shutdown, identify interruptions separately from conversion errors, and allow a second signal to terminate blocked cleanup.

Re-raise Unix signals after cleanup and use the native Windows console exit status. Include platform signal tests and document cooperative cleanup, duplicate forwarding, and numeric fallbacks.
@oschwald
oschwald force-pushed the greg/mmdbconvert-cancellation branch from 3a549f0 to b7b11a8 Compare September 28, 2026 21:25
@horgh
horgh requested a balanced review from Copilot September 28, 2026 21:45

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@horgh
horgh requested a balanced review from Copilot September 28, 2026 21:55

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@horgh horgh 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, but Claude comments.

Comment thread internal/writer/csv.go Outdated
case *mmdbtype.Uint128:
return (*big.Int)(v).String(), nil
case mmdbtype.Uint128:
return v.BigInt().String(), nil

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.

This fix covers only a top-level Uint128. A Uint128 inside a Map or Slice goes to json.Marshal in the mmdbtype.Map and mmdbtype.Slice cases below (line 434). After the mmdbwriter update, Uint128 is a value struct, so JSON encodes its internal fields.

For example, {"n": Uint128{Low: 42}} now gives {"n":{"High":0,"Low":42}}. Before the update it gave {"n":{}}. Neither is the decimal value 42. This also affects Parquet string columns that select a map. No test covers the nested case.

🤖 Comment by Claude Opus 5.5.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in e2fec23. Nested maps and slices now encode Uint128 values as exact, unquoted decimal JSON numbers without changing the input. This covers CSV and Parquet string columns, including the default string conversion. Tests cover nested and maximum values, shared containers, cycles, and nonfinite values; JSON encoding errors remain intact.


🤖 Comment by Codex on behalf of Greg.

Comment thread cmd/mmdbconvert/signals_unix.go Outdated
@@ -0,0 +1,37 @@
//go:build !windows

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.

!windows also selects js/wasm and plan9. These targets do not have syscall.SIGHUP, syscall.Kill or syscall.Signal, so the CLI no longer compiles for them:

$ GOOS=js GOARCH=wasm go build ./cmd/mmdbconvert
cmd/mmdbconvert/signals_unix.go:16:73: undefined: syscall.SIGHUP
$ GOOS=plan9 go build ./cmd/mmdbconvert
cmd/mmdbconvert/signals_unix.go:28:20: undefined: syscall.Kill

Both build on main. They are not in the release list, so the effect is small. //go:build unix, plus a fallback file for the other targets, fixes it.

🤖 Comment by Claude Opus 5.5.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Not sure about this, but I guess it is fine to add support for them.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 790e1d1. The Unix implementation and tests now use //go:build unix; other non-Windows platforms keep their native signal handling through a fallback that installs no handlers. CLI test compilation passes for Windows, macOS, Plan 9, js/wasm, and wasip1/wasm. This preserves existing builds without adding release targets.


🤖 Comment by Codex on behalf of Greg.

Comment thread cmd/mmdbconvert/signals.go Outdated
}
code, cause := runWithSignalChannel(ctx, run, signals, logger)
if cause != nil {
logger.Warn("Conversion interrupted", "cause", cause)

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.

This warning is logged whenever the context has a cause, even when the conversion finished and published its outputs. If SIGTERM arrives during Commit, or just after run returns, the log shows "Successfully completed" and then "Conversion interrupted", and the process exits by signal. An operator cannot tell from the log if the outputs were replaced.

Also, this call uses the key cause. Other log calls use the key error, so JSON log consumers that look for error miss this one.

🤖 Comment by Claude Opus 5.5.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 5a22ec6. A signal with a successful callback now logs Signal received; conversion completed and outputs were published. A nonzero result logs Signal received, since profiling or publication errors can occur after outputs were replaced. Both use error. Unrelated parent cancellation logs Conversion context canceled. Tests cover both signal outcomes and the JSON fields. Native signal termination is preserved even after successful publication so interrupted shell loops stop.


🤖 Comment by Codex on behalf of Greg.

Comment thread internal/writer/parquet.go Outdated
case *mmdbtype.Uint128:
i := (*big.Int)(v)
case mmdbtype.Uint128:
i := v.BigInt()

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.

v.BigInt() allocates a big.Int for each Uint128 cell. The same is true in the float64 case at line 421 and in convertToString at csv.go:426. The old pointer conversion did not allocate, and the new value type makes the allocation unnecessary. For example:

case mmdbtype.Uint128:
    if v.High != 0 || v.Low > math.MaxInt64 {
        return nil, fmt.Errorf("uint128 value %s overflows int64", v.BigInt())
    }
    return int64(v.Low), nil

In the CSV path, strconv.FormatUint(v.Low, 10) works when v.High == 0.

🤖 Comment by Claude Opus 5.5.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Hmm, not sure we would want to error if the uint128 is not a int64.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 315438d. Low-word values use direct int64/float64 conversions and strconv.FormatUint, avoiding BigInt. High-word float64 conversion keeps BigInt.Float64 to avoid double rounding; a regression test covers that case.

The overflow error applies only when the column explicitly requests type = "int64", preserving the existing range check. Uint128 values outside int64 still work in CSV and Parquet string columns, including the default string conversion.


🤖 Comment by Codex on behalf of Greg.

Comment thread cmd/mmdbconvert/signals_unix_test.go Outdated
}
}

func TestCLISecondSignal(t *testing.T) {

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.

TestCLISignals, TestCLISecondSignal and TestCLIInterruptStopsShellLoop each repeat the same subprocess setup: os.Executable, exec.CommandContext, the environment, the stderr buffer, StdoutPipe, Start, and the read of ready. That is about 15 lines, three times. A change to the handshake must be made in three places.

TestCLISignals is TestCLISecondSignal without the second signal, so those two can be one table-driven test. A shared helper can start the child for all three.

🤖 Comment by Claude Opus 5.5.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 2c32b0e. Single- and second-signal cases now share one table. A common subprocess helper also serves the shell-loop test and owns the timeout, readiness handshake, buffered reader, stderr capture after Wait, and cleanup/reaping. Repeated signal tests pass with the race detector. Checks with inherited ignored SIGINT/SIGHUP and missing Bash also pass.


🤖 Comment by Codex on behalf of Greg.

Comment thread cmd/mmdbconvert/main.go
})
}

func runConversion(

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.

Nit: runCLI calls extraConfigArgsMessage first (line 139) and runConversion later (line 165). The project CLAUDE.md orders helpers by first call, so runConversion goes after extraConfigArgsMessage.

🤖 Comment by Claude Opus 5.5.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 194661f: extraConfigArgsMessage now precedes runConversion, matching their first-call order.


🤖 Comment by Codex on behalf of Greg.

Comment thread cancellation_test.go Outdated
_, cfg, paths := outputTestConfig(t, "mmdb", false, `["country_code"]`)
require.NoError(t, os.WriteFile(paths[0], []byte("previous output"), 0o600))
// A single data range stays in the accumulator until Merge's final Flush.
tree, err := mmdbwriter.New(mmdbwriter.Options{IPVersion: 4})

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.

Nit: createCSVTestDatabase in mmdbconvert_test.go already builds a small MMDB for tests in this package. createCSVTestDatabase(t, 4, []string{"1.0.0.0/24"}) with a ["value"] column path gives the same single-range input with less code.

🤖 Comment by Claude Opus 5.5.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 14e276b using createCSVTestDatabase(t, 4, []string{"1.0.0.0/24"}) and the ["value"] path. The existing cancellation assertions remain. A source-overlay mutation removing the pre-serialization cancellation check still makes this test fail because Flush is called.


🤖 Comment by Codex on behalf of Greg.

@horgh horgh 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 ran another Claude review. Feel free to re-deliver if you like.

}
if interrupted {
//revive:disable-next-line:deep-exit A second signal must bypass blocked cleanup.
os.Exit(exitAfterSignal(sig, logger))

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.

One user interrupt that arrives twice counts as the second signal. The CLI then exits at once and skips cleanup.

This can happen in two ways:

  • A wrapper in the same process group forwards SIGINT (for example, npm or pnpm runners, or some init shims). Ctrl-C then delivers SIGINT from the terminal and again from the wrapper.
  • A terminal closes. The kernel sends SIGHUP to the foreground group, and the shell also sends SIGHUP to its jobs.

The first copy cancels the context. The second copy either is queued before signal.Stop and reaches this os.Exit, or arrives after Stop and gets the default action. Both paths end the process before the deferred cleanup in convert. The .mmdbconvert-* staged files then stay next to the output, and split publication can stop partway through.

A short grace period, or ignoring a repeat of the same signal for a short time, would separate a duplicate delivery from a deliberate second signal.

🤖 Comment by Claude Opus 5.5.

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.

Doesn't seem very important.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Keeping immediate termination on a second handled signal, as agreed for this PR. A grace period would delay the escape from blocked cleanup. The configuration documentation already explains that direct delivery plus wrapper forwarding can trigger this with one Ctrl-C and leave staged files behind.


🤖 Comment by Codex on behalf of Greg.

Comment thread cmd/mmdbconvert/signals.go Outdated
// so a second signal can terminate blocked cleanup.
signal.Stop(signals)
interrupted = true
cancel(&signalError{signal: sig})

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.

If the parent context is already canceled, cancel(&signalError{...}) does nothing. context.WithCancelCause keeps the parent's cause.

For example, the caller of runCLI(ctx) cancels ctx, and then SIGINT arrives. This goroutine reads the SIGINT, calls signal.Stop, and calls cancel. context.Cause returns the parent's error, so errors.AsType[*signalError] in runWithSignals fails. The CLI logs "Conversion context canceled" and returns 1. It does not re-raise SIGINT. A shell loop around the process sees a normal exit 1, and the interrupt is lost.

The goroutine could record the signal in a variable that runWithSignals checks, instead of using only the context cause.

🤖 Comment by Claude Opus 5.5.

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.

Not sure this matters either.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 1331f04. The watcher records the first received signal independently of the context cause, and the caller reads it after the watcher finishes. The callback retains the original parent cause, while a received signal still controls process termination. Regression tests cover an already-canceled parent with callback results 0 and 1.


🤖 Comment by Codex on behalf of Greg.

return exitAfterSignal(interruption.signal, logger)
}
if cause != nil {
logger.Warn("Conversion context canceled", "error", cause)

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.

Follow-up to the earlier thread about the success log: this branch still logs a cancellation warning when the conversion succeeded.

If the parent context is canceled after mmdbconvert.Run returns nil, but before runWithSignalChannel reads context.Cause (for example, while the memory profile is written), the CLI returns 0. It also logs a WARN record that says the conversion context was canceled. The signal branch above checks code. This branch does not.

🤖 Comment by Claude Opus 5.5.

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.

Probably fine.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 1331f04: a non-signal parent cancellation warning is now emitted only for a nonzero callback result. Tests cover both successful and unsuccessful callbacks. Signal diagnostics and native signal termination still apply after successful publication.


🤖 Comment by Codex on behalf of Greg.

Comment thread cancellation_test.go Outdated
require.Equal(t, "first", buf.String())
}

func TestConvert_CancellationDuringWrite(t *testing.T) {

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.

This test does not test the check in contextWriter.Write. I removed the ctx.Err() check from contextWriter.Write, and all three subtests still passed.

With this small fixture, the CSV and MMDB writers buffer all output and send it in one Write when Flush runs. cancelWriteOutput cancels only after that write succeeds. The error therefore comes from the context check after the flush in flushAndCommit. Parquet also passes. Only the flush stage of TestConvert_CancellationCleansOutputs catches a regression in contextWriter.

To test the write check, cancel after the first write and make sure a second write happens. For example, use a writer that cancels on its first Write, or a fixture that is larger than the buffer.

🤖 Comment by Claude Opus 5.5.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Renamed this to TestConvert_CancellationAfterOutputWrite and clarified its purpose in 9790cfe: cancellation after a successful underlying write must preserve the old output and remove staged files. The existing TestContextWriter_Cancellation already writes once, cancels, attempts a second write, and checks zero additional bytes. I confirmed with a source-overlay mutation that removing the adapter check makes that test fail. The real-serializer flush tests remain in place.


🤖 Comment by Codex on behalf of Greg.

Comment thread mmdbconvert.go
// Run does not install signal handlers.
func Run(ctx context.Context, opts Options) error {
if err := ctx.Err(); err != nil {
return fmt.Errorf("starting conversion: %w", err)

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.

The cancellation errors wrap ctx.Err(), not context.Cause(ctx). A library caller does not get the cause back from Run.

For example, a caller uses context.WithCancelCause and cancels with errors.New("shutdown requested"). Run returns starting conversion: context canceled, and the caller's cause is not in the error chain. The same is true at output.go:227 ("writing output"), in the merger, and in flushAndCommit. The CLI reads context.Cause separately to work around this. Any other caller must keep the context and do the same.

context.Cause(ctx) wraps context.Canceled when no cause was set, so errors.Is(err, context.Canceled) still works if these sites use it.

🤖 Comment by Claude Opus 5.5.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Keeping the standard ctx.Err() contract and documenting it in e666831. Returned cancellation errors remain discoverable as context.Canceled or context.DeadlineExceeded; callers can inspect their custom reason through context.Cause(ctx). Replacing ctx.Err() with context.Cause() alone would lose that standard error matching when the custom cause does not wrap the sentinel.


🤖 Comment by Codex on behalf of Greg.

Comment thread cmd/mmdbconvert/signals_unix_test.go Outdated
t.Cleanup(func() {
if opts.shellLoop {
// Kill any surviving second child even if the shell was already reaped.
if err := syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL); err != nil {

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.

This cleanup can send SIGKILL to a process group ID that another process has reused.

In TestCLIInterruptStopsShellLoop, child.wait reaps bash, which is the group leader. If no member of the group is still alive, the kernel can give that PGID to a new group, for example a parallel test or a CI process. syscall.Kill(-pid, SIGKILL) then kills that unrelated group. This is unlikely, but it can happen on busy CI hosts.

It is safer to kill the group before the leader is reaped, or only while a group member is known to be alive.

🤖 Comment by Claude Opus 5.5.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 9a64fc3. Removed both the cleanup group kill and the custom group-killing cancellation callback. Shell-loop helpers inherit a stdin pipe and exit when it closes; an independent context callback closes it on timeout even after Bash is reaped. Cleanup also closes it and reaps the command once. A regression test kills only Bash, then verifies pipe closure stops the helper and lets stderr draining finish. Repeated race tests pass.


🤖 Comment by Codex on behalf of Greg.

Comment thread CHANGELOG.md
plus SIGHUP on Unix. Cancellation cleans up temporary files and preserves
existing output until publication starts.

### Changed

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.

The changelog has no entry for the change to nested uint128 values in JSON-encoded map and array columns.

With the earlier mmdbwriter version, mmdbtype.Uint128 was type Uint128 big.Int, which has no exported fields. json.Marshal therefore wrote nested uint128 values as {} in CSV and Parquet string columns. This branch writes them as exact decimal numbers. Users who parse these columns, or who worked around {}, get different output with no release note. A "Fixed" entry would cover it.

🤖 Comment by Claude Opus 5.5.

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.

This one seems possibly worth mentioning.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I've never seen a uint128 in the wild, but I'll add it.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Added the Fixed entry in c30641b: nested 128-bit unsigned integers in maps and arrays now serialize as exact JSON numbers in CSV and Parquet string columns.


🤖 Comment by Codex on behalf of Greg.

Comment thread internal/writer/json.go
"github.com/maxmind/mmdbwriter/v2/mmdbtype"
)

func normalizeMMDBJSON(value mmdbtype.DataType) any {

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.

This deep-copy walker exists only because mmdbtype.Uint128 has no json.Marshaler. That type is in mmdbwriter v2, which we own and which is not released yet. That is a better place for the fix.

Each Map or Slice column value on each row is copied into map[string]any/[]any with a new memo map. In a local benchmark, this allocated about 3 times as many bytes per value (738 B to 2205 B). The pointer/length/kind memo protects against cycles that MMDB decoding cannot make.

A MarshalJSON on mmdbtype.Uint128 that writes a decimal json.Number would remove json.go and jsonContainer. It would also fix JSON encoding for other mmdbwriter users.

🤖 Comment by Claude Opus 5.5.

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.

That might be an idea.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yeah, I considered this but didn't really think anyone else would be serializing it as JSON. I'll make a follow-up issue.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Created mmdbwriter #272 for both parts: add value-receiver Uint128.MarshalJSON upstream, then update this dependency and remove the converter’s recursive normalization code. The follow-up retains exact-number and JSON-error regression coverage.


🤖 Comment by Codex on behalf of Greg.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The internal issue is ENG-5506.

Comment thread cmd/mmdbconvert/main.go

// run performs the main conversion process.
func run(opts mmdbconvert.Options, logger *slog.Logger) error {
func run(ctx context.Context, opts mmdbconvert.Options, logger *slog.Logger) error {

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.

Nit, follow-up to the earlier ordering thread: runCLI calls usage first (line 101), before extraConfigArgsMessage (line 139) and runConversion (line 165). But usage is still last in the file (line 256). By first-call order, usage goes before extraConfigArgsMessage.

🤖 Comment by Claude Opus 5.5.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in faa374b: usage now immediately follows runCLI, before extraConfigArgsMessage and runConversion.


🤖 Comment by Codex on behalf of Greg.

@horgh
horgh merged commit 1dd9434 into main Sep 29, 2026
19 checks passed
@horgh
horgh deleted the greg/mmdbconvert-cancellation branch September 29, 2026 19:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants