Skip to content

Commit 676ebb7

Browse files
authored
Add support for storage deletes in multiUpdate and adjust storage index writes (heroiclabs#1183)
1 parent 1eeaa1f commit 676ebb7

File tree

9 files changed

+319
-90
lines changed

9 files changed

+319
-90
lines changed

go.mod

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ require (
1616
github.com/gorilla/mux v1.8.1
1717
github.com/gorilla/websocket v1.5.1
1818
github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.1
19-
github.com/heroiclabs/nakama-common v1.30.2-0.20240207201555-08682d86735e
19+
github.com/heroiclabs/nakama-common v1.30.2-0.20240312140147-3a509c052c10
2020
github.com/jackc/pgconn v1.14.1
2121
github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa
2222
github.com/jackc/pgtype v1.14.0

go.sum

+2-6
Original file line numberDiff line numberDiff line change
@@ -155,12 +155,8 @@ github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZH
155155
github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.1 h1:6UKoz5ujsI55KNpsJH3UwCq3T8kKbZwNZBNPuTTje8U=
156156
github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.1/go.mod h1:YvJ2f6MplWDhfxiUC3KpyTy76kYUZA4W3pTv/wdKQ9Y=
157157
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
158-
github.com/heroiclabs/nakama-common v1.30.1 h1:2lnP71Rgix/WDbvq4hQ2EoecEiItJtMHcRLdZXpUpeo=
159-
github.com/heroiclabs/nakama-common v1.30.1/go.mod h1:Os8XeXGvHAap/p6M/8fQ3gle4eEXDGRQmoRNcPQTjXs=
160-
github.com/heroiclabs/nakama-common v1.30.2-0.20240207195416-15dafabfc934 h1:H33kk2lhsdsSIX7qzKsWgGdl2xDOdSZqhNeo22QQ6XU=
161-
github.com/heroiclabs/nakama-common v1.30.2-0.20240207195416-15dafabfc934/go.mod h1:Os8XeXGvHAap/p6M/8fQ3gle4eEXDGRQmoRNcPQTjXs=
162-
github.com/heroiclabs/nakama-common v1.30.2-0.20240207201555-08682d86735e h1:z+YFZcDoaVl9ytUTEI3h5XoakXCqRFRfVXfBsI/bX2U=
163-
github.com/heroiclabs/nakama-common v1.30.2-0.20240207201555-08682d86735e/go.mod h1:Os8XeXGvHAap/p6M/8fQ3gle4eEXDGRQmoRNcPQTjXs=
158+
github.com/heroiclabs/nakama-common v1.30.2-0.20240312140147-3a509c052c10 h1:SJAkxoFzg1e+WqtedeggzL3MJC0/vHgJ9iI0GVUawXg=
159+
github.com/heroiclabs/nakama-common v1.30.2-0.20240312140147-3a509c052c10/go.mod h1:Os8XeXGvHAap/p6M/8fQ3gle4eEXDGRQmoRNcPQTjXs=
164160
github.com/ianlancetaylor/demangle v0.0.0-20220319035150-800ac71e25c2/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w=
165161
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
166162
github.com/influxdata/influxdb v1.7.6/go.mod h1:qZna6X/4elxqT3yI9iZYdZrWWdeFOOprn86kgg4+IzY=

server/core_multi.go

+14-3
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,13 @@ import (
2424
"go.uber.org/zap"
2525
)
2626

27-
func MultiUpdate(ctx context.Context, logger *zap.Logger, db *sql.DB, metrics Metrics, accountUpdates []*accountUpdate, storageWrites StorageOpWrites, walletUpdates []*walletUpdate, updateLedger bool) ([]*api.StorageObjectAck, []*runtime.WalletUpdateResult, error) {
28-
if len(accountUpdates) == 0 && len(storageWrites) == 0 && len(walletUpdates) == 0 {
27+
func MultiUpdate(ctx context.Context, logger *zap.Logger, db *sql.DB, metrics Metrics, accountUpdates []*accountUpdate, storageWrites StorageOpWrites, storageDeletes StorageOpDeletes, storageIndex StorageIndex, walletUpdates []*walletUpdate, updateLedger bool) ([]*api.StorageObjectAck, []*runtime.WalletUpdateResult, error) {
28+
if len(accountUpdates) == 0 && len(storageWrites) == 0 && len(storageDeletes) == 0 && len(walletUpdates) == 0 {
2929
return nil, nil, nil
3030
}
3131

3232
var storageWriteAcks []*api.StorageObjectAck
33+
var storageWriteOps StorageOpWrites
3334
var walletUpdateResults []*runtime.WalletUpdateResult
3435

3536
if err := ExecuteInTxPgx(ctx, db, func(tx pgx.Tx) error {
@@ -43,11 +44,17 @@ func MultiUpdate(ctx context.Context, logger *zap.Logger, db *sql.DB, metrics Me
4344
}
4445

4546
// Execute any storage updates.
46-
storageWriteAcks, updateErr = storageWriteObjects(ctx, logger, metrics, tx, true, storageWrites)
47+
storageWriteOps, storageWriteAcks, updateErr = storageWriteObjects(ctx, logger, metrics, tx, true, storageWrites)
4748
if updateErr != nil {
4849
return updateErr
4950
}
5051

52+
// Execute any storage deletes.
53+
deleteErr := storageDeleteObjects(ctx, logger, tx, true, storageDeletes)
54+
if deleteErr != nil {
55+
return deleteErr
56+
}
57+
5158
// Execute any wallet updates.
5259
walletUpdateResults, updateErr = updateWallets(ctx, logger, tx, walletUpdates, updateLedger)
5360
if updateErr != nil {
@@ -63,5 +70,9 @@ func MultiUpdate(ctx context.Context, logger *zap.Logger, db *sql.DB, metrics Me
6370
return nil, walletUpdateResults, err
6471
}
6572

73+
// Update storage index.
74+
storageIndexWrite(ctx, storageIndex, storageWriteOps, storageWriteAcks)
75+
storageIndex.Delete(ctx, storageDeletes)
76+
6677
return storageWriteAcks, walletUpdateResults, nil
6778
}

server/core_storage.go

+74-60
Original file line numberDiff line numberDiff line change
@@ -577,11 +577,12 @@ SELECT collection, key, user_id, value, version, read, write, create_time, updat
577577

578578
func StorageWriteObjects(ctx context.Context, logger *zap.Logger, db *sql.DB, metrics Metrics, storageIndex StorageIndex, authoritativeWrite bool, ops StorageOpWrites) (*api.StorageObjectAcks, codes.Code, error) {
579579
var acks []*api.StorageObjectAck
580+
var sortedWrites StorageOpWrites
580581

581582
if err := ExecuteInTxPgx(ctx, db, func(tx pgx.Tx) error {
582583
// If the transaction is retried ensure we wipe any acks that may have been prepared by previous attempts.
583584
var writeErr error
584-
acks, writeErr = storageWriteObjects(ctx, logger, metrics, tx, authoritativeWrite, ops)
585+
sortedWrites, acks, writeErr = storageWriteObjects(ctx, logger, metrics, tx, authoritativeWrite, ops)
585586
if writeErr != nil {
586587
if writeErr == runtime.ErrStorageRejectedVersion || writeErr == runtime.ErrStorageRejectedPermission {
587588
logger.Debug("Error writing storage objects.", zap.Error(writeErr))
@@ -601,27 +602,12 @@ func StorageWriteObjects(ctx context.Context, logger *zap.Logger, db *sql.DB, me
601602
return nil, codes.Internal, err
602603
}
603604

604-
sw := make([]*api.StorageObject, 0, len(ops))
605-
for i, o := range ops {
606-
sw = append(sw, &api.StorageObject{
607-
Collection: o.Object.Collection,
608-
Key: o.Object.Key,
609-
UserId: o.OwnerID,
610-
Value: o.Object.Value,
611-
Version: acks[i].Version,
612-
PermissionRead: o.Object.PermissionRead.GetValue(),
613-
PermissionWrite: o.Object.PermissionRead.GetValue(),
614-
CreateTime: acks[i].CreateTime,
615-
UpdateTime: acks[i].UpdateTime,
616-
})
617-
}
618-
619-
storageIndex.Write(ctx, sw)
605+
storageIndexWrite(ctx, storageIndex, sortedWrites, acks)
620606

621607
return &api.StorageObjectAcks{Acks: acks}, codes.OK, nil
622608
}
623609

624-
func storageWriteObjects(ctx context.Context, logger *zap.Logger, metrics Metrics, tx pgx.Tx, authoritativeWrite bool, ops StorageOpWrites) ([]*api.StorageObjectAck, error) {
610+
func storageWriteObjects(ctx context.Context, logger *zap.Logger, metrics Metrics, tx pgx.Tx, authoritativeWrite bool, ops StorageOpWrites) (StorageOpWrites, []*api.StorageObjectAck, error) {
625611
// Ensure writes are processed in a consistent order to avoid deadlocks from concurrent operations.
626612
// Sorting done on a copy to ensure we don't modify the input, which may be re-used on transaction retries.
627613
sortedOps := make(StorageOpWrites, 0, len(ops))
@@ -654,16 +640,16 @@ func storageWriteObjects(ctx context.Context, logger *zap.Logger, metrics Metric
654640
if err != nil && errors.As(err, &pgErr) {
655641
if pgErr.Code == dbErrorUniqueViolation {
656642
metrics.StorageWriteRejectCount(map[string]string{"collection": object.Collection, "reason": "version"}, 1)
657-
return nil, runtime.ErrStorageRejectedVersion
643+
return nil, nil, runtime.ErrStorageRejectedVersion
658644
}
659-
return nil, err
645+
return nil, nil, err
660646
} else if err == pgx.ErrNoRows {
661647
// Not every case from storagePrepWriteObject can return NoRows, but those
662648
// which do are always ErrStorageRejectedVersion
663649
metrics.StorageWriteRejectCount(map[string]string{"collection": object.Collection, "reason": "version"}, 1)
664-
return nil, runtime.ErrStorageRejectedVersion
650+
return nil, nil, runtime.ErrStorageRejectedVersion
665651
} else if err != nil {
666-
return nil, err
652+
return nil, nil, err
667653
}
668654

669655
if !isUpsert {
@@ -672,11 +658,11 @@ func storageWriteObjects(ctx context.Context, logger *zap.Logger, metrics Metric
672658
if !authoritativeWrite && resultWrite != 1 {
673659
// - permission: non-authoritative write & original row write != 1
674660
metrics.StorageWriteRejectCount(map[string]string{"collection": object.Collection, "reason": "permission"}, 1)
675-
return nil, runtime.ErrStorageRejectedPermission
661+
return nil, nil, runtime.ErrStorageRejectedPermission
676662
} else if object.Version != "" {
677663
// - version mismatch
678664
metrics.StorageWriteRejectCount(map[string]string{"collection": object.Collection, "reason": "version"}, 1)
679-
return nil, runtime.ErrStorageRejectedVersion
665+
return nil, nil, runtime.ErrStorageRejectedVersion
680666
}
681667
}
682668

@@ -691,7 +677,7 @@ func storageWriteObjects(ctx context.Context, logger *zap.Logger, metrics Metric
691677
acks[indexedOps[op]] = ack
692678
}
693679

694-
return acks, nil
680+
return sortedOps, acks, nil
695681
}
696682

697683
func storagePrepBatch(batch *pgx.Batch, authoritativeWrite bool, op *StorageOpWrite) {
@@ -781,41 +767,10 @@ func storagePrepBatch(batch *pgx.Batch, authoritativeWrite bool, op *StorageOpWr
781767
}
782768

783769
func StorageDeleteObjects(ctx context.Context, logger *zap.Logger, db *sql.DB, storageIndex StorageIndex, authoritativeDelete bool, ops StorageOpDeletes) (codes.Code, error) {
784-
// Ensure deletes are processed in a consistent order.
785-
sort.Sort(ops)
786-
787-
if err := ExecuteInTx(ctx, db, func(tx *sql.Tx) error {
788-
for _, op := range ops {
789-
params := []interface{}{op.ObjectID.Collection, op.ObjectID.Key, op.OwnerID}
790-
var query string
791-
if authoritativeDelete {
792-
// Deleting from the runtime.
793-
query = "DELETE FROM storage WHERE collection = $1 AND key = $2 AND user_id = $3"
794-
} else {
795-
// Direct client request to delete.
796-
query = "DELETE FROM storage WHERE collection = $1 AND key = $2 AND user_id = $3 AND write > 0"
797-
}
798-
if op.ObjectID.GetVersion() != "" {
799-
// Conditional delete.
800-
params = append(params, op.ObjectID.Version)
801-
query += " AND version = $4"
802-
}
803-
804-
result, err := tx.ExecContext(ctx, query, params...)
805-
if err != nil {
806-
logger.Debug("Could not delete storage object.", zap.Error(err), zap.String("query", query), zap.Any("object_id", op.ObjectID))
807-
return err
808-
}
809-
810-
if authoritativeDelete && op.ObjectID.GetVersion() == "" {
811-
// If it's an authoritative delete and there is no OCC, the only reason rows affected would be 0 is having
812-
// nothing to delete. In that case it's safe to assume the deletion was just a no-op and there's no need
813-
// to check anything further. Should apply something similar to non-authoritative deletes too.
814-
continue
815-
}
816-
if rowsAffected, _ := result.RowsAffected(); rowsAffected == 0 {
817-
return StatusError(codes.InvalidArgument, "Storage delete rejected.", errors.New("Storage delete rejected - not found, version check failed, or permission denied."))
818-
}
770+
if err := ExecuteInTxPgx(ctx, db, func(tx pgx.Tx) error {
771+
deleteErr := storageDeleteObjects(ctx, logger, tx, authoritativeDelete, ops)
772+
if deleteErr != nil {
773+
return deleteErr
819774
}
820775
return nil
821776
}); err != nil {
@@ -830,3 +785,62 @@ func StorageDeleteObjects(ctx context.Context, logger *zap.Logger, db *sql.DB, s
830785

831786
return codes.OK, nil
832787
}
788+
789+
func storageDeleteObjects(ctx context.Context, logger *zap.Logger, tx pgx.Tx, authoritativeDelete bool, ops StorageOpDeletes) error {
790+
// Ensure deletes are processed in a consistent order.
791+
sort.Sort(ops)
792+
793+
for _, op := range ops {
794+
params := []interface{}{op.ObjectID.Collection, op.ObjectID.Key, op.OwnerID}
795+
var query string
796+
if authoritativeDelete {
797+
// Deleting from the runtime.
798+
query = "DELETE FROM storage WHERE collection = $1 AND key = $2 AND user_id = $3"
799+
} else {
800+
// Direct client request to delete.
801+
query = "DELETE FROM storage WHERE collection = $1 AND key = $2 AND user_id = $3 AND write > 0"
802+
}
803+
if op.ObjectID.GetVersion() != "" {
804+
// Conditional delete.
805+
params = append(params, op.ObjectID.Version)
806+
query += " AND version = $4"
807+
}
808+
809+
result, err := tx.Exec(ctx, query, params...)
810+
if err != nil {
811+
logger.Debug("Could not delete storage object.", zap.Error(err), zap.String("query", query), zap.Any("object_id", op.ObjectID))
812+
return err
813+
}
814+
815+
if authoritativeDelete && op.ObjectID.GetVersion() == "" {
816+
// If it's an authoritative delete and there is no OCC, the only reason rows affected would be 0 is having
817+
// nothing to delete. In that case it's safe to assume the deletion was just a no-op and there's no need
818+
// to check anything further. Should apply something similar to non-authoritative deletes too.
819+
continue
820+
}
821+
if rowsAffected := result.RowsAffected(); rowsAffected == 0 {
822+
return StatusError(codes.InvalidArgument, "Storage delete rejected.", errors.New("Storage delete rejected - not found, version check failed, or permission denied."))
823+
}
824+
}
825+
826+
return nil
827+
}
828+
829+
func storageIndexWrite(ctx context.Context, storageIndex StorageIndex, ops StorageOpWrites, acks []*api.StorageObjectAck) {
830+
sw := make([]*api.StorageObject, 0, len(ops))
831+
for i, o := range ops {
832+
sw = append(sw, &api.StorageObject{
833+
Collection: o.Object.Collection,
834+
Key: o.Object.Key,
835+
UserId: o.OwnerID,
836+
Value: o.Object.Value,
837+
Version: acks[i].Version,
838+
PermissionRead: o.Object.PermissionRead.GetValue(),
839+
PermissionWrite: o.Object.PermissionRead.GetValue(),
840+
CreateTime: acks[i].CreateTime,
841+
UpdateTime: acks[i].UpdateTime,
842+
})
843+
}
844+
845+
storageIndex.Write(ctx, sw)
846+
}

server/runtime_go_nakama.go

+34-2
Original file line numberDiff line numberDiff line change
@@ -2088,12 +2088,13 @@ func (n *RuntimeGoNakamaModule) StorageIndexList(ctx context.Context, callerID,
20882088
// @param ctx(type=context.Context) The context object represents information about the server and requester.
20892089
// @param accountUpdates(type=[]*runtime.AccountUpdate) Array of account information to be updated.
20902090
// @param storageWrites(type=[]*runtime.StorageWrite) Array of storage objects to be updated.
2091+
// @param storageDeletes(type=[]*runtime.StorageDelete) Array of storage objects to be deleted.
20912092
// @param walletUpdates(type=[]*runtime.WalletUpdate) Array of wallet updates to be made.
20922093
// @param updateLedger(type=bool, optional=true, default=false) Whether to record this wallet update in the ledger.
20932094
// @return storageWriteOps([]*api.StorageObjectAck) A list of acks with the version of the written objects.
20942095
// @return walletUpdateOps([]*runtime.WalletUpdateResult) A list of wallet updates results.
20952096
// @return error(error) An optional error value if an error occurred.
2096-
func (n *RuntimeGoNakamaModule) MultiUpdate(ctx context.Context, accountUpdates []*runtime.AccountUpdate, storageWrites []*runtime.StorageWrite, walletUpdates []*runtime.WalletUpdate, updateLedger bool) ([]*api.StorageObjectAck, []*runtime.WalletUpdateResult, error) {
2097+
func (n *RuntimeGoNakamaModule) MultiUpdate(ctx context.Context, accountUpdates []*runtime.AccountUpdate, storageWrites []*runtime.StorageWrite, storageDeletes []*runtime.StorageDelete, walletUpdates []*runtime.WalletUpdate, updateLedger bool) ([]*api.StorageObjectAck, []*runtime.WalletUpdateResult, error) {
20972098
// Process account update inputs.
20982099
accountUpdateOps := make([]*accountUpdate, 0, len(accountUpdates))
20992100
for _, update := range accountUpdates {
@@ -2181,6 +2182,37 @@ func (n *RuntimeGoNakamaModule) MultiUpdate(ctx context.Context, accountUpdates
21812182
storageWriteOps = append(storageWriteOps, op)
21822183
}
21832184

2185+
// Process storage delete inputs.
2186+
storageDeleteOps := make(StorageOpDeletes, 0, len(storageDeletes))
2187+
for _, del := range storageDeletes {
2188+
if del.Collection == "" {
2189+
return nil, nil, errors.New("expects collection to be a non-empty string")
2190+
}
2191+
if del.Key == "" {
2192+
return nil, nil, errors.New("expects key to be a non-empty string")
2193+
}
2194+
if del.UserID != "" {
2195+
if _, err := uuid.FromString(del.UserID); err != nil {
2196+
return nil, nil, errors.New("expects an empty or valid user id")
2197+
}
2198+
}
2199+
2200+
op := &StorageOpDelete{
2201+
ObjectID: &api.DeleteStorageObjectId{
2202+
Collection: del.Collection,
2203+
Key: del.Key,
2204+
Version: del.Version,
2205+
},
2206+
}
2207+
if del.UserID == "" {
2208+
op.OwnerID = uuid.Nil.String()
2209+
} else {
2210+
op.OwnerID = del.UserID
2211+
}
2212+
2213+
storageDeleteOps = append(storageDeleteOps, op)
2214+
}
2215+
21842216
// Process wallet update inputs.
21852217
walletUpdateOps := make([]*walletUpdate, len(walletUpdates))
21862218
for i, update := range walletUpdates {
@@ -2204,7 +2236,7 @@ func (n *RuntimeGoNakamaModule) MultiUpdate(ctx context.Context, accountUpdates
22042236
}
22052237
}
22062238

2207-
return MultiUpdate(ctx, n.logger, n.db, n.metrics, accountUpdateOps, storageWriteOps, walletUpdateOps, updateLedger)
2239+
return MultiUpdate(ctx, n.logger, n.db, n.metrics, accountUpdateOps, storageWriteOps, storageDeleteOps, n.storageIndex, walletUpdateOps, updateLedger)
22082240
}
22092241

22102242
// @group leaderboards

0 commit comments

Comments
 (0)