Files
acp/cleanup.go
T
Samuel Cui c3bcd856aa feat!: stream caller-owned items and reduce the content options
Replaces the streaming request/result API with caller-owned items and
consolidates the content options, following the Job execution performance
design.

- `Item`, `BatchSource` and `Run` replace `StreamRequest`, `StreamResult`,
  `StreamSource`, `StreamSink` and `RunStream`. Every accepted item receives
  exactly one terminal callback from a single goroutine and without doing
  I/O; an item ACP cannot process reports `Failed`, and a completion carries
  one outcome per requested target in request order with its error identity
  preserved. A graceful stop stops feeding, finishes the items in flight and
  reports the rest, and a hard stop abandons per-item accounting.
- One `HashPolicy` replaces `WithHash`, `WithSignatureCache` and
  `ForceRehash`. A stored hash is read whenever the policy uses the cache,
  reused only where the policy allows it, and refreshed only when it differs.
- A source-only `WithReadMode` selects buffered (the default) or mapped
  reads, and buffered sources open with `O_NOATIME` where the platform has
  it. `LinearDevice` keeps request ordering, append targets and abort-on-error
  but no longer selects the read mode.
- `Overwrite` becomes a target-device option, `WithReadBuffer` sets the read
  buffer, and `SelectFiles` returns the entries the command line copies,
  replacing `AccurateJob`, `WildcardJob` and the walk options.
- Copy consumers share one reference-counted chunk buffer each from a common
  pool, and the per-item write goroutines finish before the result channel
  closes, so no finished item can be dropped.

The command line tools, the documentation and the test suite move with the
API, including semantic tests for the callback contract, the stop and drain
paths, the hash policy matrix and the read modes.
2026-09-19 02:03:34 +08:00

95 lines
2.9 KiB
Go

package acp
import (
"context"
"errors"
"fmt"
"os"
)
// cleanup reports every accepted item exactly once. It is the only goroutine that
// invokes item callbacks, so callbacks arrive one at a time and a slow callback cannot
// stall the read pipeline.
func (c *Copyer) cleanup(ctx context.Context, copyed <-chan *baseJob) {
for {
select {
case job, ok := <-copyed:
if !ok {
return
}
// An item with no target outcome to report could not be processed at all, so
// it fails instead of completing with content facts it never established.
if job.itemError != nil {
c.failedJob(job)
continue
}
c.finishJob(job)
case job := <-c.abandoned:
c.failedJob(job)
case <-c.hardStop:
return
}
}
}
// failedJob reports an accepted item that ACP could not process at all.
func (c *Copyer) failedJob(job *baseJob) {
job.item.Failed(job.itemError)
}
// refreshSignature publishes a computed signature unless the target already stores it.
func (c *Copyer) refreshSignature(path string, signature CachedSignature) {
stored, ok, err := ReadCachedSignature(path)
if err == nil && ok && stored == signature {
return
}
c.signatures.enqueue(path, signature)
}
// finishJob publishes one finished job after restoring its metadata and refreshing the
// content cache.
func (c *Copyer) finishJob(job *baseJob) {
// An item ACP could not process at all is reported as a failure rather than as a
// completion with failed target outcomes.
if job.itemError != nil {
job.setStatus(jobStatusFinished)
job.item.Failed(job.itemError)
return
}
// Restore metadata before publishing the final result.
for _, dst := range append([]string(nil), job.successTargets...) {
if err := mappingError(writeSysStat(dst, job.stat)); err != nil {
c.endLinearTarget(err)
job.fail(dst, fmt.Errorf("change info, write sys stat fail, %w", err))
// Remove the failed target so the same operation can retry it.
if err := os.Remove(dst); err != nil && !errors.Is(err, os.ErrNotExist) {
c.reportItemError(job.path, dst, fmt.Errorf("delete target after metadata failure failed, %w", err))
}
}
}
// Refresh signatures backed by a complete source hash, and only where the stored
// value differs: an identical rewrite costs a physical xattr block on copy-on-write
// filesystems and leaves the cache in the same state.
shouldRefreshSignature := c.signatures != nil && job.hashValid && !job.cacheHit && c.hashPolicy.refreshesCache()
if shouldRefreshSignature {
signature, err := newCachedSignature(job.hash, job.stat)
if err != nil {
c.signatures.recordFailure(job.path, err)
} else {
c.refreshSignature(job.path, signature)
for _, dst := range job.successTargets {
c.refreshSignature(dst, signature)
}
}
}
// Publish only results whose data and metadata lifecycle has finished.
job.setStatus(jobStatusFinished)
job.item.Completed(job.result())
}