Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Using DynamoDB as an option for persistence #101

Merged
merged 82 commits into from
Nov 15, 2021
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
82 commits
Select commit Hold shift + click to select a range
e707779
copy paste
joshprzybyszewski Aug 30, 2021
4ad0d9c
poke at a suggested schema design
joshprzybyszewski Aug 30, 2021
474c324
try to start describing the goal
joshprzybyszewski Aug 30, 2021
e0e27de
start making the player service look like what it will look like
joshprzybyszewski Aug 31, 2021
5c32975
get the table created and the qa program running
joshprzybyszewski Sep 3, 2021
a4b8d67
describe it a little better...
joshprzybyszewski Sep 3, 2021
4e2b748
get most of the way there
joshprzybyszewski Sep 3, 2021
239d336
ok i think the player service is working enough
joshprzybyszewski Sep 3, 2021
da79800
upgrade to v2 before it's too much more painful
joshprzybyszewski Sep 3, 2021
4bd7bc4
ok try to use this nonsense
joshprzybyszewski Sep 3, 2021
bfeb207
Merge branch 'master' of github.com:joshprzybyszewski/cribbage into m…
joshprzybyszewski Oct 1, 2021
de5004c
go.sum is generated
joshprzybyszewski Oct 1, 2021
424d2aa
try continuing to get saving a game to work
joshprzybyszewski Oct 1, 2021
044263a
make it work?
joshprzybyszewski Oct 1, 2021
0ef12a1
resolve some TODOs
joshprzybyszewski Oct 1, 2021
abb733f
move around all the config stuff
joshprzybyszewski Oct 1, 2021
214807a
get _most_ of it working
joshprzybyszewski Oct 2, 2021
03e1e19
now we can create gamesgs!
joshprzybyszewski Oct 2, 2021
167baa3
add some unmarshalling json help
joshprzybyszewski Oct 2, 2021
bd84b8f
IT'S WORKING IT'S WORKING
joshprzybyszewski Oct 2, 2021
6d9ee77
tidy the docker-compose
joshprzybyszewski Oct 2, 2021
f83a746
go back to master
joshprzybyszewski Oct 2, 2021
e16fde9
consolidate a bunch of logic
joshprzybyszewski Oct 2, 2021
3b1228e
clean up and consolidate
joshprzybyszewski Oct 2, 2021
89b2bea
check out the magic of the aws config
joshprzybyszewski Oct 2, 2021
f832c00
not local
joshprzybyszewski Oct 2, 2021
3914ce9
Merge branch 'master' of github.com:joshprzybyszewski/cribbage into m…
joshprzybyszewski Oct 2, 2021
bb96ee1
try to get the unit tests working in ci?
joshprzybyszewski Oct 2, 2021
854dd51
add unit tests
joshprzybyszewski Oct 2, 2021
66cc0fd
connect to 127.0.0.1 instead of localhost
joshprzybyszewski Oct 2, 2021
d84d022
address golints
joshprzybyszewski Oct 2, 2021
425daf7
move this over
joshprzybyszewski Oct 2, 2021
296f1d1
poke at it a bit
joshprzybyszewski Oct 2, 2021
d82d612
recursive copy
joshprzybyszewski Oct 2, 2021
57fe1c3
emulate a healthcheck
joshprzybyszewski Oct 2, 2021
42c7cee
always check errors
joshprzybyszewski Oct 2, 2021
092c9c7
custom built retry healthcheck
joshprzybyszewski Oct 2, 2021
0eda125
update unit tests
joshprzybyszewski Oct 2, 2021
4889c6e
be smrtr
joshprzybyszewski Oct 2, 2021
d52ef7b
address the timestamp on the action
joshprzybyszewski Oct 2, 2021
af57a59
don't test transactionality because it doesn't work
joshprzybyszewski Oct 2, 2021
ee9803d
and add another
joshprzybyszewski Oct 2, 2021
156294e
move up go-acc
joshprzybyszewski Oct 2, 2021
eeb3a60
move dynamo creation up again
joshprzybyszewski Oct 2, 2021
bad6bfb
clean up expectations
joshprzybyszewski Oct 2, 2021
376809b
its not real code anymore
joshprzybyszewski Oct 2, 2021
52698dd
I think this makes sense
joshprzybyszewski Oct 2, 2021
2c4ecbd
avoid looping if unnecessary
joshprzybyszewski Oct 2, 2021
1af77c5
add more testing...
joshprzybyszewski Oct 2, 2021
5fcd4e5
Merge branch 'cleanPersistenceTests' of github.com:joshprzybyszewski/…
joshprzybyszewski Oct 4, 2021
a9122a4
Merge branch 'cleanPersistenceTests' of github.com:joshprzybyszewski/…
joshprzybyszewski Oct 4, 2021
f23bd33
make it build
joshprzybyszewski Oct 4, 2021
f9aa4ee
aint nobody got time for that
joshprzybyszewski Oct 4, 2021
9eecef1
Merge branch 'cleanPersistenceTests' of github.com:joshprzybyszewski/…
joshprzybyszewski Oct 4, 2021
7fab117
Merge branch 'master' of github.com:joshprzybyszewski/cribbage into m…
joshprzybyszewski Oct 5, 2021
200708f
genericize the conditional error
joshprzybyszewski Oct 5, 2021
ba9ad06
just remove a made-up error:#
joshprzybyszewski Oct 5, 2021
048a222
clean up some errors and things
joshprzybyszewski Oct 5, 2021
63f0343
verify the actions before saving a new game
joshprzybyszewski Oct 6, 2021
06359be
Merge branch 'master' of github.com:joshprzybyszewski/cribbage into m…
joshprzybyszewski Oct 6, 2021
f76f8b1
improve the interaction service
joshprzybyszewski Oct 6, 2021
ae02b45
avoid an expensive copy
joshprzybyszewski Oct 6, 2021
ac753e6
classic off by one error
joshprzybyszewski Oct 6, 2021
085772b
swap logics and use the right value
joshprzybyszewski Oct 7, 2021
5836b68
use a better test
joshprzybyszewski Oct 7, 2021
0d2a4ba
add unit tests
joshprzybyszewski Oct 7, 2021
b4c0bae
aint nobody got time for that
joshprzybyszewski Oct 7, 2021
948377b
add tests for historic data
joshprzybyszewski Oct 7, 2021
d93773d
consolidate
joshprzybyszewski Oct 7, 2021
8c9b9ec
show that this actually fixes the issue
joshprzybyszewski Oct 7, 2021
9cd6e31
respect new logic
joshprzybyszewski Oct 10, 2021
1573d77
use more idiomatic and simple patterns
joshprzybyszewski Oct 10, 2021
c073d05
uint is better
joshprzybyszewski Oct 10, 2021
91ca010
move things to live closer to where they work
joshprzybyszewski Oct 10, 2021
a436257
CONSTANTS
joshprzybyszewski Oct 10, 2021
4f57727
value semantics > pointer semantics
joshprzybyszewski Oct 10, 2021
460913e
avoid dupl
joshprzybyszewski Oct 10, 2021
3ae59da
move to utils
joshprzybyszewski Oct 10, 2021
8d0302e
use a string pointer
joshprzybyszewski Oct 10, 2021
35b9cbc
clean up a few things
joshprzybyszewski Oct 22, 2021
fa855ef
use an interface style instead
joshprzybyszewski Nov 14, 2021
22040cd
use a better name
joshprzybyszewski Nov 14, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,11 @@ services:
environment:
AWS_ACCESS_KEY_ID: 'DUMMYIDEXAMPLE'
AWS_SECRET_ACCESS_KEY: 'DUMMYEXAMPLEKEY'
volumes:
- ".scripts/createDynamoTable.sh:/usr/local/bin/createTable.sh"
command:
dynamodb describe-limits --endpoint-url http://dynamodb-local:8000 --region us-west-2
# dynamodb describe-limits --endpoint-url http://dynamodb-local:8000 --region us-west-2
dynamodb create-table --endpoint-url http://dynamodb-local:8000 --billing-mode PAY_PER_REQUEST --region us-west-2 --table-name cribbage --attribute-definitions AttributeName=DDBid,AttributeType=S AttributeName=spec,AttributeType=S --key-schema AttributeName=DDBid,KeyType=HASH AttributeName=spec,KeyType=Range
mysql-database:
image: mysql:8
restart: always
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ go 1.14

