github.com/voedger/voedger@v0.0.0-20240520144910-273e84102129/pkg/istorage/amazondb/impl.go (about) 1 /* 2 * Copyright (c) 2024-present unTill Pro, Ltd. 3 * @author Alisher Nurmanov 4 */ 5 6 package amazondb 7 8 import ( 9 "bytes" 10 "context" 11 "errors" 12 "fmt" 13 14 "github.com/aws/aws-sdk-go-v2/aws" 15 "github.com/aws/aws-sdk-go-v2/config" 16 "github.com/aws/aws-sdk-go-v2/credentials" 17 "github.com/aws/aws-sdk-go-v2/service/dynamodb" 18 "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" 19 20 "github.com/voedger/voedger/pkg/istorage" 21 ) 22 23 func (d implIAppStorageFactory) AppStorage(appName istorage.SafeAppName) (storage istorage.IAppStorage, err error) { 24 cfg, err := newAwsCfg(d.params) 25 if err != nil { 26 return nil, err 27 } 28 keySpace := appName.String() 29 session := getClient(cfg) 30 exist, err := doesTableExist(keySpace, session) 31 if err != nil { 32 return nil, err 33 } 34 if !exist { 35 return nil, istorage.ErrStorageDoesNotExist 36 } 37 return newStorage(cfg, appName.String()), nil 38 } 39 40 func (d implIAppStorageFactory) Init(appName istorage.SafeAppName) error { 41 cfg, err := newAwsCfg(d.params) 42 if err != nil { 43 return err 44 } 45 keySpace := appName.String() 46 session := getClient(cfg) 47 if err := newTableExistsWaiter(keySpace, session); err != nil { 48 var awsErr *types.ResourceInUseException 49 if errors.As(err, &awsErr) { 50 return istorage.ErrStorageAlreadyExists 51 } 52 return err 53 } 54 return nil 55 } 56 57 func (s *implIAppStorage) Put(pKey []byte, cCols []byte, value []byte) (err error) { 58 params := dynamodb.PutItemInput{ 59 TableName: aws.String(s.keySpace), 60 Item: map[string]types.AttributeValue{ 61 partitionKeyAttributeName: &types.AttributeValueMemberB{ 62 Value: pKey, 63 }, 64 sortKeyAttributeName: &types.AttributeValueMemberB{ 65 Value: prefixZero(cCols), 66 }, 67 valueAttributeName: &types.AttributeValueMemberB{ 68 Value: value, 69 }, 70 }, 71 } 72 _, err = s.client.PutItem(context.Background(), ¶ms) 73 return err 74 } 75 76 func (s *implIAppStorage) PutBatch(items []istorage.BatchItem) (err error) { 77 writeRequests := make([]types.WriteRequest, len(items)) 78 for i, item := range items { 79 writeRequests[i].PutRequest = &types.PutRequest{ 80 Item: map[string]types.AttributeValue{ 81 partitionKeyAttributeName: &types.AttributeValueMemberB{ 82 Value: item.PKey, 83 }, 84 sortKeyAttributeName: &types.AttributeValueMemberB{ 85 Value: prefixZero(item.CCols), 86 }, 87 valueAttributeName: &types.AttributeValueMemberB{ 88 Value: item.Value, 89 }, 90 }, 91 } 92 } 93 params := dynamodb.BatchWriteItemInput{ 94 RequestItems: map[string][]types.WriteRequest{ 95 s.keySpace: writeRequests, 96 }, 97 } 98 _, err = s.client.BatchWriteItem(context.Background(), ¶ms) 99 return err 100 } 101 102 func (s *implIAppStorage) Get(pKey []byte, cCols []byte, data *[]byte) (ok bool, err error) { 103 // arranging request payload 104 params := dynamodb.GetItemInput{ 105 TableName: aws.String(s.keySpace), 106 Key: map[string]types.AttributeValue{ 107 partitionKeyAttributeName: &types.AttributeValueMemberB{ 108 Value: pKey, 109 }, 110 sortKeyAttributeName: &types.AttributeValueMemberB{ 111 Value: prefixZero(cCols), 112 }, 113 }, 114 ProjectionExpression: aws.String(fmt.Sprintf("%s, #v", sortKeyAttributeName)), 115 ExpressionAttributeNames: map[string]string{"#v": valueAttributeName}, 116 } 117 118 // making request to DynamoDB 119 // GetItem method returns response (pointer to GetItemOutput struct) and error 120 response, err := s.client.GetItem(context.Background(), ¶ms) 121 if err != nil { 122 return false, err 123 } 124 125 // Check if any items were found 126 if response.Item == nil { 127 return false, nil 128 } 129 130 // Extract the value attribute from the response 131 valueAttribute := response.Item[valueAttributeName] 132 *data = (*data)[:0] // Reset the data slice 133 *data = valueAttribute.(*types.AttributeValueMemberB).Value 134 return true, nil 135 } 136 137 func (s *implIAppStorage) GetBatch(pKey []byte, items []istorage.GetBatchItem) error { 138 // Reset data slices for all items 139 for i, item := range items { 140 *item.Data = (*item.Data)[:0] 141 items[i].Ok = false 142 } 143 tableName := s.keySpace 144 145 cColToIndex := make(map[string][]int) 146 keyList := make([]map[string]types.AttributeValue, 0) 147 uniqueCCols := make(map[string]struct{}) 148 for i, item := range items { 149 patchedCCols := prefixZero(item.CCols) 150 strPatchedCCols := string(patchedCCols) 151 cColToIndex[strPatchedCCols] = append(cColToIndex[strPatchedCCols], i) 152 if _, ok := uniqueCCols[strPatchedCCols]; ok { 153 continue 154 } 155 uniqueCCols[strPatchedCCols] = struct{}{} 156 157 keyList = append(keyList, map[string]types.AttributeValue{ 158 partitionKeyAttributeName: &types.AttributeValueMemberB{ 159 Value: pKey, 160 }, 161 sortKeyAttributeName: &types.AttributeValueMemberB{ 162 Value: patchedCCols, 163 }, 164 }) 165 } 166 167 params := dynamodb.BatchGetItemInput{ 168 RequestItems: map[string]types.KeysAndAttributes{ 169 tableName: { 170 Keys: keyList, 171 ProjectionExpression: aws.String(fmt.Sprintf("%s, #v", sortKeyAttributeName)), 172 ExpressionAttributeNames: map[string]string{"#v": valueAttributeName}, 173 }, 174 }, 175 } 176 177 result, err := s.client.BatchGetItem(context.Background(), ¶ms) 178 if err != nil { 179 return err 180 } 181 182 if len(result.Responses) > 0 { 183 for _, item := range result.Responses[tableName] { 184 indexList := cColToIndex[string(item[sortKeyAttributeName].(*types.AttributeValueMemberB).Value)] 185 for _, index := range indexList { 186 items[index].Ok = true 187 *items[index].Data = item[valueAttributeName].(*types.AttributeValueMemberB).Value 188 } 189 } 190 } 191 return nil 192 } 193 194 func (s *implIAppStorage) Read(ctx context.Context, pKey []byte, startCCols, finishCCols []byte, cb istorage.ReadCallback) (err error) { 195 if (len(startCCols) > 0) && (len(finishCCols) > 0) && (bytes.Compare(startCCols, finishCCols) >= 0) { 196 return nil // absurd range 197 } 198 199 keyConditions := map[string]types.Condition{ 200 partitionKeyAttributeName: { 201 ComparisonOperator: types.ComparisonOperatorEq, 202 AttributeValueList: []types.AttributeValue{ 203 &types.AttributeValueMemberB{ 204 Value: pKey, 205 }, 206 }, 207 }, 208 } 209 if len(startCCols) == 0 { 210 if len(finishCCols) != 0 { 211 keyConditions[sortKeyAttributeName] = types.Condition{ 212 ComparisonOperator: types.ComparisonOperatorLe, 213 AttributeValueList: []types.AttributeValue{ 214 &types.AttributeValueMemberB{ 215 Value: prefixZero(finishCCols), 216 }, 217 }, 218 } 219 } 220 } else if len(finishCCols) == 0 { 221 // right-opened range 222 keyConditions[sortKeyAttributeName] = types.Condition{ 223 ComparisonOperator: types.ComparisonOperatorGe, 224 AttributeValueList: []types.AttributeValue{ 225 &types.AttributeValueMemberB{ 226 Value: prefixZero(startCCols), 227 }, 228 }, 229 } 230 } else { 231 // closed range 232 keyConditions[sortKeyAttributeName] = types.Condition{ 233 ComparisonOperator: types.ComparisonOperatorBetween, 234 AttributeValueList: []types.AttributeValue{ 235 &types.AttributeValueMemberB{ 236 Value: prefixZero(startCCols), 237 }, 238 &types.AttributeValueMemberB{ 239 Value: prefixZero(finishCCols), 240 }, 241 }, 242 } 243 } 244 params := dynamodb.QueryInput{ 245 TableName: aws.String(s.keySpace), 246 ProjectionExpression: aws.String(fmt.Sprintf("%s, #v", sortKeyAttributeName)), 247 ExpressionAttributeNames: map[string]string{"#v": valueAttributeName}, 248 KeyConditions: keyConditions, 249 } 250 251 result, err := s.client.Query(ctx, ¶ms) 252 if err != nil { 253 return err 254 } 255 256 if len(result.Items) > 0 { 257 for _, item := range result.Items { 258 if ctx.Err() != nil { 259 return nil // TCK contract 260 } 261 if err := cb(unprefixZero(item[sortKeyAttributeName].(*types.AttributeValueMemberB).Value), item[valueAttributeName].(*types.AttributeValueMemberB).Value); err != nil { 262 return err 263 } 264 } 265 } 266 return nil 267 } 268 269 func getClient(cfg aws.Config) *dynamodb.Client { 270 client := dynamodb.NewFromConfig(cfg) 271 return client 272 } 273 274 func newStorage(cfg aws.Config, keySpace string) (storage istorage.IAppStorage) { 275 client := getClient(cfg) 276 return &implIAppStorage{ 277 client: client, 278 keySpace: dynamoDBTableName(keySpace), 279 } 280 } 281 282 func newAwsCfg(params DynamoDBParams) (aws.Config, error) { 283 return config.LoadDefaultConfig(context.TODO(), 284 config.WithEndpointResolverWithOptions(aws.EndpointResolverWithOptionsFunc(func(service, region string, options ...interface{}) (aws.Endpoint, error) { 285 return aws.Endpoint{URL: params.EndpointURL}, nil 286 })), 287 config.WithRegion(params.Region), 288 config.WithCredentialsProvider( 289 credentials.NewStaticCredentialsProvider( 290 params.AccessKeyID, 291 params.SecretAccessKey, 292 params.SessionToken, 293 ), 294 ), 295 ) 296 } 297 298 func newTableExistsWaiter(name string, client *dynamodb.Client) error { 299 createTableInput := &dynamodb.CreateTableInput{ 300 AttributeDefinitions: []types.AttributeDefinition{ 301 { 302 AttributeName: aws.String(partitionKeyAttributeName), 303 AttributeType: types.ScalarAttributeTypeB, 304 }, 305 { 306 AttributeName: aws.String(sortKeyAttributeName), 307 AttributeType: types.ScalarAttributeTypeB, 308 }, 309 }, 310 KeySchema: []types.KeySchemaElement{ 311 { 312 AttributeName: aws.String(partitionKeyAttributeName), 313 KeyType: types.KeyTypeHash, 314 }, 315 { 316 AttributeName: aws.String(sortKeyAttributeName), 317 KeyType: types.KeyTypeRange, 318 }, 319 }, 320 ProvisionedThroughput: &types.ProvisionedThroughput{ 321 ReadCapacityUnits: aws.Int64(defaultRCU), 322 WriteCapacityUnits: aws.Int64(defaultWCU), 323 }, 324 TableName: aws.String(dynamoDBTableName(name)), 325 } 326 327 if _, err := client.CreateTable(context.TODO(), createTableInput); err != nil { 328 return err 329 } 330 return nil 331 } 332 333 func doesTableExist(name string, client *dynamodb.Client) (bool, error) { 334 describeTableInput := &dynamodb.DescribeTableInput{ 335 TableName: aws.String(dynamoDBTableName(name)), 336 } 337 338 if _, err := client.DescribeTable(context.TODO(), describeTableInput); err != nil { 339 // Check if the error indicates that the table doesn't exist 340 var resourceNotFoundException *types.ResourceNotFoundException 341 if errors.As(err, &resourceNotFoundException) { 342 return false, nil 343 } 344 // Any other error 345 return false, err 346 } 347 // Table exists 348 return true, nil 349 } 350 351 func dynamoDBTableName(name string) string { 352 return fmt.Sprintf("%s.values", name) 353 } 354 355 // prefixZero is a workaround for DynamoDB's limitation on empty byte slices in SortKey 356 // https://aws.amazon.com/ru/about-aws/whats-new/2020/05/amazon-dynamodb-now-supports-empty-values-for-non-key-string-and-binary-attributes-in-dynamodb-tables/ 357 func prefixZero(value []byte) (out []byte) { 358 newArr := make([]byte, 1, len(value)+1) 359 newArr[0] = 0 360 return append(newArr, value...) 361 } 362 363 // unprefixZero is a workaround for DynamoDB's limitation on empty byte slices in SortKey 364 // https://aws.amazon.com/ru/about-aws/whats-new/2020/05/amazon-dynamodb-now-supports-empty-values-for-non-key-string-and-binary-attributes-in-dynamodb-tables/ 365 func unprefixZero(value []byte) (out []byte) { 366 return value[1:] 367 }