Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add a state machine to Blob #101

Merged
merged 1 commit into from
Aug 21, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 67 additions & 7 deletions internal/pkg/metadb/blob.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@
package metadb

import (
"errors"
"time"

"cloud.google.com/go/datastore"
"github.com/google/uuid"
)

// BlobStatus represents the current blob status.
Expand All @@ -27,12 +31,12 @@ import (
// / fail \ success
// v v
// [BlobStatusError] [BlobStatusReady]
// | |
// Upload new blob | | Record deleted or new blob uploaded
// or | v
// delete the record | [BlobStatusPendingDeletion]
// | x |
// Upload new blob | \ fail | Record deleted or new blob uploaded
// or | \ v
// delete the record | -----[BlobStatusPendingDeletion]
// v /
// [Delete the blob entity] <-------------/ New blob is ready
// [Delete the blob entity] <-------------/ Garbage collection
//
type BlobStatus int16

Expand All @@ -56,7 +60,7 @@ const (
// Blob is a metadata document to keep track of blobs stored in an external blob store.
type Blob struct {
// Key is the primary key for the blob entry
Key string `datastore:"-"`
Key uuid.UUID `datastore:"-"`
// Size is the byte size of the blob
Size int64
// ObjectName represents the object name stored in the blob store.
Expand Down Expand Up @@ -92,6 +96,62 @@ func (b *Blob) Load(ps []datastore.Property) error {

// LoadKey implements the KeyLoader interface and sets the value to the Key field.
func (b *Blob) LoadKey(k *datastore.Key) error {
b.Key = k.Name
key, err := uuid.Parse(k.Name)
b.Key = key
return err
}

// Initialize sets up the Blob as follows:
// - Set a new UUID to Key
// - Initialize Size and ObjectName as specified
// - Set Status to BlobStatusInitializing
// - Set current time to Timestamps (both created and updated at)
//
// Initialize should be called once on a zero-initialized (empty) Blob whose
// Status is set to BlobStatusUnknown, otherwise it returns an error.
func (b *Blob) Initialize(size int64, objectName string) error {
if b.Status != BlobStatusUnknown {
return errors.New("cannot re-initialize a blob entry")
}
b.Key = uuid.New()
b.Size = size
b.ObjectName = objectName
b.Status = BlobStatusInitializing
// Nanosecond is fine as it will not be returned to clients.
b.Timestamps.NewTimestamps(time.Nanosecond)
return nil
}

// Ready changes Status to BlobStatusReady and updates Timestamps.
// It returns an error if the current Status is not BlobStatusInitializing.
func (b *Blob) Ready() error {
if b.Status != BlobStatusInitializing {
return errors.New("Ready was called when Status is not Initializing")
}
b.Status = BlobStatusReady
b.Timestamps.UpdateTimestamps(time.Nanosecond)
return nil
}

// Retire marks the Blob as BlobStatusPendingDeletion and updates Timestamps.
// Returns an error if the current Status is not BlobStatusReady.
func (b *Blob) Retire() error {
if b.Status != BlobStatusReady {
return errors.New("Retire was called when Status is not either Initializing or Ready")
}
b.Status = BlobStatusPendingDeletion
b.Timestamps.UpdateTimestamps(time.Nanosecond)
return nil
}

// Fail marks the Blob as BlobStatusError and updates Timestamps.
// Returns an error if the current Status is not either BlobStatusInitializing
// or BlobStatusPendingDeletion.
func (b *Blob) Fail() error {
if b.Status != BlobStatusInitializing && b.Status != BlobStatusPendingDeletion {
return errors.New("Fail was called when Status is not either Error or Initializing or PendingDeletion")
}
b.Status = BlobStatusError
b.Timestamps.UpdateTimestamps(time.Nanosecond)
return nil
}
70 changes: 66 additions & 4 deletions internal/pkg/metadb/blob_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,14 @@ import (

func TestBlob_LoadKey(t *testing.T) {
blob := new(metadb.Blob)
key := datastore.NameKey("kind", "testkey", nil)
assert.NoError(t, blob.LoadKey(key))
assert.Equal(t, "testkey", blob.Key)
key := uuid.MustParse("d13c289c-8845-485f-b582-c87342d5dade")
assert.NoError(t, blob.LoadKey(datastore.NameKey("kind", key.String(), nil)))
assert.Equal(t, key, blob.Key)
}

func TestBlob_Save(t *testing.T) {
blob := metadb.Blob{
Key: uuid.New().String(),
Key: uuid.New(),
Size: 123,
ObjectName: "object name",
Status: metadb.BlobStatusInitializing,
Expand Down Expand Up @@ -86,3 +86,65 @@ func TestBlob_Load(t *testing.T) {
assert.Equal(t, expected, actual)
}
}

func newInitBlob(t *testing.T) *metadb.Blob {
blob := new(metadb.Blob)
const (
size = int64(4)
name = "abc"
)

// Initialize
assert.NoError(t, blob.Initialize(size, name))
assert.NotEqual(t, uuid.Nil, blob.Key)
assert.Equal(t, size, blob.Size)
assert.Equal(t, name, blob.ObjectName)
assert.Equal(t, metadb.BlobStatusInitializing, blob.Status)
assert.NotEmpty(t, blob.Timestamps.CreatedAt)
assert.NotEmpty(t, blob.Timestamps.UpdatedAt)
assert.NotEmpty(t, blob.Timestamps.Signature)
return blob
}

func TestBlob_LifeCycle(t *testing.T) {
blob := newInitBlob(t)

// Invalid transitions
assert.Error(t, blob.Initialize(0, ""))
assert.Error(t, blob.Retire())

// Ready
assert.NoError(t, blob.Ready())
assert.Equal(t, metadb.BlobStatusReady, blob.Status)

// Invalid transitions
assert.Error(t, blob.Initialize(0, ""))
assert.Error(t, blob.Ready())

// Retire
assert.NoError(t, blob.Retire())
assert.Equal(t, metadb.BlobStatusPendingDeletion, blob.Status)

// Invalid transitions
assert.Error(t, blob.Retire())
assert.Error(t, blob.Ready())
assert.Error(t, blob.Initialize(0, ""))
}

func TestBlob_Fail(t *testing.T) {
blob := new(metadb.Blob)

// Fail should fail for BlobStatusUnknown
assert.Error(t, blob.Fail())

blob = newInitBlob(t)
assert.NoError(t, blob.Fail())
blob.Status = metadb.BlobStatusPendingDeletion
assert.NoError(t, blob.Fail())

blob.Status = metadb.BlobStatusReady
assert.Error(t, blob.Fail())

blob.Status = metadb.BlobStatusError
assert.Error(t, blob.Fail())
}