mirror of
https://github.com/imjasonh/gcsbuf
synced 2026-07-20 13:08:42 +00:00
use preconditions
Signed-off-by: Jason Hall <imjasonh@gmail.com>
This commit is contained in:
parent
5292972f87
commit
1068ab16f9
2 changed files with 44 additions and 7 deletions
24
writer.go
24
writer.go
|
|
@ -46,6 +46,7 @@ type Writer struct {
|
||||||
created bool // whether the GCS object exists yet
|
created bool // whether the GCS object exists yet
|
||||||
closed bool
|
closed bool
|
||||||
err error
|
err error
|
||||||
|
gen int64 // expected generation of the GCS object
|
||||||
|
|
||||||
stopCh chan struct{}
|
stopCh chan struct{}
|
||||||
doneCh chan struct{}
|
doneCh chan struct{}
|
||||||
|
|
@ -137,7 +138,8 @@ func (w *Writer) flushLocked() {
|
||||||
|
|
||||||
if !w.created {
|
if !w.created {
|
||||||
// First write: create the object directly.
|
// 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 {
|
if _, err := gw.Write(data); err != nil {
|
||||||
w.err = fmt.Errorf("writing initial object: %w", err)
|
w.err = fmt.Errorf("writing initial object: %w", err)
|
||||||
return
|
return
|
||||||
|
|
@ -146,6 +148,7 @@ func (w *Writer) flushLocked() {
|
||||||
w.err = fmt.Errorf("closing initial object writer: %w", err)
|
w.err = fmt.Errorf("closing initial object writer: %w", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
w.gen = gw.Attrs().Generation
|
||||||
w.created = true
|
w.created = true
|
||||||
w.buf.Reset()
|
w.buf.Reset()
|
||||||
return
|
return
|
||||||
|
|
@ -155,7 +158,7 @@ func (w *Writer) flushLocked() {
|
||||||
tmpname := fmt.Sprintf("%s-temp-%d", w.object, now().UnixNano())
|
tmpname := fmt.Sprintf("%s-temp-%d", w.object, now().UnixNano())
|
||||||
tmpobj := bh.Object(tmpname)
|
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 {
|
if _, err := gw.Write(data); err != nil {
|
||||||
w.err = fmt.Errorf("writing temp object: %w", err)
|
w.err = fmt.Errorf("writing temp object: %w", err)
|
||||||
return
|
return
|
||||||
|
|
@ -166,7 +169,7 @@ func (w *Writer) flushLocked() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check component count before composing.
|
// 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 {
|
if err != nil {
|
||||||
w.err = fmt.Errorf("getting object attrs: %w", err)
|
w.err = fmt.Errorf("getting object attrs: %w", err)
|
||||||
return
|
return
|
||||||
|
|
@ -180,10 +183,13 @@ func (w *Writer) flushLocked() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compose existing + temp into existing.
|
// 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)
|
w.err = fmt.Errorf("composing objects: %w", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
w.gen = composeAttrs.Generation
|
||||||
|
|
||||||
// Delete temp object.
|
// Delete temp object.
|
||||||
if err := tmpobj.Delete(w.ctx); err != nil {
|
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)
|
tmpobj := bh.Object(tmpname)
|
||||||
|
|
||||||
// Copy to temp (this creates a non-composite object).
|
// 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)
|
return fmt.Errorf("copying to reset temp: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy back.
|
// 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)
|
return fmt.Errorf("copying back from reset temp: %w", err)
|
||||||
}
|
}
|
||||||
|
w.gen = attrs.Generation
|
||||||
|
|
||||||
// Delete the reset temp.
|
// Delete the reset temp.
|
||||||
if err := tmpobj.Delete(w.ctx); err != nil {
|
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.
|
// Set done metadata. Only if the object was created.
|
||||||
if w.created {
|
if w.created {
|
||||||
objh := w.client.Bucket(w.bucket).Object(w.object)
|
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"},
|
Metadata: map[string]string{"done": "true"},
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return fmt.Errorf("setting done metadata: %w", err)
|
return fmt.Errorf("setting done metadata: %w", err)
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
func TestWriterComponentCountReset(t *testing.T) {
|
||||||
client := setup(t)
|
client := setup(t)
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue