mirror of
https://github.com/agent-substrate/substrate.git
synced 2026-10-27 07:34:12 +08:00
Part of #1827 Moves `cmd/atelet/internal/ategcs` to `pkg/objectstorage` and renames the package to `objectstorage`. No code changes besides the package name, its import path and uses, the tracer name, and comments that named the old package. **Why:** the [Platform-Provided Snapshot API Proposal](https://docs.google.com/document/d/1ac-hY6zsmX4FmtGOt9CXbNFHeluGouyYluer7kX6bto) moves snapshot transfer out of atelet and ate-api-server into a snapshot plugin binary that runs as a sidecar. The reference plugin (GCS and S3) reuses this package's transfer code as is, and so can snapshot plugins built outside this repository, which is why it goes in `pkg/` rather than `internal/`. **Naming:** `objectstorage`, not `objectstore`, because `internal/objectstore` already exists. That package only lists, copies and deletes objects for the control plane and is deliberately separate from this one (see its package comment, updated here). The snapshot plugin binary imports both. This PR stands alone and changes no behavior. **Reviewing:** `git show -M --stat` shows 20 renames as `{cmd/atelet/internal/ategcs => pkg/objectstorage}/...`. Each changes only its `package` clause, except: - `objects.go`: tracer name `ategcs` → `objectstorage` - `gcscompose_test.go`, `s3bench_test.go`: the `go test ./pkg/objectstorage` path in a comment; `gcscompose_test.go` also renames its emulator test bucket The other 4 files: - `cmd/atelet/{main,main_test,sandbox_assets}.go`: import path and `ategcs.` → `objectstorage.`, plus one comment in `main_test.go` - `internal/objectstore/objectstore.go`: package comment now points at `pkg/objectstorage` - [x] Tests pass (`make test`, `make verify`) - [x] Appropriate changes to documentation are included in the PR (none needed)
107 lines
3.7 KiB
Go
107 lines
3.7 KiB
Go
// Copyright 2026 Google LLC
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package objectstorage
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
|
|
"cloud.google.com/go/storage"
|
|
"golang.org/x/sync/errgroup"
|
|
)
|
|
|
|
// partWriterChunk is how much of a part the GCS client buffers before it sends. This
|
|
// bounds the memory a parallel upload holds: sparseMaxParts * this.
|
|
const partWriterChunk = 16 << 20
|
|
|
|
// PutSparseFile compresses and uploads a sparse file as parts that are composed back
|
|
// into one object, fanning the WHOLE pipeline out by file range: each part reads its
|
|
// own extents (ReadAt), compresses them, and streams into its own object over its own
|
|
// connection, so every stream starts at once.
|
|
//
|
|
// Piping one compressed stream into a part splitter instead serializes the fan-out
|
|
// behind a single compressor: measured on a GKE worker node, a 541 MB actor took 1.78s
|
|
// that way against 87 MB/s-per-stream x 8 streams available from the node.
|
|
//
|
|
// The parts are consecutive byte ranges of the same sparse-extent stream — part 0
|
|
// carries the header, the last carries the end sentinel — so the composed object is
|
|
// exactly what the single-stream writer would have produced.
|
|
func (g *gcsClient) PutSparseFile(ctx context.Context, bucket, object string, f *os.File) (res writeContentResult, err error) {
|
|
fi, err := f.Stat()
|
|
if err != nil {
|
|
return res, err
|
|
}
|
|
size := fi.Size()
|
|
exts, populated, err := sparseExtents(f, size)
|
|
if err != nil {
|
|
return res, err
|
|
}
|
|
res = writeContentResult{logicalBytes: size, populatedBytes: populated, sparse: true}
|
|
|
|
n := sparsePartCount(populated)
|
|
if n < 2 {
|
|
// Too small to be worth splitting; the plain streaming path handles it.
|
|
return res, errSparseTooSmall
|
|
}
|
|
groups := planSparseParts(exts, populated, n)
|
|
|
|
var idBytes [8]byte
|
|
if _, err := rand.Read(idBytes[:]); err != nil {
|
|
return res, fmt.Errorf("while naming upload parts: %w", err)
|
|
}
|
|
runID := hex.EncodeToString(idBytes[:])
|
|
|
|
bkt := g.client.Bucket(bucket)
|
|
parts := make([]*storage.ObjectHandle, len(groups))
|
|
for i := range groups {
|
|
parts[i] = bkt.Object(fmt.Sprintf("%s.part-%s-%04d", object, runID, i))
|
|
}
|
|
defer func() {
|
|
for _, p := range parts {
|
|
if delErr := p.Delete(context.WithoutCancel(ctx)); delErr != nil &&
|
|
!errors.Is(delErr, storage.ErrObjectNotExist) && err == nil {
|
|
err = fmt.Errorf("while removing upload part %q: %w", p.ObjectName(), delErr)
|
|
}
|
|
}
|
|
}()
|
|
|
|
grp, gctx := errgroup.WithContext(ctx)
|
|
for i, ranges := range groups {
|
|
grp.Go(func() error {
|
|
// Each part uploads over its own connection; see uploadClient.
|
|
obj := g.uploadClient(gctx, i).Bucket(bucket).Object(parts[i].ObjectName())
|
|
w := obj.NewWriter(gctx)
|
|
w.ChunkSize = partWriterChunk
|
|
if err := writeSparsePart(w, f, size, ranges, i == 0, i == len(groups)-1); err != nil {
|
|
_ = w.Close()
|
|
return fmt.Errorf("while writing part %d of %q: %w", i, object, err)
|
|
}
|
|
return w.Close()
|
|
})
|
|
}
|
|
if err := grp.Wait(); err != nil {
|
|
return res, err
|
|
}
|
|
return res, composeAll(ctx, bkt, object, parts, runID)
|
|
}
|
|
|
|
// errSparseTooSmall says the object is not worth splitting, so the caller should fall
|
|
// back to the single-stream path.
|
|
var errSparseTooSmall = errors.New("sparse file below the parallel-part threshold")
|