Skip to content

feat(b2): add a progress callback for uploads and downloads - #131

Open
ffumero2003 wants to merge 1 commit into
mainfrom
feat/progress-callback
Open

ffumero2003 wants to merge 1 commit into
mainfrom
feat/progress-callback

Conversation

@ffumero2003

Copy link
Copy Markdown
Contributor

Context

blazer reported transfer progress only by polling Client.Status() (b2/monitor.go) — per-chunk ratios, no callback, no running byte total, no terminal event. The peer SDKs expose a progress listener, so client.progress read partial.

Changes

  • WithProgress(cb func(bytesComplete, bytesTotal int64)) WriterOption for uploads (mirrors WithLegalHold), and a settable Reader.Progress field for downloads (no constructor signature change).
  • Callbacks fire per completed part/chunk, with a cumulative total maintained via atomic.AddInt64 and delivery serialized under a mutex (may be invoked from an upload/download worker goroutine — documented).
  • bytesTotal is the known size for ReadFrom of an io.Seeker (upload) or a finite NewRangeReader length (download), and -1 when unknown (plain Write, unbound streams, open-ended download).
  • Reuses the existing meteredReader/completeChunk plumbing; Client.Status() and the pull model are untouched (additive).

Verification

  • go build ./... — clean.
  • go test ./base/... ./b2/... -race — green, incl. new in-process callback tests (upload + download: fires, monotonic, ends at total).
  • golangci-lint run --new-from-rev=origin/main ./... — 0 issues.
  • Repo-owned conformance client.progress @simulator — PASS (the test was rewritten from a pull-only reflection guard to a callback assertion: fires, monotonic, final == size).
  • Real-B2 demo across 8 usage paths (large/small/known-total/unknown-total upload; full/ranged/parallel download; unset-callback safety) — all PASS.

Note

Shares b2/writer.go with the SSE-C / SSE-B2 / compose branches, so whichever merges after the others needs routine conflict resolution.

Closes #130

@ffumero2003 ffumero2003 added the enhancement New feature or request label Oct 16, 2026
@ffumero2003 ffumero2003 self-assigned this Oct 16, 2026
@ffumero2003 ffumero2003 added this to the v0.9.0 milestone Oct 16, 2026

@jakub-zytka-reef jakub-zytka-reef 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.

I am really confused by this PR.
The issue description says:

Every transferred byte already flows through one primitive, meteredReader (b2/writer.go:720), for both upload paths and downloads, and parts are marked done at completeChunk — so a callback can be fired from the existing plumbing without touching the concurrent-upload goroutine model.

This makes sense - let's update the progress as the bytes flow.
But then the PR updates progress only on full chunk download/upload. This is by default 10MB on download, 100MB on upload.
So if I'm uploading 80MB files the progress is essentially either 0% or 100%.

I don't have full business context regarding this feature, but this really doesn't seem right.

Comment thread b2/writer.go
}
cnk.buf.Close()
w.completeChunk(cnk.id)
w.completeChunk(cnk.id, completedBytes(cnk.buf))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

does that really work?
Doesn't cnk.buf.Close sets the buf to nil, so the completedBytes will panic?

Is there at test that covers this?

Comment thread b2/writer.go
}
w.progressMu.Lock()
defer w.progressMu.Unlock()
completed := atomic.AddInt64(&w.progressCompleted, bytesComplete)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

IIRC go will panic if atomic instructions are used on non-8-bytes aligned fields.
This is unlikely to happen (raspberry pi? ancient PC?)

As I see it - atomics are not needed currently:

  1. progressCompleted is only used under the lock (i.e. no need for atomic)
  2. progressTotal is not set concurrently with reads (this would only be a problem on 32bit anyway)

Or perhaps we could use atomic.Int64 which are aligned correctly?

Comment thread b2/writer.go
defer w.progressMu.Unlock()
completed := atomic.AddInt64(&w.progressCompleted, bytesComplete)
if w.progress != nil {
w.progress(completed, atomic.LoadInt64(&w.progressTotal))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I feel uneasy about running an arbitrary callback under a lock. Additionally, reportProgress can be called from within Close, which also keeps some lock.
It may be tricky to avoid deadlocks etc.

Would it perhaps be better to use a 1-element channel instead?

Comment thread b2/reader.go
}
r.progressMu.Lock()
defer r.progressMu.Unlock()
completed := atomic.AddInt64(&r.progressCompleted, bytesComplete)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

same as in the writer - atomic operations under lock on potentially unaligned data

Comment thread b2/reader.go
defer r.progressMu.Unlock()
completed := atomic.AddInt64(&r.progressCompleted, bytesComplete)
if r.Progress != nil {
r.Progress(completed, atomic.LoadInt64(&r.progressTotal))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

please see the comment in the writer

Comment thread b2/writer.go
if err != nil {
return 0, err
}
atomic.StoreInt64(&w.progressTotal, size)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

why do we set progressTotal only here?
What if we are resuming?

Are there perhaps other scenarios where we can set total up front, like when we know the file size?

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

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(b2): add a progress callback for uploads and downloads

2 participants