Files
seaweedfs/weed/replication/sink/gcssink/gcs_sink.go
T
b2eefb8a73 filer: fail the request, not the process, when chunks cannot be resolved (#11685)
* filer: fail the request, not the process, when chunks cannot be resolved

CompactFileChunks and ViewFromChunks discarded the error from
NonOverlappingVisibleIntervals and iterated the nil interval list it
returns on failure. A CreateEntry or UpdateEntry whose context was
cancelled therefore panicked in SeparateGarbageChunks and took the whole
filer down, losing the metadata log it had not yet flushed.

Both functions now return the error. cleanupChunks fails the request
with it, the mount flush paths return it, the replication sinks and the
S3 SSE range reader return it, and a ChunkStreamReader that cannot build
its view fails every read with it and reports it as its SourceError
instead of reading as an empty file.

Fixes #11682

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* filer: propagate chunk resolution failures in scan paths

NonOverlappingVisibleIntervals can fail without a cancelled context; the
query engine and log-store callers discarded the error and dereferenced
nil intervals.

* query: propagate chunk resolution errors instead of skipping files

When a manifest cannot be resolved, ReadParquetStatistics,
countLiveLogRowsExcludingParquetSources, and computeLiveLogMinMax logged a
warning and skipped the failed file, so queries returned understated counts
and wrong min/max values. Surface the errors instead: the fast-path
aggregation collector now returns a DataSourceError so the engine falls
back to a full scan, and the row-count helpers return the error rather
than a partial total.

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Chris Lu <chrislusf@users.noreply.github.com>
Co-authored-by: Chris Lu <chris.lu@gmail.com>
2026-10-10 23:39:29 +08:00

162 lines
4.4 KiB
Go

package gcssink
import (
"context"
"fmt"
"os"
"strings"
"cloud.google.com/go/storage"
"github.com/seaweedfs/seaweedfs/weed/replication/repl_util"
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
"google.golang.org/api/option"
"github.com/seaweedfs/seaweedfs/weed/filer"
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
"github.com/seaweedfs/seaweedfs/weed/replication/sink"
"github.com/seaweedfs/seaweedfs/weed/replication/source"
"github.com/seaweedfs/seaweedfs/weed/util"
)
type GcsSink struct {
client *storage.Client
bucket string
dir string
filerSource *source.FilerSource
isIncremental bool
}
func init() {
sink.Sinks = append(sink.Sinks, &GcsSink{})
}
func (g *GcsSink) GetName() string {
return "google_cloud_storage"
}
func (g *GcsSink) GetSinkToDirectory() string {
return g.dir
}
func (g *GcsSink) GetDestinationIdentity() string {
return g.bucket + "\x00" + g.dir
}
func (g *GcsSink) IsIncremental() bool {
return g.isIncremental
}
func (g *GcsSink) Initialize(configuration util.Configuration, prefix string) error {
g.isIncremental = configuration.GetBool(prefix + "is_incremental")
return g.initialize(
configuration.GetString(prefix+"google_application_credentials"),
configuration.GetString(prefix+"bucket"),
configuration.GetString(prefix+"directory"),
)
}
func (g *GcsSink) SetSourceFiler(s *source.FilerSource) {
g.filerSource = s
}
func (g *GcsSink) initialize(google_application_credentials, bucketName, dir string) error {
g.bucket = bucketName
g.dir = dir
// Creates a client.
var clientOpts []option.ClientOption
if google_application_credentials != "" {
var data []byte
var err error
if strings.HasPrefix(google_application_credentials, "{") {
data = []byte(google_application_credentials)
} else {
googleCredentialsPath := util.ResolvePath(google_application_credentials)
data, err = os.ReadFile(googleCredentialsPath)
if err != nil {
return fmt.Errorf("failed to read credentials file %s: %v", googleCredentialsPath, err)
}
}
creds, err := google.CredentialsFromJSON(context.Background(), data, storage.ScopeFullControl)
if err != nil {
return fmt.Errorf("failed to parse credentials: %v", err)
}
httpClient := oauth2.NewClient(context.Background(), creds.TokenSource)
clientOpts = append(clientOpts, option.WithHTTPClient(httpClient), option.WithoutAuthentication())
}
client, err := storage.NewClient(context.Background(), clientOpts...)
if err != nil {
return fmt.Errorf("failed to create client with credentials \"%s\" env \"%s\": %v",
google_application_credentials, os.Getenv("GOOGLE_APPLICATION_CREDENTIALS"), err)
}
g.client = client
return nil
}
func (g *GcsSink) DeleteEntry(key string, isDirectory, deleteIncludeChunks bool, signatures []int32) error {
if isDirectory {
key = key + "/"
}
if err := g.client.Bucket(g.bucket).Object(key).Delete(context.Background()); err != nil {
return fmt.Errorf("gcs delete %s/%s: %v", g.bucket, key, err)
}
return nil
}
func (g *GcsSink) CreateEntry(key string, entry *filer_pb.Entry, signatures []int32) error {
if entry.IsDirectory {
return nil
}
totalSize := filer.FileSize(entry)
chunkViews, err := filer.ViewFromChunks(context.Background(), g.filerSource.LookupFileId, entry.GetChunks(), 0, int64(totalSize))
if err != nil {
return fmt.Errorf("resolve chunks of %s: %w", key, err)
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
wc := g.client.Bucket(g.bucket).Object(key).NewWriter(ctx)
writeFunc := func(data []byte) error {
_, writeErr := wc.Write(data)
return writeErr
}
var writeErr error
if len(entry.Content) > 0 {
content, decErr := repl_util.MaybeDecryptContent(entry.Content, entry)
if decErr != nil {
writeErr = fmt.Errorf("decrypt inline SSE content: %w", decErr)
} else {
writeErr = writeFunc(content)
}
} else {
writeErr = repl_util.CopyFromChunkViews(chunkViews, g.filerSource, writeFunc, entry)
}
if writeErr != nil {
// Cancel the context to abort the GCS upload without touching
// any existing object at this key.
cancel()
wc.Close()
return writeErr
}
return wc.Close()
}
func (g *GcsSink) UpdateEntry(key string, oldEntry *filer_pb.Entry, newParentPath string, newEntry *filer_pb.Entry, deleteIncludeChunks bool, signatures []int32) (foundExistingEntry bool, err error) {
return true, g.CreateEntry(key, newEntry, signatures)
}