Skip to content

Conversation

@juliusmh
Copy link

@juliusmh juliusmh commented Aug 21, 2025

Summary by CodeRabbit

  • New Features

    • Decoder now supports standard stream closing with error reporting and idempotent behavior (safe to close multiple times).
    • Improved resource cleanup on close, ensuring pending work is drained and background operations are cancelled.
  • Refactor

    • Decoder aligns with common stream interfaces for easier integration with existing tooling.
  • Chores

    • Internal wrappers updated to match the new closing semantics.

@coderabbitai
Copy link

coderabbitai bot commented Aug 21, 2025

📝 Walkthrough

Walkthrough

Adds an error-returning, idempotent Close on Decoder, updates interface conformance to io.ReadCloser, revises close wrapper to ignore Close errors, and integrates errors.Is checks with updated shutdown flow (drain output, cancel, wait, close inner decoders, mark closed).

Changes

Cohort / File(s) Summary of Changes
Decoder close semantics & API
zstd/decoder.go
- Change Close signature to Close() error and make idempotent using errors.Is with ErrDecoderClosed
- Update interface assertion to io.ReadCloser
- Adjust shutdown sequence: drain output, cancel, wait for goroutines, close inner decoders, mark closed
- Import errors package
- Update closeWrapper.Close to call Decoder.Close but return nil

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant C as Client
  participant D as Decoder
  participant O as Output Drain
  participant Ctx as Cancelation
  participant G as Goroutines
  participant ID as Inner Decoder

  C->>D: Close()
  alt Already closed
    D->>D: errors.Is(current.err, ErrDecoderClosed)
    D-->>C: return nil
  else Active
    D->>O: drainOutput()
    D->>Ctx: cancel()
    D->>G: wait()
    D->>ID: Close()
    D->>D: set current.err = ErrDecoderClosed
    D-->>C: return nil
  end
Loading
sequenceDiagram
  autonumber
  participant W as closeWrapper
  participant D as Decoder

  W->>D: Close() error
  D-->>W: error (ignored)
  W-->>W: return nil
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

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

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@klauspost
Copy link
Owner

klauspost commented Aug 21, 2025

This is a breaking change.

Use func (*Decoder) IOReadCloser

@klauspost klauspost closed this Aug 21, 2025
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
zstd/decoder.go (1)

575-599: Fix send-on-closed-channel panic and make Close concurrency-safe.

Two high-impact issues in Close:

  1. Closing d.decoders while a concurrent DecodeAll is in-flight can panic. DecodeAll always returns the borrowed block back to the pool in a defer via “d.decoders <- block” (see Line 329). If Close closes the channel first, that deferred send will panic (“send on closed channel”). This is a realistic race: callers can (and do) run stateless decodes concurrently with streaming, and may call Close from another goroutine.

  2. Close is not concurrency-safe. Two goroutines calling Close concurrently can race on closing d.decoders (panic: close of closed channel) and on writing d.current.err without synchronization.

Recommended fix (minimal behavioral change):

  • Track in-flight stateless decodes with a WaitGroup and wait for them to finish before closing the channel.
  • Use sync.Once to make Close concurrency-safe and idempotent.
  • Wait for stream goroutines unconditionally after drainOutput, then wait for stateless decodes, then close and drain the pool.

Apply this diff within Close:

-func (d *Decoder) Close() error {
-	if errors.Is(d.current.err, ErrDecoderClosed) {
-		// Calling Close() multiple times will not throw an error.
-		return nil
-	}
-	d.drainOutput()
-	if d.current.cancel != nil {
-		d.current.cancel()
-		d.streamWg.Wait()
-		d.current.cancel = nil
-	}
-	if d.decoders != nil {
-		close(d.decoders)
-		for dec := range d.decoders {
-			dec.Close()
-		}
-		d.decoders = nil
-	}
-	if d.current.d != nil {
-		d.current.d.Close()
-		d.current.d = nil
-	}
-	d.current.err = ErrDecoderClosed
-	return nil
-}
+func (d *Decoder) Close() error {
+	// Concurrency-safe, idempotent shutdown.
+	d.closeOnce.Do(func() {
+		d.drainOutput()
+		// Ensure stream goroutines have fully terminated.
+		d.streamWg.Wait()
+		// Ensure no in-flight stateless decodes hold a blockDec.
+		d.statelessWg.Wait()
+		if d.decoders != nil {
+			close(d.decoders)
+			for dec := range d.decoders {
+				dec.Close()
+			}
+			d.decoders = nil
+		}
+		if d.current.d != nil {
+			d.current.d.Close()
+			d.current.d = nil
+		}
+		d.current.err = ErrDecoderClosed
+	})
+	return nil
+}

And add the following outside this hunk (new fields + DecodeAll bookkeeping):

  • In type Decoder (near streamWg):
// track in-flight stateless DecodeAll users that hold a blockDec.
statelessWg sync.WaitGroup
// ensure Close runs exactly once even under concurrent calls.
closeOnce   sync.Once
  • In DecodeAll, after taking a block from the pool, account for the in-flight user and ensure Done occurs after the block is returned:
block := <-d.decoders
d.statelessWg.Add(1)
frame := block.localFrame
initialSize := len(dst)
defer func() {
    if debugDecoder {
        printf("re-adding decoder: %p", block)
    }
    frame.rawInput = nil
    frame.bBuf = nil
    if frame.history.decoders.br != nil {
        frame.history.decoders.br.in = nil
        frame.history.decoders.br.cursor = 0
    }
    d.decoders <- block
    d.statelessWg.Done()
}()

This removes the panic window and guarantees Close is safe under concurrency and truly idempotent.

🧹 Nitpick comments (2)
zstd/decoder.go (2)

575-599: Order-of-operations nit: rely on WaitGroup, not cancel presence.

drainOutput() already clears d.current.cancel, so the subsequent “if d.current.cancel != nil { d.streamWg.Wait() }” never runs. Even with the essential refactor above, prefer an unconditional d.streamWg.Wait() after drainOutput() for clarity and robustness.


625-628: Propagate Close errors from the wrapper (or confirm intentional suppression).

closeWrapper.Close currently swallows decoder.Close() errors. If underlying Close ever returns a non-nil error in the future, callers will never see it. If the intent is to preserve old “no-error” behavior for IOReadCloser users, keep as-is; otherwise, propagate:

 func (c closeWrapper) Close() error {
-	c.d.Close()
-	return nil
+	return c.d.Close()
 }

Please confirm which behavior you want long-term; I can follow up with tests either way.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 86a9489 and 4c91501.

📒 Files selected for processing (1)
  • zstd/decoder.go (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
zstd/decoder.go (1)
zstd/zstd.go (1)
  • ErrDecoderClosed (90-90)
🔇 Additional comments (2)
zstd/decoder.go (2)

7-13: Importing errors aligns with idempotent Close semantics.

The new errors import is used by Close via errors.Is and is appropriate for the idempotency check.


68-72: Assert Decoder implements io.ReadCloser (in addition to io.WriterTo).

Good compile-time check. This makes the new Close() error part of the public surface and enables seamless use anywhere io.ReadCloser is expected.

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