@@ -8,20 +8,24 @@ use aws_config::{BehaviorVersion, Region, SdkConfig};
8
8
use aws_credential_types:: Credentials ;
9
9
use aws_sdk_dynamodb:: {
10
10
config:: { ProvideCredentials , SharedCredentialsProvider } ,
11
- operation:: { batch_get_item:: BatchGetItemOutput , batch_write_item:: BatchWriteItemOutput } ,
11
+ operation:: {
12
+ batch_get_item:: BatchGetItemOutput , batch_write_item:: BatchWriteItemOutput ,
13
+ get_item:: GetItemOutput , update_item:: UpdateItemOutput ,
14
+ } ,
12
15
primitives:: Blob ,
13
16
types:: {
14
- AttributeValue , DeleteRequest , Get , KeysAndAttributes , PutRequest , TransactGetItem ,
15
- TransactWriteItem , Update , WriteRequest ,
17
+ AttributeValue , DeleteRequest , KeysAndAttributes , PutRequest , TransactWriteItem , Update ,
18
+ WriteRequest ,
16
19
} ,
17
20
Client ,
18
21
} ;
19
22
use spin_core:: async_trait;
20
23
use spin_factor_key_value:: { log_error, Cas , Error , Store , StoreManager , SwapError } ;
21
24
22
25
pub struct KeyValueAwsDynamo {
26
+ region : String ,
27
+ // Needs to be cloned when getting a store
23
28
table : Arc < String > ,
24
- region : Arc < String > ,
25
29
client : async_once_cell:: Lazy <
26
30
Client ,
27
31
std:: pin:: Pin < Box < dyn std:: future:: Future < Output = Client > + Send > > ,
@@ -99,18 +103,17 @@ impl KeyValueAwsDynamo {
99
103
} ) ;
100
104
101
105
Ok ( Self {
106
+ region,
102
107
table : Arc :: new ( table) ,
103
- region : Arc :: new ( region) ,
104
108
client : async_once_cell:: Lazy :: from_future ( client_fut) ,
105
109
} )
106
110
}
107
111
}
108
112
109
113
#[ async_trait]
110
114
impl StoreManager for KeyValueAwsDynamo {
111
- async fn get ( & self , name : & str ) -> Result < Arc < dyn Store > , Error > {
115
+ async fn get ( & self , _name : & str ) -> Result < Arc < dyn Store > , Error > {
112
116
Ok ( Arc :: new ( AwsDynamoStore {
113
- _name : name. to_owned ( ) ,
114
117
client : self . client . get_unpin ( ) . await . clone ( ) ,
115
118
table : self . table . clone ( ) ,
116
119
} ) )
@@ -122,14 +125,14 @@ impl StoreManager for KeyValueAwsDynamo {
122
125
123
126
fn summary ( & self , _store_name : & str ) -> Option < String > {
124
127
Some ( format ! (
125
- "AWS DynamoDB region: {:? }, table: {}" ,
128
+ "AWS DynamoDB region: {}, table: {}" ,
126
129
self . region, self . table
127
130
) )
128
131
}
129
132
}
130
133
131
134
struct AwsDynamoStore {
132
- _name : String ,
135
+ // Client wraps an Arc so should be low cost to clone
133
136
client : Client ,
134
137
table : Arc < String > ,
135
138
}
@@ -139,20 +142,40 @@ struct CompareAndSwap {
139
142
client : Client ,
140
143
table : Arc < String > ,
141
144
bucket_rep : u32 ,
142
- version : Mutex < Option < String > > ,
145
+ has_lock : Mutex < bool > ,
143
146
}
144
147
145
148
/// Primary key in DynamoDB items used for querying items
146
149
const PK : & str = "PK" ;
147
150
/// Value key in DynamoDB items storing item value as binary
148
151
const VAL : & str = "val" ;
149
- /// Version key in DynamoDB items used for optimistic locking
150
- const VER : & str = "ver " ;
152
+ /// Lock key in DynamoDB items used for atomic operations
153
+ const LOCK : & str = "lock " ;
151
154
152
155
#[ async_trait]
153
156
impl Store for AwsDynamoStore {
154
157
async fn get ( & self , key : & str ) -> Result < Option < Vec < u8 > > , Error > {
155
- let item = self . get_item ( key) . await ?;
158
+ let response = self
159
+ . client
160
+ . get_item ( )
161
+ . table_name ( self . table . as_str ( ) )
162
+ . key (
163
+ PK ,
164
+ aws_sdk_dynamodb:: types:: AttributeValue :: S ( key. to_string ( ) ) ,
165
+ )
166
+ . projection_expression ( VAL )
167
+ . send ( )
168
+ . await
169
+ . map_err ( log_error) ?;
170
+
171
+ let item = response. item . and_then ( |mut item| {
172
+ if let Some ( AttributeValue :: B ( val) ) = item. remove ( VAL ) {
173
+ Some ( val. into_inner ( ) )
174
+ } else {
175
+ None
176
+ }
177
+ } ) ;
178
+
156
179
Ok ( item)
157
180
}
158
181
@@ -182,7 +205,20 @@ impl Store for AwsDynamoStore {
182
205
}
183
206
184
207
async fn exists ( & self , key : & str ) -> Result < bool , Error > {
185
- Ok ( self . get_item ( key) . await ?. is_some ( ) )
208
+ let GetItemOutput { item, .. } = self
209
+ . client
210
+ . get_item ( )
211
+ . table_name ( self . table . as_str ( ) )
212
+ . key (
213
+ PK ,
214
+ aws_sdk_dynamodb:: types:: AttributeValue :: S ( key. to_string ( ) ) ,
215
+ )
216
+ . projection_expression ( PK )
217
+ . send ( )
218
+ . await
219
+ . map_err ( log_error) ?;
220
+
221
+ Ok ( item. map ( |item| item. contains_key ( PK ) ) . unwrap_or ( false ) )
186
222
}
187
223
188
224
async fn get_keys ( & self ) -> Result < Vec < String > , Error > {
@@ -192,7 +228,8 @@ impl Store for AwsDynamoStore {
192
228
async fn get_many ( & self , keys : Vec < String > ) -> Result < Vec < ( String , Option < Vec < u8 > > ) > , Error > {
193
229
let mut results = Vec :: with_capacity ( keys. len ( ) ) ;
194
230
195
- let mut keys_and_attributes_builder = KeysAndAttributes :: builder ( ) ;
231
+ let mut keys_and_attributes_builder =
232
+ KeysAndAttributes :: builder ( ) . projection_expression ( format ! ( "{PK},{VAL}" ) ) ;
196
233
for key in keys {
197
234
keys_and_attributes_builder = keys_and_attributes_builder. keys ( HashMap :: from_iter ( [ (
198
235
PK . to_owned ( ) ,
@@ -344,7 +381,7 @@ impl Store for AwsDynamoStore {
344
381
key : key. to_string ( ) ,
345
382
client : self . client . clone ( ) ,
346
383
table : self . table . clone ( ) ,
347
- version : Mutex :: new ( None ) ,
384
+ has_lock : Mutex :: new ( false ) ,
348
385
bucket_rep,
349
386
} ) )
350
387
}
@@ -353,47 +390,28 @@ impl Store for AwsDynamoStore {
353
390
#[ async_trait]
354
391
impl Cas for CompareAndSwap {
355
392
async fn current ( & self ) -> Result < Option < Vec < u8 > > , Error > {
356
- // TransactGetItems fails if concurrent writes are in progress on an item
357
- let output = self
393
+ let UpdateItemOutput { attributes, .. } = self
358
394
. client
359
- . transact_get_items ( )
360
- . transact_items (
361
- TransactGetItem :: builder ( )
362
- . get (
363
- Get :: builder ( )
364
- . table_name ( self . table . as_str ( ) )
365
- . key (
366
- PK ,
367
- aws_sdk_dynamodb:: types:: AttributeValue :: S ( self . key . clone ( ) ) ,
368
- )
369
- . build ( )
370
- . map_err ( log_error) ?,
371
- )
372
- . build ( ) ,
373
- )
395
+ . update_item ( )
396
+ . table_name ( self . table . as_str ( ) )
397
+ . key ( PK , AttributeValue :: S ( self . key . clone ( ) ) )
398
+ . update_expression ( "SET #lock=:lock" )
399
+ . expression_attribute_names ( "#lock" , LOCK )
400
+ . expression_attribute_values ( ":lock" , AttributeValue :: Null ( true ) )
401
+ . condition_expression ( "attribute_not_exists (#lock)" )
402
+ . return_values ( aws_sdk_dynamodb:: types:: ReturnValue :: AllNew )
374
403
. send ( )
375
404
. await
376
405
. map_err ( log_error) ?;
377
406
378
- let item = output
379
- . responses
380
- . and_then ( |responses| responses. into_iter ( ) . next ( ) )
381
- . and_then ( |response| response. item ) ;
382
-
383
- let Some ( mut current_item) = item else {
384
- return Ok ( None ) ;
385
- } ;
386
-
387
- if let Some ( AttributeValue :: B ( val) ) = current_item. remove ( VAL ) {
388
- let version = match current_item. remove ( VER ) {
389
- Some ( AttributeValue :: N ( ver) ) => Some ( ver) ,
390
- _ => None ,
391
- } ;
407
+ self . has_lock . lock ( ) . unwrap ( ) . clone_from ( & true ) ;
392
408
393
- self . version . lock ( ) . unwrap ( ) . clone_from ( & version) ;
394
- Ok ( Some ( val. into_inner ( ) ) )
395
- } else {
396
- Ok ( None )
409
+ match attributes {
410
+ Some ( mut item) => match item. remove ( VAL ) {
411
+ Some ( AttributeValue :: B ( val) ) => Ok ( Some ( val. into_inner ( ) ) ) ,
412
+ _ => Ok ( None ) ,
413
+ } ,
414
+ None => Ok ( None ) ,
397
415
}
398
416
}
399
417
@@ -403,30 +421,18 @@ impl Cas for CompareAndSwap {
403
421
let mut update_item = Update :: builder ( )
404
422
. table_name ( self . table . as_str ( ) )
405
423
. key ( PK , AttributeValue :: S ( self . key . clone ( ) ) )
424
+ . update_expression ( "SET #val=:val REMOVE #lock" )
406
425
. expression_attribute_names ( "#val" , VAL )
407
426
. expression_attribute_values ( ":val" , AttributeValue :: B ( Blob :: new ( value) ) )
408
- . expression_attribute_names ( "#ver" , VER )
409
- . expression_attribute_values ( ":increment" , AttributeValue :: N ( "1" . to_owned ( ) ) )
410
- . return_values_on_condition_check_failure (
411
- aws_sdk_dynamodb:: types:: ReturnValuesOnConditionCheckFailure :: None ,
412
- ) ;
413
-
414
- let current_version = self . version . lock ( ) . unwrap ( ) . clone ( ) ;
415
- match current_version {
416
- // Existing item with version, update under condition that version in DynamoDB matches cached version
417
- Some ( version) => {
418
- update_item = update_item
419
- . update_expression ( "SET #val=:val ADD #ver :increment" )
420
- . condition_expression ( "#ver = :ver" )
421
- . expression_attribute_values ( ":ver" , AttributeValue :: N ( version) ) ;
422
- }
423
- // New/unversioned item, upsert atomically but without optimistic locking guarantee
424
- None => {
425
- update_item = update_item. update_expression ( "SET #val=:val, #ver=:increment" ) ;
426
- }
427
- } ;
427
+ . expression_attribute_names ( "#lock" , LOCK ) ;
428
+
429
+ let has_lock = * self . has_lock . lock ( ) . unwrap ( ) ;
430
+ // Ensure exclusive access between fetching the current value of the item and swapping
431
+ if has_lock {
432
+ update_item = update_item. condition_expression ( "attribute_exists (#lock)" ) ;
433
+ }
428
434
429
- // TransactWriteItems fails if concurrent writes are in progress on an item.
435
+ // TransactWriteItems fails if concurrent writes are in progress on an item, so even without locking, we get atomicity in overwriting
430
436
self . client
431
437
. transact_write_items ( )
432
438
. transact_items (
@@ -455,30 +461,6 @@ impl Cas for CompareAndSwap {
455
461
}
456
462
457
463
impl AwsDynamoStore {
458
- async fn get_item ( & self , key : & str ) -> Result < Option < Vec < u8 > > , Error > {
459
- let response = self
460
- . client
461
- . get_item ( )
462
- . table_name ( self . table . as_str ( ) )
463
- . key (
464
- PK ,
465
- aws_sdk_dynamodb:: types:: AttributeValue :: S ( key. to_string ( ) ) ,
466
- )
467
- . send ( )
468
- . await
469
- . map_err ( log_error) ?;
470
-
471
- let val = response. item . and_then ( |mut item| {
472
- if let Some ( AttributeValue :: B ( val) ) = item. remove ( VAL ) {
473
- Some ( val. into_inner ( ) )
474
- } else {
475
- None
476
- }
477
- } ) ;
478
-
479
- Ok ( val)
480
- }
481
-
482
464
async fn get_keys ( & self ) -> Result < Vec < String > , Error > {
483
465
let mut primary_keys = Vec :: new ( ) ;
484
466
let mut last_evaluated_key = None ;
0 commit comments