1
0
Fork 0
mirror of https://github.com/imjasonh/gcsbuf synced 2026-07-09 01:26:07 +00:00

use preconditions

Signed-off-by: Jason Hall <imjasonh@gmail.com>
This commit is contained in:
Jason Hall 2026-03-24 12:55:59 -04:00
parent 5292972f87
commit 1068ab16f9
2 changed files with 44 additions and 7 deletions

View file

@ -46,6 +46,7 @@ type Writer struct {
created bool // whether the GCS object exists yet
closed bool
err error
gen int64 // expected generation of the GCS object
stopCh chan struct{}
doneCh chan struct{}
@ -137,7 +138,8 @@ func (w *Writer) flushLocked() {
if !w.created {
// First write: create the object directly.
gw := objh.NewWriter(w.ctx)
// DoesNotExist ensures we don't overwrite an object created by someone else.
gw := objh.If(storage.Conditions{DoesNotExist: true}).NewWriter(w.ctx)
if _, err := gw.Write(data); err != nil {
w.err = fmt.Errorf("writing initial object: %w", err)
return
@ -146,6 +148,7 @@ func (w *Writer) flushLocked() {
w.err = fmt.Errorf("closing initial object writer: %w", err)
return
}
w.gen = gw.Attrs().Generation
w.created = true
w.buf.Reset()
return
@ -155,7 +158,7 @@ func (w *Writer) flushLocked() {
tmpname := fmt.Sprintf("%s-temp-%d", w.object, now().UnixNano())
tmpobj := bh.Object(tmpname)
gw := tmpobj.NewWriter(w.ctx)
gw := tmpobj.If(storage.Conditions{DoesNotExist: true}).NewWriter(w.ctx)
if _, err := gw.Write(data); err != nil {
w.err = fmt.Errorf("writing temp object: %w", err)
return
@ -166,7 +169,7 @@ func (w *Writer) flushLocked() {
}
// Check component count before composing.
attrs, err := objh.Attrs(w.ctx)
attrs, err := objh.If(storage.Conditions{GenerationMatch: w.gen}).Attrs(w.ctx)
if err != nil {
w.err = fmt.Errorf("getting object attrs: %w", err)
return
@ -180,10 +183,13 @@ func (w *Writer) flushLocked() {
}
// Compose existing + temp into existing.
if _, err := objh.ComposerFrom(objh, tmpobj).Run(w.ctx); err != nil {
composer := objh.If(storage.Conditions{GenerationMatch: w.gen}).ComposerFrom(objh, tmpobj)
composeAttrs, err := composer.Run(w.ctx)
if err != nil {
w.err = fmt.Errorf("composing objects: %w", err)
return
}
w.gen = composeAttrs.Generation
// Delete temp object.
if err := tmpobj.Delete(w.ctx); err != nil {
@ -200,14 +206,18 @@ func (w *Writer) resetComponentCount(bh *storage.BucketHandle, objh *storage.Obj
tmpobj := bh.Object(tmpname)
// Copy to temp (this creates a non-composite object).
if _, err := tmpobj.CopierFrom(objh).Run(w.ctx); err != nil {
// Source precondition ensures we're copying the generation we expect.
if _, err := tmpobj.CopierFrom(objh.If(storage.Conditions{GenerationMatch: w.gen})).Run(w.ctx); err != nil {
return fmt.Errorf("copying to reset temp: %w", err)
}
// Copy back.
if _, err := objh.CopierFrom(tmpobj).Run(w.ctx); err != nil {
// Destination precondition ensures nobody modified the main object while we copied.
attrs, err := objh.If(storage.Conditions{GenerationMatch: w.gen}).CopierFrom(tmpobj).Run(w.ctx)
if err != nil {
return fmt.Errorf("copying back from reset temp: %w", err)
}
w.gen = attrs.Generation
// Delete the reset temp.
if err := tmpobj.Delete(w.ctx); err != nil {
@ -244,7 +254,7 @@ func (w *Writer) Close() error {
// Set done metadata. Only if the object was created.
if w.created {
objh := w.client.Bucket(w.bucket).Object(w.object)
if _, err := objh.Update(w.ctx, storage.ObjectAttrsToUpdate{
if _, err := objh.If(storage.Conditions{GenerationMatch: w.gen}).Update(w.ctx, storage.ObjectAttrsToUpdate{
Metadata: map[string]string{"done": "true"},
}); err != nil {
return fmt.Errorf("setting done metadata: %w", err)

View file

@ -208,6 +208,33 @@ func TestWriterCloseNoData(t *testing.T) {
}
}
func TestWriterExternalWriteDetected(t *testing.T) {
client := setup(t)
ctx := context.Background()
// External process creates the object first.
extw := client.Bucket("test").Object("obj").NewWriter(ctx)
extw.Write([]byte("external"))
if err := extw.Close(); err != nil {
t.Fatalf("external write: %v", err)
}
// Our writer tries to create the same object — DoesNotExist should fail.
w := NewWriter(ctx, WriterOptions{
Client: client,
Bucket: "test",
Object: "obj",
FlushInterval: time.Hour,
})
w.Write([]byte("hello"))
err := w.Flush()
if err == nil {
t.Fatal("expected error from flush when object already exists, got nil")
}
t.Logf("got expected error: %v", err)
}
func TestWriterComponentCountReset(t *testing.T) {
client := setup(t)
ctx := context.Background()