require (
github.com/AlecAivazis/survey/v2 v2.0.4
github.com/aws/aws-sdk-go v1.40.32
github.com/gin-gonic/contrib v0.0.0-20191209060500-d6e26eeaa607
github.com/gin-gonic/gin v1.6.3
github.com/glacjay/goini v0.0.0-20161120062552-fd3024d87ee2 // indirect
Expand Down
14 changes: 14 additions & 0 deletions go.sum

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 15 additions & 0 deletions scripts/createDynamoTable.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#!/bin/bash
set -e
set -x

aws dynamodb create-table \
--endpoint-url http://dynamodb-local:8000 \
--region us-west-2 \
--billing-mode PAY_PER_REQUEST \
--table-name cribbage \
--attribute-definitions \
AttributeName=DDBid,AttributeType=S \
AttributeName=spec,AttributeType=S \
--key-schema \
AttributeName=DDBid,KeyType=HASH \
AttributeName=spec,KeyType=Range
302 changes: 302 additions & 0 deletions server/persistence/dynamo/dynamo_service_game.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,302 @@
//nolint:dupl
package dynamo

import (
"context"
"encoding/json"
"errors"
"fmt"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/dynamodb"

"go.mongodb.org/mongo-driver/bson"

"github.com/joshprzybyszewski/cribbage/jsonutils"
"github.com/joshprzybyszewski/cribbage/model"
"github.com/joshprzybyszewski/cribbage/server/persistence"
)

type gameList struct {
GameID model.GameID `bson:"gameID"`
Games []model.Game `bson:"games,omitempty"`
}

type persistedGameList struct {
GameID model.GameID `bson:"gameID"`
TempGames []bson.M `bson:"games,omitempty"`
}

type getGameOptions struct {
latest bool
all bool
actions map[int]struct{}
}

var _ persistence.GameService = (*gameService)(nil)

type gameService struct {
ctx context.Context

svc *dynamodb.DynamoDB
}

func getGameService(
ctx context.Context,
svc *dynamodb.DynamoDB,
) (persistence.GameService, error) {

cszczepaniak marked this conversation as resolved.
Show resolved Hide resolved
return &gameService{
ctx: ctx,
svc: svc,
}, nil
}

func (gs *gameService) Get(id model.GameID) (model.Game, error) {
return gs.getSingleGame(id, getGameOptions{
latest: true,
})
}

func (gs *gameService) GetAt(id model.GameID, numActions uint) (model.Game, error) {
return gs.getSingleGame(id, getGameOptions{
actions: map[int]struct{}{int(numActions): {}},
})
}

func (gs *gameService) getSingleGame(
id model.GameID,
opts getGameOptions,
) (model.Game, error) {

games, err := gs.getGameStates(id, opts)
if err != nil {
return model.Game{}, err
}
if len(games) != 1 {
return model.Game{}, errors.New(`action doesn't exist`)
}
return games[0], nil
}

func (gs *gameService) getGameStates(id model.GameID, opts getGameOptions) ([]model.Game, error) { // nolint:gocyclo
pgl := persistedGameList{}

svc := dynamodb.New(session.New())
// I want to minimize the number of dynamo tables I use:
// "You should maintain as few tables as possible in a DynamoDB application."
// -https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/bp-general-nosql-design.html
input := &dynamodb.BatchGetItemInput{
RequestItems: map[string]*dynamodb.KeysAndAttributes{
dbName: {
Keys: []map[string]*dynamodb.AttributeValue{{
partitionKey: &dynamodb.AttributeValue{
S: aws.String(string(id)),
},
// TODO I don't remember right now how to get based on partition/sort key
}},
// TODO if only latest, then we can use a projexp to filter down
ProjectionExpression: aws.String("max(numGameActions)"),
},
},
}

result, err := svc.BatchGetItem(input)
if err != nil {
fmt.Println(err)
}
fmt.Println(result)

/*
err = mongo.WithSession(gs.ctx, gs.session, func(sc mongo.SessionContext) error {
err := gs.col.FindOne(sc, filter).Decode(&pgl)
if err != nil {
if err == mongo.ErrNoDocuments {
return persistence.ErrGameNotFound
}
return err
}
return nil
})
*/

if err != nil {
return nil, err
}

if opts.actions == nil {
opts.actions = make(map[int]struct{})
}
if opts.latest {
opts.actions[len(pgl.TempGames)-1] = struct{}{}
}

gl := gameList{
GameID: id,
Games: make([]model.Game, 0, len(pgl.TempGames)),
}

for i, tempGame := range pgl.TempGames {
if _, ok := opts.actions[i]; !ok && !opts.all {
continue
}

obj, err := json.Marshal(tempGame)
if err != nil {
return nil, err
}

g, err := jsonutils.UnmarshalGame(obj)
if err != nil {
return nil, err
}

gl.Games = append(gl.Games, g)
}

return gl.Games, nil
}

func (gs *gameService) UpdatePlayerColor(gID model.GameID, pID model.PlayerID, color model.PlayerColor) error {
cszczepaniak marked this conversation as resolved.
Show resolved Hide resolved
g, err := gs.Get(gID)
if err != nil {
return err
}

if c, ok := g.PlayerColors[pID]; ok {
if c != color {
return errors.New(`mismatched game-player color`)
}

// the Game already knows this player's color; nothing to do
return nil
}

games, err := gs.getGameStates(gID, getGameOptions{
all: true,
})
if err != nil {
return err
}

recentGame := games[len(games)-1]
if recentGame.PlayerColors == nil {
recentGame.PlayerColors = make(map[model.PlayerID]model.PlayerColor, 1)
}
recentGame.PlayerColors[pID] = color

games[len(games)-1] = recentGame
newGameList := gameList{
GameID: gID,
Games: games,
}

return gs.saveGameList(newGameList)
}

func (gs *gameService) Begin(g model.Game) error {
return gs.Save(g)
}

func (gs *gameService) Save(g model.Game) error {
saved := gameList{}
var err error
/*
filter := bsonGameIDFilter(g.ID)

err := mongo.WithSession(gs.ctx, gs.session, func(sc mongo.SessionContext) error {
return gs.col.FindOne(sc, filter).Decode(&saved)
})

if err != nil {
// if this is the first time saving the game, then we get ErrNoDocuments
if err != mongo.ErrNoDocuments {
return err
}

// Since this is the first save, we should have _no_ actions
if len(g.Actions) != 0 {
return persistence.ErrGameInitialSave
}

saved.GameID = g.ID
saved.Games = []model.Game{g}

return mongo.WithSession(gs.ctx, gs.session, func(sc mongo.SessionContext) error {
var ior *mongo.InsertOneResult
ior, err = gs.col.InsertOne(sc, saved)
if err != nil {
return err
}
if ior.InsertedID == nil {
// not sure if this is the right thing to check
return errors.New(`game not saved`)
}

return nil
})
}
*/

if saved.GameID != g.ID {
return errors.New(`bad save somewhere`)
}
err = validateGameState(saved.Games, g)
if err != nil {
return err
}

err = persistence.ValidateLatestActionBelongs(g)
if err != nil {
return err
}

saved.Games = append(saved.Games, g)

return gs.saveGameList(saved)
}

func validateGameState(savedGames []model.Game, newGameState model.Game) error {
if len(savedGames) != len(newGameState.Actions) {
return persistence.ErrGameActionsOutOfOrder
}
for i := range savedGames {
savedActions := savedGames[i].Actions
myKnownActions := newGameState.Actions[:i]
if len(savedActions) != len(myKnownActions) {
return persistence.ErrGameActionsOutOfOrder
}
for ai := range savedActions {
a := savedActions[ai]
if a.ID != myKnownActions[ai].ID || a.Overcomes != myKnownActions[ai].Overcomes {
return persistence.ErrGameActionsOutOfOrder
}
}
}
return nil
}

func (gs *gameService) saveGameList(saved gameList) error {
return errors.New(`todo`)
/*
filter := bsonGameIDFilter(saved.GameID)
return mongo.WithSession(gs.ctx, gs.session, func(sc mongo.SessionContext) error {
ur, err := gs.col.ReplaceOne(sc, filter, saved)
if err != nil {
return err
}

switch {
case ur.ModifiedCount > 1:
return errors.New(`modified too many games`)
case ur.MatchedCount > 1:
return errors.New(`matched more than one game entry`)
case ur.UpsertedCount > 1:
return errors.New(`replaced more than one game`)
}

return nil
})
*/
}
Loading