(Implementing New Requirements) A naive update
Updating the design
The Storage Cleaner is going to to query the blob store and get a batch of keys. It will then query the database in one query and find all the images that are missing a database entry. Then it will delete those unused keys using a batch API call.
Storage Cleaner Run
Modeling the design
This is the first example in our modeling tasks in which the model will not match the solution 1-1.
- Relaxing a constraint: While the design calls for batches, for simplicity’s sake we will model it as if the entire blob store keyset can fit into one batch. This hides the complexity of figuring out which items have already been checked and how large of a batch size to use; we can either handle these considerations in implementation or model them separately. In this model, we will need to handle new keys being added after we query for keys, as well as the deletion process failing before all key are deleted. This should also alert us to problems that may be introduced by batching. Note: This design decision is a judgment call that may or may not be correct, but it holds for the current examples.
- Enhancing a constraint: Deleting from the blob store will be modeled as a one by one operation, even though it is submitted in one API call. This is because blob stores don’t provide transactions. A batch delete may happen over the course of time.
The Storage Cleaner state diagram looks like this:
Only the core additions to the spec are shown here. Click Download Code or Download PDF to see the whole thing.
Verifying the design
Let’s start small and see what happens:
Invariant ConsistentReads is violated.
- 1. Initial predicate
- blobStoreState
- i2
- : UNSET
- i1
- : UNSET
- i2
- cleanerStates
- c1
- state: waiting
- blobKeys:
- unusedBlobKeys:
- c1
- databaseState
- ui3
- metadata: UNSET
- imageId: UNSET
- ui2
- metadata: UNSET
- imageId: UNSET
- ui1
- metadata: UNSET
- imageId: UNSET
- ui3
- operations :
- serverStates
- s1
- metadata: UNSET
- imageId: UNSET
- state: waiting
- userId: UNSET
- image: UNSET
- s1
- blobStoreState
- 2. ServerStartWrite
- blobStoreState
- i2
- : UNSET
- i1
- : UNSET
- i2
- cleanerStates
- c1
- state: waiting
- blobKeys:
- unusedBlobKeys:
- c1
- databaseState
- ui3
- metadata: UNSET
- imageId: UNSET
- ui2
- metadata: UNSET
- imageId: UNSET
- ui1
- metadata: UNSET
- imageId: UNSET
- ui3
- operations
-
- metadata : m1
- userId : ui1
- image : u1
- type : WRITE
-
- serverStates
- s1
- metadata: m1
- imageId: UNSET
- state: started_write
- userId: ui1
- image: u1
- s1
- blobStoreState
- 3. ServerWriteBlob Our server wrote an image
- blobStoreState
- i2
- : UNSET
- i1
- : u1
- i2
- cleanerStates
- c1
- state: waiting
- blobKeys:
- unusedBlobKeys:
- c1
- databaseState
- ui3
- metadata: UNSET
- imageId: UNSET
- ui2
- metadata: UNSET
- imageId: UNSET
- ui1
- metadata: UNSET
- imageId: UNSET
- ui3
- operations
-
- metadata : m1
- userId : ui1
- image : u1
- type : WRITE
-
- serverStates
- s1
- metadata: m1
- imageId: i1
- state: wrote_blob
- userId: ui1
- image: u1
- s1
- blobStoreState
- 4. CleanerStartGetBlobKeys Our cleaner starts
- blobStoreState
- i2
- : UNSET
- i1
- : u1
- i2
- cleanerStates
- c1
- state: got_blob_keys
- blobKeys: i1
- unusedBlobKeys:
- c1
- databaseState
- ui3
- metadata: UNSET
- imageId: UNSET
- ui2
- metadata: UNSET
- imageId: UNSET
- ui1
- metadata: UNSET
- imageId: UNSET
- ui3
- operations
-
- metadata : m1
- userId : ui1
- image : u1
- type : WRITE
-
- serverStates
- s1
- metadata: m1
- imageId: i1
- state: wrote_blob
- userId: ui1
- image: u1
- s1
- blobStoreState
- 5. CleanerGetUnusedKeys and notices that the written image isn't in the database
- blobStoreState
- i2
- : UNSET
- i1
- : u1
- i2
- cleanerStates
- c1
- state: got_unused_keys
- blobKeys: i1
- unusedBlobKeys: i1
- c1
- databaseState
- ui3
- metadata: UNSET
- imageId: UNSET
- ui2
- metadata: UNSET
- imageId: UNSET
- ui1
- metadata: UNSET
- imageId: UNSET
- ui3
- operations
-
- metadata : m1
- userId : ui1
- image : u1
- type : WRITE
-
- serverStates
- s1
- metadata: m1
- imageId: i1
- state: wrote_blob
- userId: ui1
- image: u1
- s1
- blobStoreState
- 6. ServerWriteMetadataAndReturn The server completes the write, thinking everything's fine
- blobStoreState
- i2
- : UNSET
- i1
- : u1
- i2
- cleanerStates
- c1
- state: got_unused_keys
- blobKeys: i1
- unusedBlobKeys: i1
- c1
- databaseState
- ui3
- metadata: UNSET
- imageId: UNSET
- ui2
- metadata: UNSET
- imageId: UNSET
- ui1
- metadata: m1
- imageId: i1
- ui3
- operations
-
- metadata : m1
- userId : ui1
- image : u1
- type : WRITE
-
- serverStates
- s1
- metadata: UNSET
- imageId: UNSET
- state: waiting
- userId: UNSET
- image: UNSET
- s1
- blobStoreState
- 7. ServerStartRead A read starts for the same user
- blobStoreState
- i2
- : UNSET
- i1
- : u1
- i2
- cleanerStates
- c1
- state: got_unused_keys
- blobKeys: i1
- unusedBlobKeys: i1
- c1
- databaseState
- ui3
- metadata: UNSET
- imageId: UNSET
- ui2
- metadata: UNSET
- imageId: UNSET
- ui1
- metadata: m1
- imageId: i1
- ui3
- operations
-
- metadata : m1
- userId : ui1
- image : u1
- type : WRITE
-
- serverStates
- s1
- metadata: UNSET
- imageId: UNSET
- state: started_read
- userId: ui1
- image: UNSET
- s1
- blobStoreState
- 8. ServerReadMetadata
- blobStoreState
- i2
- : UNSET
- i1
- : u1
- i2
- cleanerStates
- c1
- state: got_unused_keys
- blobKeys: i1
- unusedBlobKeys: i1
- c1
- databaseState
- ui3
- metadata: UNSET
- imageId: UNSET
- ui2
- metadata: UNSET
- imageId: UNSET
- ui1
- metadata: m1
- imageId: i1
- ui3
- operations
-
- metadata : m1
- userId : ui1
- image : u1
- type : WRITE
-
- serverStates
- s1
- metadata: m1
- imageId: i1
- state: read_metadata
- userId: ui1
- image: UNSET
- s1
- blobStoreState
- 9. CleanerDeletingKeys But oh no, the cleaner deleted the key
- blobStoreState
- i2
- : UNSET
- i1
- : UNSET
- i2
- cleanerStates
- c1
- state: got_unused_keys
- blobKeys: i1
- unusedBlobKeys:
- c1
- databaseState
- ui3
- metadata: UNSET
- imageId: UNSET
- ui2
- metadata: UNSET
- imageId: UNSET
- ui1
- metadata: m1
- imageId: i1
- ui3
- operations
-
- metadata : m1
- userId : ui1
- image : u1
- type : WRITE
-
- serverStates
- s1
- metadata: m1
- imageId: i1
- state: read_metadata
- userId: ui1
- image: UNSET
- s1
- blobStoreState
- 10. ServerReadBlobAndReturn Leading to a corrupted read
- blobStoreState
- i2
- : UNSET
- i1
- : UNSET
- i2
- cleanerStates
- c1
- state: got_unused_keys
- blobKeys: i1
- unusedBlobKeys:
- c1
- databaseState
- ui3
- metadata: UNSET
- imageId: UNSET
- ui2
- metadata: UNSET
- imageId: UNSET
- ui1
- metadata: m1
- imageId: i1
- ui3
- operations
-
- metadata : m1
- userId : ui1
- image : u1
- type : WRITE
-
- metadata : m1
- userId : ui1
- image : UNSET
- type : READ
-
- serverStates
- s1
- metadata: UNSET
- imageId: UNSET
- state: waiting
- userId: UNSET
- image: UNSET
- s1
- blobStoreState
Let’s try it again with two servers and two cleaners to see if we get different behavior.
Same behavior. Invariant ConsistentReads is violated.
- 1. Initial predicate
- blobStoreState
- i2
- : UNSET
- i1
- : UNSET
- i2
- cleanerStates
- c2
- state: waiting
- blobKeys:
- unusedBlobKeys:
- c1
- state: waiting
- blobKeys:
- unusedBlobKeys:
- c2
- databaseState
- ui3
- metadata: UNSET
- imageId: UNSET
- ui2
- metadata: UNSET
- imageId: UNSET
- ui1
- metadata: UNSET
- imageId: UNSET
- ui3
- operations :
- serverStates
- s2
- metadata: UNSET
- imageId: UNSET
- state: waiting
- userId: UNSET
- image: UNSET
- s1
- metadata: UNSET
- imageId: UNSET
- state: waiting
- userId: UNSET
- image: UNSET
- s2
- blobStoreState
- 2. ServerStartWrite
- blobStoreState
- i2
- : UNSET
- i1
- : UNSET
- i2
- cleanerStates
- c2
- state: waiting
- blobKeys:
- unusedBlobKeys:
- c1
- state: waiting
- blobKeys:
- unusedBlobKeys:
- c2
- databaseState
- ui3
- metadata: UNSET
- imageId: UNSET
- ui2
- metadata: UNSET
- imageId: UNSET
- ui1
- metadata: UNSET
- imageId: UNSET
- ui3
- operations
-
- metadata : m1
- userId : ui1
- image : u1
- type : WRITE
-
- serverStates
- s2
- metadata: UNSET
- imageId: UNSET
- state: waiting
- userId: UNSET
- image: UNSET
- s1
- metadata: m1
- imageId: UNSET
- state: started_write
- userId: ui1
- image: u1
- s2
- blobStoreState
- 3. ServerWriteBlob
- blobStoreState
- i2
- : UNSET
- i1
- : u1
- i2
- cleanerStates
- c2
- state: waiting
- blobKeys:
- unusedBlobKeys:
- c1
- state: waiting
- blobKeys:
- unusedBlobKeys:
- c2
- databaseState
- ui3
- metadata: UNSET
- imageId: UNSET
- ui2
- metadata: UNSET
- imageId: UNSET
- ui1
- metadata: UNSET
- imageId: UNSET
- ui3
- operations
-
- metadata : m1
- userId : ui1
- image : u1
- type : WRITE
-
- serverStates
- s2
- metadata: UNSET
- imageId: UNSET
- state: waiting
- userId: UNSET
- image: UNSET
- s1
- metadata: m1
- imageId: i1
- state: wrote_blob
- userId: ui1
- image: u1
- s2
- blobStoreState
- 4. ServerStartRead
- blobStoreState
- i2
- : UNSET
- i1
- : u1
- i2
- cleanerStates
- c2
- state: waiting
- blobKeys:
- unusedBlobKeys:
- c1
- state: waiting
- blobKeys:
- unusedBlobKeys:
- c2
- databaseState
- ui3
- metadata: UNSET
- imageId: UNSET
- ui2
- metadata: UNSET
- imageId: UNSET
- ui1
- metadata: UNSET
- imageId: UNSET
- ui3
- operations
-
- metadata : m1
- userId : ui1
- image : u1
- type : WRITE
-
- serverStates
- s2
- metadata: UNSET
- imageId: UNSET
- state: started_read
- userId: ui1
- image: UNSET
- s1
- metadata: m1
- imageId: i1
- state: wrote_blob
- userId: ui1
- image: u1
- s2
- blobStoreState
- 5. CleanerStartGetBlobKeys
- blobStoreState
- i2
- : UNSET
- i1
- : u1
- i2
- cleanerStates
- c2
- state: waiting
- blobKeys:
- unusedBlobKeys:
- c1
- state: got_blob_keys
- blobKeys: i1
- unusedBlobKeys:
- c2
- databaseState
- ui3
- metadata: UNSET
- imageId: UNSET
- ui2
- metadata: UNSET
- imageId: UNSET
- ui1
- metadata: UNSET
- imageId: UNSET
- ui3
- operations
-
- metadata : m1
- userId : ui1
- image : u1
- type : WRITE
-
- serverStates
- s2
- metadata: UNSET
- imageId: UNSET
- state: started_read
- userId: ui1
- image: UNSET
- s1
- metadata: m1
- imageId: i1
- state: wrote_blob
- userId: ui1
- image: u1
- s2
- blobStoreState
- 6. CleanerGetUnusedKeys
- blobStoreState
- i2
- : UNSET
- i1
- : u1
- i2
- cleanerStates
- c2
- state: waiting
- blobKeys:
- unusedBlobKeys:
- c1
- state: got_unused_keys
- blobKeys: i1
- unusedBlobKeys: i1
- c2
- databaseState
- ui3
- metadata: UNSET
- imageId: UNSET
- ui2
- metadata: UNSET
- imageId: UNSET
- ui1
- metadata: UNSET
- imageId: UNSET
- ui3
- operations
-
- metadata : m1
- userId : ui1
- image : u1
- type : WRITE
-
- serverStates
- s2
- metadata: UNSET
- imageId: UNSET
- state: started_read
- userId: ui1
- image: UNSET
- s1
- metadata: m1
- imageId: i1
- state: wrote_blob
- userId: ui1
- image: u1
- s2
- blobStoreState
- 7. CleanerDeletingKeys
- blobStoreState
- i2
- : UNSET
- i1
- : UNSET
- i2
- cleanerStates
- c2
- state: waiting
- blobKeys:
- unusedBlobKeys:
- c1
- state: got_unused_keys
- blobKeys: i1
- unusedBlobKeys:
- c2
- databaseState
- ui3
- metadata: UNSET
- imageId: UNSET
- ui2
- metadata: UNSET
- imageId: UNSET
- ui1
- metadata: UNSET
- imageId: UNSET
- ui3
- operations
-
- metadata : m1
- userId : ui1
- image : u1
- type : WRITE
-
- serverStates
- s2
- metadata: UNSET
- imageId: UNSET
- state: started_read
- userId: ui1
- image: UNSET
- s1
- metadata: m1
- imageId: i1
- state: wrote_blob
- userId: ui1
- image: u1
- s2
- blobStoreState
- 8. ServerWriteMetadataAndReturn
- blobStoreState
- i2
- : UNSET
- i1
- : UNSET
- i2
- cleanerStates
- c2
- state: waiting
- blobKeys:
- unusedBlobKeys:
- c1
- state: got_unused_keys
- blobKeys: i1
- unusedBlobKeys:
- c2
- databaseState
- ui3
- metadata: UNSET
- imageId: UNSET
- ui2
- metadata: UNSET
- imageId: UNSET
- ui1
- metadata: m1
- imageId: i1
- ui3
- operations
-
- metadata : m1
- userId : ui1
- image : u1
- type : WRITE
-
- serverStates
- s2
- metadata: UNSET
- imageId: UNSET
- state: started_read
- userId: ui1
- image: UNSET
- s1
- metadata: UNSET
- imageId: UNSET
- state: waiting
- userId: UNSET
- image: UNSET
- s2
- blobStoreState
- 9. ServerReadMetadata
- blobStoreState
- i2
- : UNSET
- i1
- : UNSET
- i2
- cleanerStates
- c2
- state: waiting
- blobKeys:
- unusedBlobKeys:
- c1
- state: got_unused_keys
- blobKeys: i1
- unusedBlobKeys:
- c2
- databaseState
- ui3
- metadata: UNSET
- imageId: UNSET
- ui2
- metadata: UNSET
- imageId: UNSET
- ui1
- metadata: m1
- imageId: i1
- ui3
- operations
-
- metadata : m1
- userId : ui1
- image : u1
- type : WRITE
-
- serverStates
- s2
- metadata: m1
- imageId: i1
- state: read_metadata
- userId: ui1
- image: UNSET
- s1
- metadata: UNSET
- imageId: UNSET
- state: waiting
- userId: UNSET
- image: UNSET
- s2
- blobStoreState
- 10. ServerReadBlobAndReturn Same corrupted read
- blobStoreState
- i2
- : UNSET
- i1
- : UNSET
- i2
- cleanerStates
- c2
- state: waiting
- blobKeys:
- unusedBlobKeys:
- c1
- state: got_unused_keys
- blobKeys: i1
- unusedBlobKeys:
- c2
- databaseState
- ui3
- metadata: UNSET
- imageId: UNSET
- ui2
- metadata: UNSET
- imageId: UNSET
- ui1
- metadata: m1
- imageId: i1
- ui3
- operations
-
- metadata : m1
- userId : ui1
- image : u1
- type : WRITE
-
- metadata : m1
- userId : ui1
- image : UNSET
- type : READ
-
- serverStates
- s2
- metadata: UNSET
- imageId: UNSET
- state: waiting
- userId: UNSET
- image: UNSET
- s1
- metadata: UNSET
- imageId: UNSET
- state: waiting
- userId: UNSET
- image: UNSET
- s2
- blobStoreState
Adding more servers and cleaners didn’t change the failure mode. We’ve likely hit upon the essential failure of this design.
Summary
Clearly this solution isn’t going to work as is. It can delete images that were part of records being created at that moment. Normal cleanup systems don’t do that; normally they wait a little while…