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(), &params)
    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(), &params)
    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(), &params)
   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(), &params)
   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, &params)
   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  }