diff --git a/cmd/lilypad/solver.go b/cmd/lilypad/solver.go index c990121b..332fb082 100644 --- a/cmd/lilypad/solver.go +++ b/cmd/lilypad/solver.go @@ -1,8 +1,12 @@ package lilypad import ( + "fmt" + optionsfactory "github.com/lilypad-tech/lilypad/pkg/options" "github.com/lilypad-tech/lilypad/pkg/solver" + "github.com/lilypad-tech/lilypad/pkg/solver/store" + db "github.com/lilypad-tech/lilypad/pkg/solver/store/db" memorystore "github.com/lilypad-tech/lilypad/pkg/solver/store/memory" "github.com/lilypad-tech/lilypad/pkg/system" "github.com/lilypad-tech/lilypad/pkg/web3" @@ -19,7 +23,6 @@ func newSolverCmd() *cobra.Command { Long: "Start the lilypad solver service.", Example: "", RunE: func(cmd *cobra.Command, _ []string) error { - network, _ := cmd.Flags().GetString("network") options, err := optionsfactory.ProcessSolverOptions(options, network) if err != nil { @@ -57,7 +60,7 @@ func runSolver(cmd *cobra.Command, options solver.SolverOptions, network string) return err } - solverStore, err := memorystore.NewSolverStoreMemory() + solverStore, err := getSolverStore(options.Store) if err != nil { return err } @@ -79,3 +82,25 @@ func runSolver(cmd *cobra.Command, options solver.SolverOptions, network string) } } } + +func getSolverStore(options store.StoreOptions) (store.SolverStore, error) { + var solverStore store.SolverStore + var err error + + switch options.Type { + case "database": + solverStore, err = db.NewSolverStoreDatabase(options.ConnStr, options.GormLogLevel) + if err != nil { + return nil, err + } + case "memory": + solverStore, err = memorystore.NewSolverStoreMemory() + if err != nil { + return nil, err + } + default: + return nil, fmt.Errorf("expected solver store type database or memory, but received: %s", options.Type) + } + + return solverStore, nil +} diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml index 098f5bf7..e04dbeb9 100644 --- a/docker/docker-compose.dev.yml +++ b/docker/docker-compose.dev.yml @@ -7,6 +7,7 @@ services: container_name: solver depends_on: - chain + - postgres build: context: .. dockerfile: ./docker/solver/Dockerfile @@ -21,6 +22,9 @@ services: - SERVER_URL=${SERVER_URL} - WEB3_RPC_URL=${WEB3_RPC_URL} - DISABLE_TELEMETRY=${DISABLE_TELEMETRY} + - STORE_TYPE=${STORE_TYPE} + - STORE_CONN_STR=${STORE_CONN_STR} + - STORE_GORM_LOG_LEVEL=${STORE_GORM_LOG_LEVEL} ports: - 8081:8081 healthcheck: diff --git a/go.mod b/go.mod index ab75e147..874dfd7f 100644 --- a/go.mod +++ b/go.mod @@ -43,7 +43,11 @@ require ( go.opentelemetry.io/otel/sdk/metric v1.32.0 go.opentelemetry.io/otel/trace v1.32.0 golang.org/x/crypto v0.28.0 + golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 gorgonia.org/cu v0.9.7-0.20240623234718-3cd40db700e9 + gorm.io/datatypes v1.2.4 + gorm.io/driver/postgres v1.5.9 + gorm.io/gorm v1.25.12 k8s.io/apimachinery v0.29.0 pgregory.net/rapid v1.1.0 ) @@ -51,6 +55,7 @@ require ( require ( bazil.org/fuse v0.0.0-20200407214033-5883e5a4b512 // indirect dario.cat/mergo v1.0.0 // indirect + filippo.io/edwards25519 v1.1.0 // indirect github.com/BTBurke/k8sresource v1.2.0 // indirect github.com/Jorropo/jsync v1.0.1 // indirect github.com/Masterminds/semver v1.5.0 // indirect @@ -98,6 +103,7 @@ require ( github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.2.6 // indirect + github.com/go-sql-driver/mysql v1.8.1 // indirect github.com/go-stack/stack v1.8.1 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect @@ -144,11 +150,17 @@ require ( github.com/ipld/go-car/v2 v2.13.1 // indirect github.com/ipld/go-codec-dagpb v1.6.0 // indirect github.com/ipld/go-ipld-prime v0.21.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect + github.com/jackc/pgx/v5 v5.5.5 // indirect + github.com/jackc/puddle/v2 v2.2.1 // indirect github.com/jackpal/go-nat-pmp v1.0.2 // indirect github.com/jaypipes/pcidb v1.0.0 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/jbenet/go-temp-err-catcher v0.1.0 // indirect github.com/jbenet/goprocess v0.1.4 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/klauspost/compress v1.17.11 // indirect github.com/klauspost/cpuid/v2 v2.2.8 // indirect @@ -270,7 +282,6 @@ require ( go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect go4.org v0.0.0-20230225012048-214862532bf5 // indirect - golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect golang.org/x/mod v0.19.0 // indirect golang.org/x/net v0.30.0 // indirect golang.org/x/sync v0.9.0 // indirect @@ -287,6 +298,7 @@ require ( gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + gorm.io/driver/mysql v1.5.6 // indirect howett.net/plist v1.0.0 // indirect k8s.io/klog/v2 v2.110.1 // indirect k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect diff --git a/go.sum b/go.sum index f9a4c05a..671df914 100644 --- a/go.sum +++ b/go.sum @@ -25,6 +25,8 @@ dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7 dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU= dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4= dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU= +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 h1:cTp8I5+VIoKjsnZuH8vjyaysT/ses3EvZeaV/1UkF2M= @@ -261,6 +263,9 @@ github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= +github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/go-stack/stack v1.8.1 h1:ntEHSVwIt7PNXNpgPmVfMrNhLtgjlmnZha2kOpuRiDw= github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= @@ -282,6 +287,10 @@ github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keL github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= +github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= +github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= +github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -506,6 +515,14 @@ github.com/ipld/go-ipld-prime v0.21.0 h1:n4JmcpOlPDIxBcY037SVfpd1G+Sj1nKZah0m6QH github.com/ipld/go-ipld-prime v0.21.0/go.mod h1:3RLqy//ERg/y5oShXXdx5YIp50cFGOanyMctpPjsvxQ= github.com/ipld/go-ipld-prime/storage/bsadapter v0.0.0-20230102063945-1a409dc236dd h1:gMlw/MhNr2Wtp5RwGdsW23cs+yCuj9k2ON7i9MiJlRo= github.com/ipld/go-ipld-prime/storage/bsadapter v0.0.0-20230102063945-1a409dc236dd/go.mod h1:wZ8hH8UxeryOs4kJEJaiui/s00hDSbE37OKsL47g+Sw= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 h1:L0QtFUgDarD7Fpv9jeVMgy/+Ec0mtnmYuImjTz6dtDA= +github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw= +github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= +github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= +github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= github.com/jaypipes/ghw v0.12.0 h1:xU2/MDJfWmBhJnujHY9qwXQLs3DBsf0/Xa9vECY0Tho= @@ -525,6 +542,10 @@ github.com/jbenet/goprocess v0.1.4/go.mod h1:5yspPrukOVuOLORacaBi858NqyClJPQxYZl github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU= github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= @@ -642,9 +663,13 @@ github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI= +github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4= +github.com/microsoft/go-mssqldb v0.17.0 h1:Fto83dMZPnYv1Zwx5vHHxpNraeEaUlQ/hhHLgZiaenE= +github.com/microsoft/go-mssqldb v0.17.0/go.mod h1:OkoNGhGEs8EZqchVTtochlXruEhEOaO4S0d2sB5aeGQ= github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4= github.com/miekg/dns v1.1.61 h1:nLxbwF3XxhwVSm8g9Dghm9MHPaUZuqhPiGL+675ZmEs= @@ -1437,6 +1462,19 @@ gorgonia.org/vecf32 v0.7.0/go.mod h1:iHG+kvTMqGYA0SgahfO2k62WRnxmHsqAREGbayRDzy8 gorgonia.org/vecf32 v0.9.0/go.mod h1:NCc+5D2oxddRL11hd+pCB1PEyXWOyiQxfZ/1wwhOXCA= gorgonia.org/vecf64 v0.7.0/go.mod h1:1y4pmcSd+wh3phG+InwWQjYrqwyrtN9h27WLFVQfV1Q= gorgonia.org/vecf64 v0.9.0/go.mod h1:hp7IOWCnRiVQKON73kkC/AUMtEXyf9kGlVrtPQ9ccVA= +gorm.io/datatypes v1.2.4 h1:uZmGAcK/QZ0uyfCuVg0VQY1ZmV9h1fuG0tMwKByO1z4= +gorm.io/datatypes v1.2.4/go.mod h1:f4BsLcFAX67szSv8svwLRjklArSHAvHLeE3pXAS5DZI= +gorm.io/driver/mysql v1.5.6 h1:Ld4mkIickM+EliaQZQx3uOJDJHtrd70MxAUqWqlx3Y8= +gorm.io/driver/mysql v1.5.6/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM= +gorm.io/driver/postgres v1.5.9 h1:DkegyItji119OlcaLjqN11kHoUgZ/j13E0jkJZgD6A8= +gorm.io/driver/postgres v1.5.9/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI= +gorm.io/driver/sqlite v1.4.3 h1:HBBcZSDnWi5BW3B3rwvVTc510KGkBkexlOg0QrmLUuU= +gorm.io/driver/sqlite v1.4.3/go.mod h1:0Aq3iPO+v9ZKbcdiz8gLWRw5VOPcBOPUQJFLq5e2ecI= +gorm.io/driver/sqlserver v1.4.1 h1:t4r4r6Jam5E6ejqP7N82qAJIJAht27EGT41HyPfXRw0= +gorm.io/driver/sqlserver v1.4.1/go.mod h1:DJ4P+MeZbc5rvY58PnmN1Lnyvb5gw5NPzGshHDnJLig= +gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= +gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= +gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/pkg/jsonl/reader.go b/pkg/jsonl/reader.go deleted file mode 100644 index 03f5d64b..00000000 --- a/pkg/jsonl/reader.go +++ /dev/null @@ -1,49 +0,0 @@ -package jsonl - -import ( - "bufio" - "encoding/json" - "fmt" - "io" -) - -type Reader struct { - r io.Reader - scanner *bufio.Scanner -} - -func NewReader(r io.Reader) Reader { - scanner := bufio.NewScanner(r) - scanner.Split(bufio.ScanLines) - - return Reader{ - r: r, - scanner: scanner, - } -} - -func (r Reader) Close() error { - if c, ok := r.r.(io.ReadCloser); ok { - return c.Close() - } - return fmt.Errorf("given reader is no ReadCloser") -} - -func (r Reader) ReadSingleLine(output interface{}) error { - ok := r.scanner.Scan() - if !ok { - return fmt.Errorf("could not read from scanner. Scanner done") - } - - return json.Unmarshal(r.scanner.Bytes(), output) -} - -func (r Reader) ReadLines(callback func(data []byte) error) error { - for r.scanner.Scan() { - err := callback(r.scanner.Bytes()) - if err != nil { - return fmt.Errorf("error in callback: %w", err) - } - } - return nil -} diff --git a/pkg/jsonl/writer.go b/pkg/jsonl/writer.go deleted file mode 100644 index d13557a1..00000000 --- a/pkg/jsonl/writer.go +++ /dev/null @@ -1,48 +0,0 @@ -package jsonl - -import ( - "encoding/json" - "fmt" - "io" - "net/http" -) - -type Writer struct { - w io.Writer -} - -func NewWriter(w io.Writer) Writer { - return Writer{ - w: w, - } -} - -func (w Writer) Close() error { - if c, ok := w.w.(io.WriteCloser); ok { - return c.Close() - } - return fmt.Errorf("given writer is no WriteCloser") -} - -func (w Writer) Write(data interface{}) error { - j, err := json.Marshal(data) - if err != nil { - return fmt.Errorf("could not json marshal data: %w", err) - } - - _, err = w.w.Write(j) - if err != nil { - return fmt.Errorf("could not write json data to underlying io.Writer: %w", err) - } - - _, err = w.w.Write([]byte("\n")) - if err != nil { - return fmt.Errorf("could not write newline to underlying io.Writer: %w", err) - } - - if f, ok := w.w.(http.Flusher); ok { - // If http writer, flush as well - f.Flush() - } - return nil -} diff --git a/pkg/options/solver.go b/pkg/options/solver.go index 4bc2b328..ae16452b 100644 --- a/pkg/options/solver.go +++ b/pkg/options/solver.go @@ -9,6 +9,7 @@ import ( func NewSolverOptions() solver.SolverOptions { options := solver.SolverOptions{ Server: GetDefaultServerOptions(), + Store: GetDefaultStoreOptions(), Web3: GetDefaultWeb3Options(), Services: GetDefaultServicesOptions(), Telemetry: GetDefaultTelemetryOptions(), @@ -19,19 +20,24 @@ func NewSolverOptions() solver.SolverOptions { } func AddSolverCliFlags(cmd *cobra.Command, options *solver.SolverOptions) { - AddWeb3CliFlags(cmd, &options.Web3) AddServerCliFlags(cmd, &options.Server) + AddStoreCliFlags(cmd, &options.Store) + AddWeb3CliFlags(cmd, &options.Web3) AddServicesCliFlags(cmd, &options.Services) AddTelemetryCliFlags(cmd, &options.Telemetry) AddMetricsCliFlags(cmd, &options.Metrics) } func CheckSolverOptions(options solver.SolverOptions) error { - err := CheckWeb3Options(options.Web3) + err := CheckServerOptions(options.Server) + if err != nil { + return err + } + err = CheckStoreOptions(options.Store) if err != nil { return err } - err = CheckServerOptions(options.Server) + err = CheckWeb3Options(options.Web3) if err != nil { return err } diff --git a/pkg/options/store.go b/pkg/options/store.go new file mode 100644 index 00000000..600c0b74 --- /dev/null +++ b/pkg/options/store.go @@ -0,0 +1,48 @@ +package options + +import ( + "fmt" + + "github.com/lilypad-tech/lilypad/pkg/solver/store" + "github.com/spf13/cobra" +) + +func GetDefaultStoreOptions() store.StoreOptions { + return store.StoreOptions{ + Type: GetDefaultServeOptionString("STORE_TYPE", "database"), + ConnStr: GetDefaultServeOptionString("STORE_CONN_STR", ""), + GormLogLevel: GetDefaultServeOptionString("STORE_GORM_LOG_LEVEL", "silent"), + } +} + +func AddStoreCliFlags(cmd *cobra.Command, storeOptions *store.StoreOptions) { + cmd.PersistentFlags().StringVar( + &storeOptions.Type, "store-type", storeOptions.Type, + `The store type used by the solver, one of "database" or "memory" (STORE_TYPE).`, + ) + cmd.PersistentFlags().StringVar( + &storeOptions.ConnStr, "store-conn-str", storeOptions.ConnStr, + `The database store connection string (STORE_CONN_STR).`, + ) + cmd.PersistentFlags().StringVar( + &storeOptions.GormLogLevel, "store-gorm-log-level", storeOptions.GormLogLevel, + `The database store gorm log level, one of "silent", "info", "error", "warn" (STORE_GORM_LOG_LEVEL).`, + ) +} + +func CheckStoreOptions(options store.StoreOptions) error { + if options.Type != "database" && options.Type != "memory" { + return fmt.Errorf("STORE_TYPE must be \"database\" or \"memory\"") + } + if options.Type == "database" && options.ConnStr == "" { + return fmt.Errorf("STORE_CONN_STR is required when using the database store") + } + if options.GormLogLevel != "silent" && + options.GormLogLevel != "info" && + options.GormLogLevel != "error" && + options.GormLogLevel != "warn" { + return fmt.Errorf("STORE_GORM_LOG_LEVEL must be \"silent\", \"info\", \"error\", or \"warn\"") + } + + return nil +} diff --git a/pkg/solver/solver.go b/pkg/solver/solver.go index 314b2ac3..e519795b 100644 --- a/pkg/solver/solver.go +++ b/pkg/solver/solver.go @@ -15,8 +15,9 @@ import ( ) type SolverOptions struct { - Web3 web3.Web3Options Server http.ServerOptions + Store store.StoreOptions + Web3 web3.Web3Options Services data.ServiceConfig Telemetry system.TelemetryOptions Metrics system.MetricsOptions diff --git a/pkg/solver/store/db/db.go b/pkg/solver/store/db/db.go new file mode 100644 index 00000000..9d8aab99 --- /dev/null +++ b/pkg/solver/store/db/db.go @@ -0,0 +1,621 @@ +package store + +import ( + "errors" + "fmt" + + "github.com/lilypad-tech/lilypad/pkg/data" + "github.com/lilypad-tech/lilypad/pkg/solver/store" + "gorm.io/datatypes" + "gorm.io/driver/postgres" + "gorm.io/gorm" + "gorm.io/gorm/logger" +) + +type SolverStoreDatabase struct { + db *gorm.DB +} + +func NewSolverStoreDatabase(connStr string, gormLogLevel string) (*SolverStoreDatabase, error) { + config := &gorm.Config{} + switch gormLogLevel { + case "silent": + config.Logger = logger.Default.LogMode(logger.Silent) + case "info": + config.Logger = logger.Default.LogMode(logger.Info) + case "error": + config.Logger = logger.Default.LogMode(logger.Error) + case "warn": + config.Logger = logger.Default.LogMode(logger.Warn) + default: + // The option is validated for a correct log level string, + // so we should not enter the default case. + config.Logger = logger.Default.LogMode(logger.Silent) + } + + db, err := gorm.Open(postgres.Open(connStr), config) + if err != nil { + return nil, err + } + + db.AutoMigrate(&JobOffer{}) + db.AutoMigrate(&ResourceOffer{}) + db.AutoMigrate(&Deal{}) + db.AutoMigrate(&Result{}) + db.AutoMigrate(&MatchDecision{}) + + return &SolverStoreDatabase{db}, nil +} + +func (store *SolverStoreDatabase) AddJobOffer(jobOffer data.JobOfferContainer) (*data.JobOfferContainer, error) { + record := JobOffer{ + CID: jobOffer.ID, + JobCreator: jobOffer.JobCreator, + DealID: jobOffer.DealID, + State: jobOffer.State, + Attributes: datatypes.NewJSONType(jobOffer), + } + + result := store.db.Create(&record) + if result.Error != nil { + return nil, result.Error + } + + return &jobOffer, nil +} + +func (store *SolverStoreDatabase) AddResourceOffer(resourceOffer data.ResourceOfferContainer) (*data.ResourceOfferContainer, error) { + record := ResourceOffer{ + CID: resourceOffer.ID, + ResourceProvider: resourceOffer.ResourceProvider, + DealID: resourceOffer.DealID, + State: resourceOffer.State, + Attributes: datatypes.NewJSONType(resourceOffer), + } + + result := store.db.Create(&record) + if result.Error != nil { + return nil, result.Error + } + + return &resourceOffer, nil +} + +func (store *SolverStoreDatabase) AddDeal(deal data.DealContainer) (*data.DealContainer, error) { + record := Deal{ + CID: deal.ID, + JobCreator: deal.JobCreator, + ResourceProvider: deal.ResourceProvider, + Mediator: deal.Mediator, + State: deal.State, + Attributes: datatypes.NewJSONType(deal), + } + + result := store.db.Create(&record) + if result.Error != nil { + return nil, result.Error + } + + return &deal, nil +} + +func (store *SolverStoreDatabase) AddResult(result data.Result) (*data.Result, error) { + record := Result{ + DealID: result.DealID, + CID: result.ID, + Attributes: datatypes.NewJSONType(result), + } + + res := store.db.Create(&record) + if res.Error != nil { + return nil, res.Error + } + + return &result, nil +} + +func (store *SolverStoreDatabase) AddMatchDecision(resourceOffer string, jobOffer string, deal string, result bool) (*data.MatchDecision, error) { + decision := &data.MatchDecision{ + ResourceOffer: resourceOffer, + JobOffer: jobOffer, + Deal: deal, + Result: result, + } + record := MatchDecision{ + ResourceOffer: resourceOffer, + JobOffer: jobOffer, + Attributes: datatypes.NewJSONType(*decision), + } + + res := store.db.Create(&record) + if res.Error != nil { + return nil, res.Error + } + + return decision, nil +} + +func (store *SolverStoreDatabase) GetJobOffers(query store.GetJobOffersQuery) ([]data.JobOfferContainer, error) { + q := store.db.Where([]JobOffer{}) + + // Apply filters + if query.JobCreator != "" { + q = q.Where("job_creator = ?", query.JobCreator) + } + if query.NotMatched { + q = q.Where("deal_id = ''") + } + if !query.IncludeCancelled { + q = q.Where("state != ?", data.GetAgreementStateIndex("JobOfferCancelled")) + } + + var records []JobOffer + if err := q.Find(&records).Error; err != nil { + return nil, err + } + + jobOffers := make([]data.JobOfferContainer, len(records)) + for i, record := range records { + jobOffers[i] = record.Attributes.Data() + } + + return jobOffers, nil +} + +func (store *SolverStoreDatabase) GetResourceOffers(query store.GetResourceOffersQuery) ([]data.ResourceOfferContainer, error) { + q := store.db.Where([]ResourceOffer{}) + + // Apply filters + if query.ResourceProvider != "" { + q = q.Where("resource_provider = ?", query.ResourceProvider) + } + if query.NotMatched { + q = q.Where("deal_id = ''") + } + if query.Active { + q = q.Where("state IN (?)", []uint8{ + data.GetAgreementStateIndex("DealNegotiating"), + data.GetAgreementStateIndex("DealAgreed"), + }) + } + + var records []ResourceOffer + if err := q.Find(&records).Error; err != nil { + return nil, err + } + + resourceOffers := make([]data.ResourceOfferContainer, len(records)) + for i, record := range records { + resourceOffers[i] = record.Attributes.Data() + } + + return resourceOffers, nil +} + +func (store *SolverStoreDatabase) GetDeals(query store.GetDealsQuery) ([]data.DealContainer, error) { + q := store.db.Where([]Deal{}) + + // Apply filters + if query.JobCreator != "" { + q = q.Where("job_creator = ?", query.JobCreator) + } + if query.ResourceProvider != "" { + q = q.Where("resource_provider = ?", query.ResourceProvider) + } + if query.Mediator != "" { + q = q.Where("mediator = ?", query.Mediator) + } + if query.State != "" { + parsedState, err := data.GetAgreementState(query.State) + if err != nil { + return nil, err + } + q = q.Where("state = ?", parsedState) + } + + var records []Deal + if err := q.Find(&records).Error; err != nil { + return nil, err + } + + deals := make([]data.DealContainer, len(records)) + for i, record := range records { + deals[i] = record.Attributes.Data() + } + + return deals, nil +} + +func (store *SolverStoreDatabase) GetDealsAll() ([]data.DealContainer, error) { + var records []Deal + if err := store.db.Find(&records).Error; err != nil { + return nil, err + } + + deals := make([]data.DealContainer, len(records)) + for i, record := range records { + deals[i] = record.Attributes.Data() + } + + return deals, nil +} + +func (store *SolverStoreDatabase) GetResults() ([]data.Result, error) { + var records []Result + if err := store.db.Find(&records).Error; err != nil { + return nil, err + } + + results := make([]data.Result, len(records)) + for i, record := range records { + results[i] = record.Attributes.Data() + } + + return results, nil +} + +func (store *SolverStoreDatabase) GetMatchDecisions() ([]data.MatchDecision, error) { + var records []MatchDecision + if err := store.db.Find(&records).Error; err != nil { + return nil, err + } + + decisions := make([]data.MatchDecision, len(records)) + for i, record := range records { + decisions[i] = record.Attributes.Data() + } + + return decisions, nil +} + +func (store *SolverStoreDatabase) GetJobOffer(id string) (*data.JobOfferContainer, error) { + // Offers are unique by CID, so we can query first + var record JobOffer + result := store.db.Where("c_id = ?", id).First(&record) + + if result.Error != nil { + if errors.Is(result.Error, gorm.ErrRecordNotFound) { + return nil, nil + } + return nil, result.Error + } + + jobOffer := record.Attributes.Data() + return &jobOffer, nil +} + +func (store *SolverStoreDatabase) GetResourceOffer(id string) (*data.ResourceOfferContainer, error) { + // Offers are unique by CID, so we can query first + var record ResourceOffer + result := store.db.Where("c_id = ?", id).First(&record) + + if result.Error != nil { + if errors.Is(result.Error, gorm.ErrRecordNotFound) { + return nil, nil + } + return nil, result.Error + } + + resourceOffer := record.Attributes.Data() + return &resourceOffer, nil +} + +func (store *SolverStoreDatabase) GetResourceOfferByAddress(address string) (*data.ResourceOfferContainer, error) { + var record ResourceOffer + result := store.db.Where("resource_provider = ?", address).First(&record) + + if result.Error != nil { + if errors.Is(result.Error, gorm.ErrRecordNotFound) { + return nil, nil + } + return nil, result.Error + } + + resourceOffer := record.Attributes.Data() + return &resourceOffer, nil +} + +func (store *SolverStoreDatabase) GetDeal(id string) (*data.DealContainer, error) { + // Deals are unique by CID, so we can query first + var record Deal + result := store.db.Where("c_id = ?", id).First(&record) + + if result.Error != nil { + if errors.Is(result.Error, gorm.ErrRecordNotFound) { + return nil, nil + } + return nil, result.Error + } + + deal := record.Attributes.Data() + return &deal, nil +} + +func (store *SolverStoreDatabase) GetResult(id string) (*data.Result, error) { + // Results are queried by deal ID for now + // Deal IDs are unique, so we can query first + var record Result + res := store.db.Where("deal_id = ?", id).First(&record) + + if res.Error != nil { + if errors.Is(res.Error, gorm.ErrRecordNotFound) { + return nil, nil + } + return nil, res.Error + } + + result := record.Attributes.Data() + return &result, nil +} + +func (store *SolverStoreDatabase) GetMatchDecision(resourceOffer string, jobOffer string) (*data.MatchDecision, error) { + // The resource offer and job offer are unique + // CIDs, so we can query first + var record MatchDecision + result := store.db.Where("resource_offer = ? AND job_offer = ?", resourceOffer, jobOffer).First(&record) + + if result.Error != nil { + if errors.Is(result.Error, gorm.ErrRecordNotFound) { + return nil, nil + } + return nil, result.Error + } + + decision := record.Attributes.Data() + return &decision, nil +} + +func (store *SolverStoreDatabase) UpdateJobOfferState(id string, dealID string, state uint8) (*data.JobOfferContainer, error) { + var record JobOffer + result := store.db.Where("c_id = ?", id).First(&record) + + if result.Error != nil { + if errors.Is(result.Error, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("job offer not found: %s", id) + } + return nil, result.Error + } + + // Update the jsonb data + inner := record.Attributes.Data() + inner.DealID = dealID + inner.State = state + + if err := store.db.Model(&record). + Select("DealID", "State", "Attributes"). + Updates(JobOffer{ + DealID: dealID, + State: state, + Attributes: datatypes.NewJSONType(inner), + }).Error; err != nil { + return nil, err + } + + return &inner, nil +} + +func (store *SolverStoreDatabase) UpdateResourceOfferState(id string, dealID string, state uint8) (*data.ResourceOfferContainer, error) { + var record ResourceOffer + result := store.db.Where("c_id = ?", id).First(&record) + + if result.Error != nil { + if errors.Is(result.Error, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("resource offer not found: %s", id) + } + return nil, result.Error + } + + // Update the jsonb data + inner := record.Attributes.Data() + inner.DealID = dealID + inner.State = state + + if err := store.db.Model(&record). + Select("DealID", "State", "Attributes"). + Updates(ResourceOffer{ + DealID: dealID, + State: state, + Attributes: datatypes.NewJSONType(inner), + }).Error; err != nil { + return nil, err + } + + return &inner, nil +} + +func (store *SolverStoreDatabase) UpdateDealState(id string, state uint8) (*data.DealContainer, error) { + var record Deal + result := store.db.Where("c_id = ?", id).First(&record) + + if result.Error != nil { + if errors.Is(result.Error, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("deal not found: %s", id) + } + return nil, result.Error + } + + // Update the jsonb data + inner := record.Attributes.Data() + inner.State = state + + if err := store.db.Model(&record). + Select("State", "Attributes"). + Updates(Deal{ + State: state, + Attributes: datatypes.NewJSONType(inner), + }).Error; err != nil { + return nil, err + } + + return &inner, nil +} + +func (store *SolverStoreDatabase) UpdateDealMediator(id string, mediator string) (*data.DealContainer, error) { + var record Deal + result := store.db.Where("c_id = ?", id).First(&record) + + if result.Error != nil { + if errors.Is(result.Error, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("deal not found: %s", id) + } + return nil, result.Error + } + + // Update the jsonb data + inner := record.Attributes.Data() + inner.Mediator = mediator + + if err := store.db.Model(&record). + Select("Mediator", "Attributes"). + Updates(Deal{ + Mediator: mediator, + Attributes: datatypes.NewJSONType(inner), + }).Error; err != nil { + return nil, err + } + + return &inner, nil +} + +func (store *SolverStoreDatabase) UpdateDealTransactionsJobCreator(id string, data data.DealTransactionsJobCreator) (*data.DealContainer, error) { + var record Deal + result := store.db.Where("c_id = ?", id).First(&record) + + if result.Error != nil { + if errors.Is(result.Error, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("deal not found: %s", id) + } + return nil, result.Error + } + + // Update the jsonb data + inner := record.Attributes.Data() + inner.Transactions.JobCreator = data + + if err := store.db.Model(&record). + Select("Attributes"). + Updates(Deal{ + Attributes: datatypes.NewJSONType(inner), + }).Error; err != nil { + return nil, err + } + + return &inner, nil +} + +func (store *SolverStoreDatabase) UpdateDealTransactionsResourceProvider(id string, data data.DealTransactionsResourceProvider) (*data.DealContainer, error) { + var record Deal + result := store.db.Where("c_id = ?", id).First(&record) + + if result.Error != nil { + if errors.Is(result.Error, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("deal not found: %s", id) + } + return nil, result.Error + } + + // Update the jsonb data + inner := record.Attributes.Data() + inner.Transactions.ResourceProvider = data + + if err := store.db.Model(&record). + Select("Attributes"). + Updates(Deal{ + Attributes: datatypes.NewJSONType(inner), + }).Error; err != nil { + return nil, err + } + + return &inner, nil +} + +func (store *SolverStoreDatabase) UpdateDealTransactionsMediator(id string, data data.DealTransactionsMediator) (*data.DealContainer, error) { + var record Deal + result := store.db.Where("c_id = ?", id).First(&record) + + if result.Error != nil { + if errors.Is(result.Error, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("deal not found: %s", id) + } + return nil, result.Error + } + + // Update the jsonb data + inner := record.Attributes.Data() + inner.Transactions.Mediator = data + + if err := store.db.Model(&record). + Select("Attributes"). + Updates(Deal{ + Attributes: datatypes.NewJSONType(inner), + }).Error; err != nil { + return nil, err + } + + return &inner, nil +} + +func (store *SolverStoreDatabase) RemoveJobOffer(id string) error { + var record JobOffer + result := store.db.Where("c_id = ?", id).Delete(&record) + if result.Error != nil { + return result.Error + } + return nil +} + +func (store *SolverStoreDatabase) RemoveResourceOffer(id string) error { + var record ResourceOffer + result := store.db.Where("c_id = ?", id).Delete(&record) + if result.Error != nil { + return result.Error + } + return nil +} + +func (store *SolverStoreDatabase) RemoveDeal(id string) error { + var record Deal + result := store.db.Where("c_id = ?", id).Delete(&record) + if result.Error != nil { + return result.Error + } + return nil +} + +func (store *SolverStoreDatabase) RemoveResult(id string) error { + var record Result + result := store.db.Where("deal_id = ?", id).Delete(&record) + if result.Error != nil { + return result.Error + } + return nil +} + +func (store *SolverStoreDatabase) RemoveMatchDecision(resourceOffer string, jobOffer string) error { + if resourceOffer == "" && jobOffer == "" { + return fmt.Errorf("resource offer or job offer must be set") + } + + // Build the query based on which parameters are provided + query := store.db.Where([]MatchDecision{}) + + if resourceOffer != "" { + query = query.Where("resource_offer = ?", resourceOffer) + } + if jobOffer != "" { + query = query.Where("job_offer = ?", jobOffer) + } + + // Execute the delete + result := query.Delete(&MatchDecision{}) + if result.Error != nil { + return result.Error + } + + return nil +} + +// Strictly speaking, the compiler will check the interface +// implementation without this check. But some code editors +// report errors more effectively when we have it. +var _ store.SolverStore = (*SolverStoreDatabase)(nil) diff --git a/pkg/solver/store/db/models.go b/pkg/solver/store/db/models.go new file mode 100644 index 00000000..1c405622 --- /dev/null +++ b/pkg/solver/store/db/models.go @@ -0,0 +1,49 @@ +package store + +import ( + "github.com/lilypad-tech/lilypad/pkg/data" + "gorm.io/datatypes" + "gorm.io/gorm" +) + +type JobOffer struct { + gorm.Model + CID string `gorm:"index"` + JobCreator string `gorm:"index"` + DealID string `gorm:"index"` + State uint8 + Attributes datatypes.JSONType[data.JobOfferContainer] +} + +type ResourceOffer struct { + gorm.Model + CID string `gorm:"index"` + ResourceProvider string `gorm:"index"` + DealID string `gorm:"index"` + State uint8 + Attributes datatypes.JSONType[data.ResourceOfferContainer] +} + +type Deal struct { + gorm.Model + CID string `gorm:"index"` + JobCreator string `gorm:"index"` + ResourceProvider string `gorm:"index"` + Mediator string + State uint8 + Attributes datatypes.JSONType[data.DealContainer] +} + +type Result struct { + gorm.Model + DealID string `gorm:"index"` // We query with deal ID for now + CID string + Attributes datatypes.JSONType[data.Result] +} + +type MatchDecision struct { + gorm.Model + ResourceOffer string `gorm:"primaryKey"` + JobOffer string `gorm:"primaryKey"` + Attributes datatypes.JSONType[data.MatchDecision] +} diff --git a/pkg/solver/store/memory/store.go b/pkg/solver/store/memory/store.go index 2c35783f..bc83ce3b 100644 --- a/pkg/solver/store/memory/store.go +++ b/pkg/solver/store/memory/store.go @@ -6,7 +6,6 @@ import ( "sync" "github.com/lilypad-tech/lilypad/pkg/data" - "github.com/lilypad-tech/lilypad/pkg/jsonl" "github.com/lilypad-tech/lilypad/pkg/solver/store" ) @@ -17,23 +16,15 @@ type SolverStoreMemory struct { resultMap map[string]*data.Result matchDecisionMap map[string]*data.MatchDecision mutex sync.RWMutex - logWriters map[string]jsonl.Writer } func NewSolverStoreMemory() (*SolverStoreMemory, error) { - kinds := []string{"job_offers", "resource_offers", "deals", "decisions", "results"} - logWriters, err := store.GetLogWriters(kinds) - if err != nil { - return nil, err - } - return &SolverStoreMemory{ jobOfferMap: map[string]*data.JobOfferContainer{}, resourceOfferMap: map[string]*data.ResourceOfferContainer{}, dealMap: map[string]*data.DealContainer{}, resultMap: map[string]*data.Result{}, matchDecisionMap: map[string]*data.MatchDecision{}, - logWriters: logWriters, }, nil } @@ -42,7 +33,6 @@ func (s *SolverStoreMemory) AddJobOffer(jobOffer data.JobOfferContainer) (*data. defer s.mutex.Unlock() s.jobOfferMap[jobOffer.ID] = &jobOffer - s.logWriters["job_offers"].Write(jobOffer) return &jobOffer, nil } @@ -51,7 +41,6 @@ func (s *SolverStoreMemory) AddResourceOffer(resourceOffer data.ResourceOfferCon defer s.mutex.Unlock() s.resourceOfferMap[resourceOffer.ID] = &resourceOffer - s.logWriters["resource_offers"].Write(resourceOffer) return &resourceOffer, nil } @@ -59,7 +48,7 @@ func (s *SolverStoreMemory) AddDeal(deal data.DealContainer) (*data.DealContaine s.mutex.Lock() defer s.mutex.Unlock() s.dealMap[deal.ID] = &deal - s.logWriters["deals"].Write(deal) + return &deal, nil } @@ -67,7 +56,7 @@ func (s *SolverStoreMemory) AddResult(result data.Result) (*data.Result, error) s.mutex.Lock() defer s.mutex.Unlock() s.resultMap[result.DealID] = &result - s.logWriters["results"].Write(result) + return &result, nil } @@ -86,7 +75,7 @@ func (s *SolverStoreMemory) AddMatchDecision(resourceOffer string, jobOffer stri Result: result, } s.matchDecisionMap[id] = decision - s.logWriters["decisions"].Write(decision) + return decision, nil } @@ -181,6 +170,26 @@ func (s *SolverStoreMemory) GetDealsAll() ([]data.DealContainer, error) { return deals, nil } +func (s *SolverStoreMemory) GetResults() ([]data.Result, error) { + s.mutex.RLock() + defer s.mutex.RUnlock() + results := []data.Result{} + for _, result := range s.resultMap { + results = append(results, *result) + } + return results, nil +} + +func (s *SolverStoreMemory) GetMatchDecisions() ([]data.MatchDecision, error) { + s.mutex.RLock() + defer s.mutex.RUnlock() + results := []data.MatchDecision{} + for _, decision := range s.matchDecisionMap { + results = append(results, *decision) + } + return results, nil +} + func (s *SolverStoreMemory) GetJobOffer(id string) (*data.JobOfferContainer, error) { s.mutex.RLock() defer s.mutex.RUnlock() @@ -380,5 +389,32 @@ func (s *SolverStoreMemory) RemoveResourceOffer(id string) error { return nil } -// Compile-time interface check: +func (s *SolverStoreMemory) RemoveDeal(id string) error { + s.mutex.Lock() + defer s.mutex.Unlock() + delete(s.dealMap, id) + return nil +} + +func (s *SolverStoreMemory) RemoveResult(id string) error { + s.mutex.Lock() + defer s.mutex.Unlock() + delete(s.resultMap, id) + return nil +} + +func (s *SolverStoreMemory) RemoveMatchDecision(resourceOffer string, jobOffer string) error { + s.mutex.Lock() + defer s.mutex.Unlock() + for k := range s.matchDecisionMap { + if strings.Contains(k, jobOffer) || strings.Contains(k, resourceOffer) { + delete(s.matchDecisionMap, k) + } + } + return nil +} + +// Strictly speaking, the compiler will check the interface +// implementation without this check. But some code editors +// report errors more effectively when we have it. var _ store.SolverStore = (*SolverStoreMemory)(nil) diff --git a/pkg/solver/store/store.go b/pkg/solver/store/store.go index 0b9ee881..61f86045 100644 --- a/pkg/solver/store/store.go +++ b/pkg/solver/store/store.go @@ -2,12 +2,16 @@ package store import ( "fmt" - "os" "github.com/lilypad-tech/lilypad/pkg/data" - "github.com/lilypad-tech/lilypad/pkg/jsonl" ) +type StoreOptions struct { + Type string + ConnStr string + GormLogLevel string +} + type GetJobOffersQuery struct { JobCreator string `json:"job_creator"` // this means job offers that have not been matched at all yet @@ -53,7 +57,7 @@ type GetDealsQuery struct { type SolverStore interface { AddJobOffer(jobOffer data.JobOfferContainer) (*data.JobOfferContainer, error) - AddResourceOffer(jobOffer data.ResourceOfferContainer) (*data.ResourceOfferContainer, error) + AddResourceOffer(resourceOffer data.ResourceOfferContainer) (*data.ResourceOfferContainer, error) AddDeal(deal data.DealContainer) (*data.DealContainer, error) AddResult(result data.Result) (*data.Result, error) AddMatchDecision(resourceOffer string, jobOffer string, deal string, result bool) (*data.MatchDecision, error) @@ -61,6 +65,8 @@ type SolverStore interface { GetResourceOffers(query GetResourceOffersQuery) ([]data.ResourceOfferContainer, error) GetDeals(query GetDealsQuery) ([]data.DealContainer, error) GetDealsAll() ([]data.DealContainer, error) + GetResults() ([]data.Result, error) + GetMatchDecisions() ([]data.MatchDecision, error) GetJobOffer(id string) (*data.JobOfferContainer, error) GetResourceOffer(id string) (*data.ResourceOfferContainer, error) GetResourceOfferByAddress(address string) (*data.ResourceOfferContainer, error) @@ -76,22 +82,11 @@ type SolverStore interface { UpdateDealTransactionsMediator(id string, data data.DealTransactionsMediator) (*data.DealContainer, error) RemoveJobOffer(id string) error RemoveResourceOffer(id string) error + RemoveDeal(id string) error + RemoveResult(id string) error + RemoveMatchDecision(resourceOffer string, jobOffer string) error } func GetMatchID(resourceOffer string, jobOffer string) string { return fmt.Sprintf("%s-%s", resourceOffer, jobOffer) } - -func GetLogWriters(kinds []string) (map[string]jsonl.Writer, error) { - logWriters := make(map[string]jsonl.Writer) - - for k := range kinds { - logfile, err := os.OpenFile(fmt.Sprintf("/var/tmp/lilypad_%s.jsonl", kinds[k]), os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644) - if err != nil { - return nil, err - } - logWriters[kinds[k]] = jsonl.NewWriter(logfile) - } - - return logWriters, nil -} diff --git a/pkg/solver/store/store_test.go b/pkg/solver/store/store_test.go new file mode 100644 index 00000000..e06bdaea --- /dev/null +++ b/pkg/solver/store/store_test.go @@ -0,0 +1,1588 @@ +//go:build integration && solver + +package store_test + +import ( + "fmt" + "slices" + "sort" + "sync" + "testing" + + "github.com/lilypad-tech/lilypad/pkg/data" + "github.com/lilypad-tech/lilypad/pkg/solver/store" + solverstore "github.com/lilypad-tech/lilypad/pkg/solver/store" + databasestore "github.com/lilypad-tech/lilypad/pkg/solver/store/db" + memorystore "github.com/lilypad-tech/lilypad/pkg/solver/store/memory" + "golang.org/x/exp/rand" +) + +const DB_CONN_STR = "postgres://postgres:postgres@localhost:5432/solver-db?sslmode=disable" + +// Job offers + +func TestJobOfferOps(t *testing.T) { + storeConfigs := setupStores(t) + for _, config := range storeConfigs { + // Test multiple job offers in a single test run + t.Run(config.name, func(t *testing.T) { + getStore, clearStore := config.init() + store := getStore() + defer clearStore() + + // Generate multiple job offers + jobOffers := generateJobOffers(5, 50) + + for _, jobOffer := range jobOffers { + // Add job offer + added, err := store.AddJobOffer(jobOffer) + if err != nil { + t.Fatalf("Failed to add job offer: %v", err) + } + if added.ID != jobOffer.ID { + t.Errorf("Expected ID %s, got %s", jobOffer.ID, added.ID) + } + + // Get job offer + retrieved, err := store.GetJobOffer(jobOffer.ID) + if err != nil { + t.Fatalf("Failed to get job offer: %v", err) + } + if retrieved == nil { + t.Fatalf("Expected job offer, got nil") + } + if retrieved.ID != jobOffer.ID { + t.Errorf("Expected ID %s, got %s", jobOffer.ID, retrieved.ID) + } + + // Update job offer + newDealID := generateCID() + newState := generateState() + + updated, err := store.UpdateJobOfferState(jobOffer.ID, newDealID, newState) + if err != nil { + t.Fatalf("Failed to update job offer state: %v", err) + } + if updated.DealID != newDealID || updated.State != newState { + t.Errorf("Update failed: expected dealID=%s state=%d, got dealID=%s state=%d", + newDealID, newState, updated.DealID, updated.State) + } + + // Remove job offer + err = store.RemoveJobOffer(jobOffer.ID) + if err != nil { + t.Fatalf("Failed to remove job offer: %v", err) + } + + // Verify removal + removed, err := store.GetJobOffer(jobOffer.ID) + if err != nil { + t.Fatalf("Error checking removed job offer: %v", err) + } + if removed != nil { + t.Error("Job offer still exists after removal") + } + } + }) + } +} + +func TestJobOfferQuery(t *testing.T) { + // Test cases set offer fields relevant to querying. + // All other fields are left with their zero-values. + testCases := []struct { + name string + offers []data.JobOfferContainer + query store.GetJobOffersQuery + expected []string // expected IDs in results + }{ + { + name: "filter by job creator", + offers: []data.JobOfferContainer{ + { + ID: "QmY8JwJh3bYDUuAnwfpxwStjUY1nQwyhJJ4SPpdV3bZ9Kx", + JobCreator: "0x1234567890123456789012345678901234567890", + DealID: "", + State: 0, + }, + { + ID: "QmX9JwJh3bYDUuAnwfpxwStjUY1nQwyhJJ4SPpdV3bZ9Ky", + JobCreator: "0xabcdef0123456789abcdef0123456789abcdef01", + DealID: "", + State: 0, + }, + }, + query: store.GetJobOffersQuery{ + JobCreator: "0x1234567890123456789012345678901234567890", + }, + expected: []string{"QmY8JwJh3bYDUuAnwfpxwStjUY1nQwyhJJ4SPpdV3bZ9Kx"}, + }, + { + name: "filter not matched offers", + offers: []data.JobOfferContainer{ + { + ID: "QmY8JwJh3bYDUuAnwfpxwStjUY1nQwyhJJ4SPpdV3bZ9Kx", + JobCreator: "0x1234567890123456789012345678901234567890", + DealID: "QmZ9JwJh3bYDUuAnwfpxwStjUY1nQwyhJJ4SPpdV3bZ9Kz", + State: 0, + }, + { + ID: "QmX9JwJh3bYDUuAnwfpxwStjUY1nQwyhJJ4SPpdV3bZ9Ky", + JobCreator: "0x1234567890123456789012345678901234567890", + DealID: "", + State: 0, + }, + }, + query: store.GetJobOffersQuery{ + NotMatched: true, + }, + expected: []string{"QmX9JwJh3bYDUuAnwfpxwStjUY1nQwyhJJ4SPpdV3bZ9Ky"}, + }, + { + name: "filter out cancelled offers", + offers: []data.JobOfferContainer{ + { + ID: "QmY8JwJh3bYDUuAnwfpxwStjUY1nQwyhJJ4SPpdV3bZ9Kx", + JobCreator: "0x1234567890123456789012345678901234567890", + DealID: "", + State: data.GetDefaultAgreementState(), + }, + { + ID: "QmX9JwJh3bYDUuAnwfpxwStjUY1nQwyhJJ4SPpdV3bZ9Ky", + JobCreator: "0x1234567890123456789012345678901234567890", + DealID: "", + State: data.GetAgreementStateIndex("JobOfferCancelled"), + }, + }, + query: store.GetJobOffersQuery{ + IncludeCancelled: false, + }, + expected: []string{"QmY8JwJh3bYDUuAnwfpxwStjUY1nQwyhJJ4SPpdV3bZ9Kx"}, + }, + { + name: "include cancelled offers", + offers: []data.JobOfferContainer{ + { + ID: "QmY8JwJh3bYDUuAnwfpxwStjUY1nQwyhJJ4SPpdV3bZ9Kx", + JobCreator: "0x1234567890123456789012345678901234567890", + DealID: "", + State: data.GetDefaultAgreementState(), + }, + { + ID: "QmX9JwJh3bYDUuAnwfpxwStjUY1nQwyhJJ4SPpdV3bZ9Ky", + JobCreator: "0x1234567890123456789012345678901234567890", + DealID: "", + State: data.GetAgreementStateIndex("JobOfferCancelled"), + }, + }, + query: store.GetJobOffersQuery{ + IncludeCancelled: true, + }, + expected: []string{ + "QmY8JwJh3bYDUuAnwfpxwStjUY1nQwyhJJ4SPpdV3bZ9Kx", + "QmX9JwJh3bYDUuAnwfpxwStjUY1nQwyhJJ4SPpdV3bZ9Ky", + }, + }, + { + name: "combined filters", + offers: []data.JobOfferContainer{ + { + ID: "QmY8JwJh3bYDUuAnwfpxwStjUY1nQwyhJJ4SPpdV3bZ9Kx", + JobCreator: "0x1234567890123456789012345678901234567890", + DealID: "", + State: data.GetDefaultAgreementState(), + }, + { + ID: "QmX9JwJh3bYDUuAnwfpxwStjUY1nQwyhJJ4SPpdV3bZ9Ky", + JobCreator: "0xabcdef0123456789abcdef0123456789abcdef01", + DealID: "", + State: data.GetDefaultAgreementState(), + }, + { + ID: "QmZ9JwJh3bYDUuAnwfpxwStjUY1nQwyhJJ4SPpdV3bZ9Kz", + JobCreator: "0x1234567890123456789012345678901234567890", + DealID: "QmW9JwJh3bYDUuAnwfpxwStjUY1nQwyhJJ4SPpdV3bZ9Kw", + State: data.GetDefaultAgreementState(), + }, + { + ID: "QmV9JwJh3bYDUuAnwfpxwStjUY1nQwyhJJ4SPpdV3bZ9Kv", + JobCreator: "0x1234567890123456789012345678901234567890", + DealID: "", + State: data.GetAgreementStateIndex("JobOfferCancelled"), + }, + }, + query: store.GetJobOffersQuery{ + JobCreator: "0x1234567890123456789012345678901234567890", + NotMatched: true, + IncludeCancelled: false, + }, + expected: []string{"QmY8JwJh3bYDUuAnwfpxwStjUY1nQwyhJJ4SPpdV3bZ9Kx"}, + }, + } + + storeConfigs := setupStores(t) + for _, config := range storeConfigs { + getStore, clearStore := config.init() + defer clearStore() + + for _, tc := range testCases { + // Test each case in a separate test run + t.Run(fmt.Sprintf("%s/%s", config.name, tc.name), func(t *testing.T) { + store := getStore() + + // Add test job offers + for _, jo := range tc.offers { + _, err := store.AddJobOffer(jo) + if err != nil { + t.Fatalf("Failed to add job offer: %v", err) + } + } + + // Run query + results, err := store.GetJobOffers(tc.query) + if err != nil { + t.Fatalf("GetJobOffers failed: %v", err) + } + + // Extract result IDs + resultIDs := make([]string, len(results)) + for i, r := range results { + resultIDs[i] = r.ID + } + + // Sort both slices for comparison + sort.Strings(resultIDs) + sort.Strings(tc.expected) + + if !slices.Equal(resultIDs, tc.expected) { + t.Errorf("Expected results %v, got %v", tc.expected, resultIDs) + } + }) + } + } +} + +// Resource Offer + +func TestResourceOfferOps(t *testing.T) { + storeConfigs := setupStores(t) + for _, config := range storeConfigs { + // Test multiple resource offers in a single test run + t.Run(config.name, func(t *testing.T) { + getStore, clearStore := config.init() + store := getStore() + defer clearStore() + + // Generate multiple resource offers + resourceOffers := generateResourceOffers(5, 50) + + for _, resourceOffer := range resourceOffers { + // Add resource offer + added, err := store.AddResourceOffer(resourceOffer) + if err != nil { + t.Fatalf("Failed to add resource offer: %v", err) + } + if added.ID != resourceOffer.ID { + t.Errorf("Expected ID %s, got %s", resourceOffer.ID, added.ID) + } + + // Get resource offer + retrieved, err := store.GetResourceOffer(resourceOffer.ID) + if err != nil { + t.Fatalf("Failed to get resource offer: %v", err) + } + if retrieved == nil { + t.Fatalf("Expected resource offer, got nil") + } + if retrieved.ID != resourceOffer.ID { + t.Errorf("Expected ID %s, got %s", resourceOffer.ID, retrieved.ID) + } + + // Get resource offer by address + byAddress, err := store.GetResourceOfferByAddress(resourceOffer.ResourceProvider) + if err != nil { + t.Fatalf("Failed to get resource offer by address: %v", err) + } + if byAddress == nil { + t.Fatalf("Expected resource offer by address, got nil") + } + if byAddress.ResourceProvider != resourceOffer.ResourceProvider { + t.Errorf("Expected provider %s, got %s", resourceOffer.ResourceProvider, byAddress.ResourceProvider) + } + + // Update resource offer + newDealID := generateCID() + newState := generateState() + + updated, err := store.UpdateResourceOfferState(resourceOffer.ID, newDealID, newState) + if err != nil { + t.Fatalf("Failed to update resource offer state: %v", err) + } + if updated.DealID != newDealID || updated.State != newState { + t.Errorf("Update failed: expected dealID=%s state=%d, got dealID=%s state=%d", + newDealID, newState, updated.DealID, updated.State) + } + + // Remove resource offer + err = store.RemoveResourceOffer(resourceOffer.ID) + if err != nil { + t.Fatalf("Failed to remove resource offer: %v", err) + } + + // Verify removal + removed, err := store.GetResourceOffer(resourceOffer.ID) + if err != nil { + t.Fatalf("Error checking removed resource offer: %v", err) + } + if removed != nil { + t.Error("Resource offer still exists after removal") + } + + // Verify removal by address + removedByAddr, err := store.GetResourceOfferByAddress(resourceOffer.ResourceProvider) + if err != nil { + t.Fatalf("Error checking removed resource offer by address: %v", err) + } + if removedByAddr != nil { + t.Error("Resource offer still exists after removal when checking by address") + } + } + }) + } +} + +func TestResourceOfferQuery(t *testing.T) { + // Test cases set offer fields relevant to querying. + // All other fields are left with their zero-values. + testCases := []struct { + name string + offers []data.ResourceOfferContainer + query store.GetResourceOffersQuery + expected []string // expected IDs in results + }{ + { + name: "filter by resource provider", + offers: []data.ResourceOfferContainer{ + { + ID: "QmY8JwJh3bYDUuAnwfpxwStjUY1nQwyhJJ4SPpdV3bZ9Kx", + ResourceProvider: "0x1234567890123456789012345678901234567890", + DealID: "", + State: data.GetDefaultAgreementState(), + }, + { + ID: "QmX9JwJh3bYDUuAnwfpxwStjUY1nQwyhJJ4SPpdV3bZ9Ky", + ResourceProvider: "0xabcdef0123456789abcdef0123456789abcdef01", + DealID: "", + State: data.GetDefaultAgreementState(), + }, + }, + query: store.GetResourceOffersQuery{ + ResourceProvider: "0x1234567890123456789012345678901234567890", + }, + expected: []string{"QmY8JwJh3bYDUuAnwfpxwStjUY1nQwyhJJ4SPpdV3bZ9Kx"}, + }, + { + name: "filter not matched offers", + offers: []data.ResourceOfferContainer{ + { + ID: "QmY8JwJh3bYDUuAnwfpxwStjUY1nQwyhJJ4SPpdV3bZ9Kx", + ResourceProvider: "0x1234567890123456789012345678901234567890", + DealID: "QmZ9JwJh3bYDUuAnwfpxwStjUY1nQwyhJJ4SPpdV3bZ9Kz", + State: data.GetDefaultAgreementState(), + }, + { + ID: "QmX9JwJh3bYDUuAnwfpxwStjUY1nQwyhJJ4SPpdV3bZ9Ky", + ResourceProvider: "0x1234567890123456789012345678901234567890", + DealID: "", + State: data.GetDefaultAgreementState(), + }, + }, + query: store.GetResourceOffersQuery{ + NotMatched: true, + }, + expected: []string{"QmX9JwJh3bYDUuAnwfpxwStjUY1nQwyhJJ4SPpdV3bZ9Ky"}, + }, + { + name: "filter active offers", + offers: []data.ResourceOfferContainer{ + { + ID: "QmY8JwJh3bYDUuAnwfpxwStjUY1nQwyhJJ4SPpdV3bZ9Kx", + ResourceProvider: "0x1234567890123456789012345678901234567890", + DealID: "", + State: data.GetAgreementStateIndex("DealNegotiating"), + }, + { + ID: "QmX9JwJh3bYDUuAnwfpxwStjUY1nQwyhJJ4SPpdV3bZ9Ky", + ResourceProvider: "0x1234567890123456789012345678901234567890", + DealID: "", + State: data.GetAgreementStateIndex("DealAgreed"), + }, + { + ID: "QmZ9JwJh3bYDUuAnwfpxwStjUY1nQwyhJJ4SPpdV3bZ9Kz", + ResourceProvider: "0x1234567890123456789012345678901234567890", + DealID: "", + State: data.GetAgreementStateIndex("ResultsSubmitted"), + }, + { + ID: "QmV9JwJh3bYDUuAnwfpxwStjUY1nQwyhJJ4SPpdV3bZ9Kv", + ResourceProvider: "0x1234567890123456789012345678901234567890", + DealID: "", + State: data.GetAgreementStateIndex("ResultsAccepted"), + }, + }, + query: store.GetResourceOffersQuery{ + Active: true, + }, + expected: []string{ + "QmY8JwJh3bYDUuAnwfpxwStjUY1nQwyhJJ4SPpdV3bZ9Kx", + "QmX9JwJh3bYDUuAnwfpxwStjUY1nQwyhJJ4SPpdV3bZ9Ky", + }, + }, + { + name: "combined filters", + offers: []data.ResourceOfferContainer{ + { + ID: "QmY8JwJh3bYDUuAnwfpxwStjUY1nQwyhJJ4SPpdV3bZ9Kx", + ResourceProvider: "0x1234567890123456789012345678901234567890", + DealID: "", + State: data.GetAgreementStateIndex("DealNegotiating"), + }, + { + ID: "QmX9JwJh3bYDUuAnwfpxwStjUY1nQwyhJJ4SPpdV3bZ9Ky", + ResourceProvider: "0xabcdef0123456789abcdef0123456789abcdef01", + DealID: "", + State: data.GetAgreementStateIndex("DealNegotiating"), + }, + { + ID: "QmZ9JwJh3bYDUuAnwfpxwStjUY1nQwyhJJ4SPpdV3bZ9Kz", + ResourceProvider: "0x1234567890123456789012345678901234567890", + DealID: "QmW9JwJh3bYDUuAnwfpxwStjUY1nQwyhJJ4SPpdV3bZ9Kw", + State: data.GetAgreementStateIndex("DealNegotiating"), + }, + { + ID: "QmV9JwJh3bYDUuAnwfpxwStjUY1nQwyhJJ4SPpdV3bZ9Kv", + ResourceProvider: "0x1234567890123456789012345678901234567890", + DealID: "", + State: data.GetAgreementStateIndex("ResultsSubmitted"), + }, + }, + query: store.GetResourceOffersQuery{ + ResourceProvider: "0x1234567890123456789012345678901234567890", + NotMatched: true, + Active: true, + }, + expected: []string{"QmY8JwJh3bYDUuAnwfpxwStjUY1nQwyhJJ4SPpdV3bZ9Kx"}, + }, + } + + storeConfigs := setupStores(t) + for _, config := range storeConfigs { + getStore, clearStore := config.init() + defer clearStore() + + for _, tc := range testCases { + // Test each case in a separate test run + t.Run(fmt.Sprintf("%s/%s", config.name, tc.name), func(t *testing.T) { + store := getStore() + + // Add test resource offers + for _, ro := range tc.offers { + _, err := store.AddResourceOffer(ro) + if err != nil { + t.Fatalf("Failed to add resource offer: %v", err) + } + } + + // Run query + results, err := store.GetResourceOffers(tc.query) + if err != nil { + t.Fatalf("GetResourceOffers failed: %v", err) + } + + // Extract result IDs + resultIDs := make([]string, len(results)) + for i, r := range results { + resultIDs[i] = r.ID + } + + // Sort both slices for comparison + sort.Strings(resultIDs) + sort.Strings(tc.expected) + + if !slices.Equal(resultIDs, tc.expected) { + t.Errorf("Expected results %v, got %v", tc.expected, resultIDs) + } + }) + } + } +} + +// Deals + +func TestDealOps(t *testing.T) { + storeConfigs := setupStores(t) + for _, config := range storeConfigs { + // Test multiple deals in a single test run + t.Run(config.name, func(t *testing.T) { + getStore, clearStore := config.init() + store := getStore() + defer clearStore() + + // Generate multiple deals + deals := generateDeals(5, 50) + + for _, deal := range deals { + // Add deal + added, err := store.AddDeal(deal) + if err != nil { + t.Fatalf("Failed to add deal: %v", err) + } + if added.ID != deal.ID { + t.Errorf("Expected ID %s, got %s", deal.ID, added.ID) + } + + // Get deal + retrieved, err := store.GetDeal(deal.ID) + if err != nil { + t.Fatalf("Failed to get deal: %v", err) + } + if retrieved == nil { + t.Fatalf("Expected deal, got nil") + } + if retrieved.ID != deal.ID { + t.Errorf("Expected ID %s, got %s", deal.ID, retrieved.ID) + } + + // Remove deal + err = store.RemoveDeal(deal.ID) + if err != nil { + t.Fatalf("Failed to remove deal: %v", err) + } + + // Verify removal + removed, err := store.GetDeal(deal.ID) + if err != nil { + t.Fatalf("Error checking removed deal: %v", err) + } + if removed != nil { + t.Error("Deal still exists after removal") + } + } + }) + } +} + +func TestDealGetAll(t *testing.T) { + storeConfigs := setupStores(t) + for _, config := range storeConfigs { + // Test batch of deals in a test run + t.Run(config.name, func(t *testing.T) { + getStore, clearStore := config.init() + store := getStore() + defer clearStore() + + // Generate multiple deals or no deals + deals := generateDeals(0, 10) + addedIDs := make([]string, len(deals)) + + // Add deals + for i, deal := range deals { + added, err := store.AddDeal(deal) + if err != nil { + t.Fatalf("Failed to add deal: %v", err) + } + addedIDs[i] = added.ID + } + + // Get all deals + allDeals, err := store.GetDealsAll() + if err != nil { + t.Fatalf("Failed to get all deals: %v", err) + } + + // Verify count matches + if len(allDeals) != len(deals) { + t.Errorf("Expected %d deals, got %d", len(deals), len(allDeals)) + } + + // Verify all added deals are present + retrievedIDs := make([]string, len(allDeals)) + for i, deal := range allDeals { + retrievedIDs[i] = deal.ID + } + + // Sort both slices for comparison + sort.Strings(addedIDs) + sort.Strings(retrievedIDs) + + if !slices.Equal(retrievedIDs, addedIDs) { + t.Errorf("Retrieved deals don't match added deals.\nAdded: %v\nRetrieved: %v", + addedIDs, retrievedIDs) + } + }) + } +} + +func TestDealUpdates(t *testing.T) { + storeConfigs := setupStores(t) + for _, config := range storeConfigs { + // Test multiple deals in a single test run + t.Run(config.name, func(t *testing.T) { + getStore, clearStore := config.init() + store := getStore() + defer clearStore() + + // Generate multiple deals + deals := generateDeals(5, 50) + + for _, deal := range deals { + // Add deal + added, err := store.AddDeal(deal) + if err != nil { + t.Fatalf("Failed to add deal: %v", err) + } + + // Update deal state + newState := generateState() + updated, err := store.UpdateDealState(added.ID, newState) + if err != nil { + t.Fatalf("Failed to update deal state: %v", err) + } + if updated.State != newState { + t.Errorf("Update state failed: expected state=%d, got state=%d", + newState, updated.State) + } + + // Update deal mediator + newMediator := generateEthAddress() + updated, err = store.UpdateDealMediator(added.ID, newMediator) + if err != nil { + t.Fatalf("Failed to update deal mediator: %v", err) + } + if updated.Mediator != newMediator { + t.Errorf("Update mediator failed: expected mediator=%s, got mediator=%s", + newMediator, updated.Mediator) + } + + // Update deal job creator transactions + jcTxs := data.DealTransactionsJobCreator{ + Agree: generateEthTxHash(), + AcceptResult: generateEthTxHash(), + CheckResult: generateEthTxHash(), + TimeoutAgree: generateEthTxHash(), + TimeoutSubmitResult: generateEthTxHash(), + TimeoutMediateResult: generateEthTxHash(), + } + updated, err = store.UpdateDealTransactionsJobCreator(added.ID, jcTxs) + if err != nil { + t.Fatalf("Failed to update job creator transactions: %v", err) + } + if updated.Transactions.JobCreator != jcTxs { + t.Error("Job creator transactions not updated correctly") + } + + // Update deal transactions resource provider + rpTxs := data.DealTransactionsResourceProvider{ + Agree: generateEthTxHash(), + AddResult: generateEthTxHash(), + TimeoutAgree: generateEthTxHash(), + TimeoutJudgeResult: generateEthTxHash(), + TimeoutMediateResult: generateEthTxHash(), + } + updated, err = store.UpdateDealTransactionsResourceProvider(added.ID, rpTxs) + if err != nil { + t.Fatalf("Failed to update resource provider transactions: %v", err) + } + if updated.Transactions.ResourceProvider != rpTxs { + t.Error("Resource provider transactions not updated correctly") + } + + // Update deal transactions mediator + mediatorTxs := data.DealTransactionsMediator{ + MediationAcceptResult: generateEthTxHash(), + MediationRejectResult: generateEthTxHash(), + } + updatedMediatorTxs, err := store.UpdateDealTransactionsMediator(added.ID, mediatorTxs) + if err != nil { + t.Fatalf("Failed to update mediator transactions: %v", err) + } + if updatedMediatorTxs.Transactions.Mediator != mediatorTxs { + t.Error("Mediator transactions not updated correctly") + } + } + }) + } +} + +func TestDealQuery(t *testing.T) { + // Test cases set deal fields relevant to querying. + // All other fields are left with their zero-values. + testCases := []struct { + name string + deals []data.DealContainer + query store.GetDealsQuery + expected []string // expected IDs in results + }{ + { + name: "filter by job creator", + deals: []data.DealContainer{ + { + ID: "QmY8JwJh3bYDUuAnwfpxwStjUY1nQwyhJJ4SPpdV3bZ9Kx", + JobCreator: "0x1234567890123456789012345678901234567890", + State: data.GetDefaultAgreementState(), + }, + { + ID: "QmX9JwJh3bYDUuAnwfpxwStjUY1nQwyhJJ4SPpdV3bZ9Ky", + JobCreator: "0xabcdef0123456789abcdef0123456789abcdef01", + State: data.GetDefaultAgreementState(), + }, + }, + query: store.GetDealsQuery{ + JobCreator: "0x1234567890123456789012345678901234567890", + }, + expected: []string{"QmY8JwJh3bYDUuAnwfpxwStjUY1nQwyhJJ4SPpdV3bZ9Kx"}, + }, + { + name: "filter by resource provider", + deals: []data.DealContainer{ + { + ID: "QmY8JwJh3bYDUuAnwfpxwStjUY1nQwyhJJ4SPpdV3bZ9Kx", + ResourceProvider: "0x1234567890123456789012345678901234567890", + State: data.GetDefaultAgreementState(), + }, + { + ID: "QmX9JwJh3bYDUuAnwfpxwStjUY1nQwyhJJ4SPpdV3bZ9Ky", + ResourceProvider: "0xabcdef0123456789abcdef0123456789abcdef01", + State: data.GetDefaultAgreementState(), + }, + }, + query: store.GetDealsQuery{ + ResourceProvider: "0x1234567890123456789012345678901234567890", + }, + expected: []string{"QmY8JwJh3bYDUuAnwfpxwStjUY1nQwyhJJ4SPpdV3bZ9Kx"}, + }, + { + name: "filter by mediator", + deals: []data.DealContainer{ + { + ID: "QmY8JwJh3bYDUuAnwfpxwStjUY1nQwyhJJ4SPpdV3bZ9Kx", + Mediator: "0x1234567890123456789012345678901234567890", + State: data.GetDefaultAgreementState(), + }, + { + ID: "QmX9JwJh3bYDUuAnwfpxwStjUY1nQwyhJJ4SPpdV3bZ9Ky", + Mediator: "0xabcdef0123456789abcdef0123456789abcdef01", + State: data.GetDefaultAgreementState(), + }, + }, + query: store.GetDealsQuery{ + Mediator: "0x1234567890123456789012345678901234567890", + }, + expected: []string{"QmY8JwJh3bYDUuAnwfpxwStjUY1nQwyhJJ4SPpdV3bZ9Kx"}, + }, + { + name: "filter by state", + deals: []data.DealContainer{ + { + ID: "QmY8JwJh3bYDUuAnwfpxwStjUY1nQwyhJJ4SPpdV3bZ9Kx", + State: data.GetAgreementStateIndex("DealNegotiating"), + }, + { + ID: "QmX9JwJh3bYDUuAnwfpxwStjUY1nQwyhJJ4SPpdV3bZ9Ky", + State: data.GetAgreementStateIndex("DealAgreed"), + }, + }, + query: store.GetDealsQuery{ + State: "DealNegotiating", + }, + expected: []string{"QmY8JwJh3bYDUuAnwfpxwStjUY1nQwyhJJ4SPpdV3bZ9Kx"}, + }, + { + name: "combined filters", + deals: []data.DealContainer{ + { + ID: "QmY8JwJh3bYDUuAnwfpxwStjUY1nQwyhJJ4SPpdV3bZ9Kx", + JobCreator: "0x1234567890123456789012345678901234567890", + ResourceProvider: "0x2234567890123456789012345678901234567890", + State: data.GetAgreementStateIndex("DealNegotiating"), + }, + { + ID: "QmX9JwJh3bYDUuAnwfpxwStjUY1nQwyhJJ4SPpdV3bZ9Ky", + JobCreator: "0x1234567890123456789012345678901234567890", + ResourceProvider: "0x2234567890123456789012345678901234567890", + State: data.GetAgreementStateIndex("DealAgreed"), + }, + { + ID: "QmZ9JwJh3bYDUuAnwfpxwStjUY1nQwyhJJ4SPpdV3bZ9Kz", + JobCreator: "0x1234567890123456789012345678901234567890", + ResourceProvider: "0x3234567890123456789012345678901234567890", + State: data.GetAgreementStateIndex("DealNegotiating"), + }, + }, + query: store.GetDealsQuery{ + JobCreator: "0x1234567890123456789012345678901234567890", + ResourceProvider: "0x2234567890123456789012345678901234567890", + State: "DealNegotiating", + }, + expected: []string{"QmY8JwJh3bYDUuAnwfpxwStjUY1nQwyhJJ4SPpdV3bZ9Kx"}, + }, + } + + storeConfigs := setupStores(t) + for _, config := range storeConfigs { + getStore, clearStore := config.init() + defer clearStore() + + for _, tc := range testCases { + // Test each case in a separate test run + t.Run(fmt.Sprintf("%s/%s", config.name, tc.name), func(t *testing.T) { + store := getStore() + + // Add deals + for _, deal := range tc.deals { + _, err := store.AddDeal(deal) + if err != nil { + t.Fatalf("Failed to add deal: %v", err) + } + } + + // Run query + results, err := store.GetDeals(tc.query) + if err != nil { + t.Fatalf("GetDeals failed: %v", err) + } + + // Extract result IDs + resultIDs := make([]string, len(results)) + for i, r := range results { + resultIDs[i] = r.ID + } + + // Sort both slices for comparison + sort.Strings(resultIDs) + sort.Strings(tc.expected) + + if !slices.Equal(resultIDs, tc.expected) { + t.Errorf("Expected results %v, got %v", tc.expected, resultIDs) + } + }) + } + } +} + +// Results + +func TestResultOps(t *testing.T) { + storeConfigs := setupStores(t) + for _, config := range storeConfigs { + // Test multiple results in a single test run + t.Run(config.name, func(t *testing.T) { + getStore, clearStore := config.init() + store := getStore() + defer clearStore() + + // Generate multiple results + results := generateResults(5, 50) + addedResults := make(map[string]data.Result) + + // Add results + for _, result := range results { + added, err := store.AddResult(result) + if err != nil { + t.Fatalf("Failed to add result: %v", err) + } + if added.DealID != result.DealID { + t.Errorf("Expected DealID %s, got %s", result.DealID, added.DealID) + } + if added.ID != result.ID { + t.Errorf("Expected ID %s, got %s", result.ID, added.ID) + } + addedResults[result.DealID] = result + } + + // Get results + allResults, err := store.GetResults() + if err != nil { + t.Fatalf("Failed to get all results: %v", err) + } + + // Verify count matches + if len(allResults) != len(results) { + t.Errorf("Expected %d results, got %d", len(results), len(allResults)) + } + + // Verify results are present and correct + for _, result := range allResults { + original, exists := addedResults[result.DealID] + if !exists { + t.Errorf("Got unexpected result with DealID %s", result.DealID) + continue + } + if result.ID != original.ID { + t.Errorf("Result ID mismatch for DealID %s: expected %s, got %s", + result.DealID, original.ID, result.ID) + } + } + + // Test individual result operations + for _, result := range results { + // Get result by deal ID + retrieved, err := store.GetResult(result.DealID) + if err != nil { + t.Fatalf("Failed to get result: %v", err) + } + if retrieved == nil { + t.Fatalf("Expected result, got nil") + } + if retrieved.DealID != result.DealID { + t.Errorf("Expected DealID %s, got %s", result.DealID, retrieved.DealID) + } + if retrieved.ID != result.ID { + t.Errorf("Expected ID %s, got %s", result.ID, retrieved.ID) + } + + // Remove result + err = store.RemoveResult(result.DealID) + if err != nil { + t.Fatalf("Failed to remove result: %v", err) + } + + // Verify removal + removed, err := store.GetResult(result.DealID) + if err != nil { + t.Fatalf("Error checking removed result: %v", err) + } + if removed != nil { + t.Error("Result still exists after removal") + } + } + + // Verify results were removed using GetResults + finalResults, err := store.GetResults() + if err != nil { + t.Fatalf("Failed to get final results: %v", err) + } + if len(finalResults) != 0 { + t.Errorf("Expected 0 results after removal, got %d", len(finalResults)) + } + }) + } +} + +// Match decisions + +func TestMatchDecisionOps(t *testing.T) { + storeConfigs := setupStores(t) + for _, config := range storeConfigs { + // Test multiple match decisions in a single test run + t.Run(config.name, func(t *testing.T) { + getStore, clearStore := config.init() + store := getStore() + defer clearStore() + + // Generate match decisions + decisions := generateMatchDecisions(5, 50) + addedDecisions := make(map[string]*data.MatchDecision) + + // Add match decisions + for _, decision := range decisions { + // Add match decision + added, err := store.AddMatchDecision( + decision.ResourceOffer, + decision.JobOffer, + decision.Deal, + decision.Result, + ) + if err != nil { + t.Fatalf("Failed to add match decision: %v", err) + } + if added.ResourceOffer != decision.ResourceOffer { + t.Errorf("Expected ResourceOffer %s, got %s", + decision.ResourceOffer, added.ResourceOffer) + } + if added.JobOffer != decision.JobOffer { + t.Errorf("Expected JobOffer %s, got %s", + decision.JobOffer, added.JobOffer) + } + if added.Deal != decision.Deal { + t.Errorf("Expected Deal %s, got %s", + decision.Deal, added.Deal) + } + if added.Result != decision.Result { + t.Errorf("Expected Result %v, got %v", + decision.Result, added.Result) + } + + // Store for later verification + matchID := solverstore.GetMatchID(decision.ResourceOffer, decision.JobOffer) + addedDecisions[matchID] = added + } + + // Get match decisions + allDecisions, err := store.GetMatchDecisions() + if err != nil { + t.Fatalf("Failed to get all match decisions: %v", err) + } + + // Verify count matches + if len(allDecisions) != len(addedDecisions) { + t.Errorf("Expected %d match decisions, got %d", + len(addedDecisions), len(allDecisions)) + } + + // Verify decisions are present and correct + for _, decision := range allDecisions { + matchID := solverstore.GetMatchID(decision.ResourceOffer, decision.JobOffer) + original, exists := addedDecisions[matchID] + if !exists { + t.Errorf("Got unexpected match decision for resource offer %s and job offer %s", + decision.ResourceOffer, decision.JobOffer) + continue + } + // Check deal and result. JobOffer and ResourceOffer in individual checks below. + if decision.Deal != original.Deal { + t.Errorf("Match decision Deal mismatch for ID %s: expected %s, got %s", + matchID, original.Deal, decision.Deal) + } + if decision.Result != original.Result { + t.Errorf("Match decision Result mismatch for ID %s: expected %v, got %v", + matchID, original.Result, decision.Result) + } + } + + // Test individual match decision operations + for _, decision := range decisions { + // Get match decision + retrieved, err := store.GetMatchDecision( + decision.ResourceOffer, + decision.JobOffer, + ) + if err != nil { + t.Fatalf("Failed to get match decision: %v", err) + } + if retrieved == nil { + t.Fatal("Expected match decision, got nil") + } + if retrieved.ResourceOffer != decision.ResourceOffer { + t.Errorf("Expected ResourceOffer %s, got %s", + decision.ResourceOffer, retrieved.ResourceOffer) + } + if retrieved.JobOffer != decision.JobOffer { + t.Errorf("Expected JobOffer %s, got %s", + decision.JobOffer, retrieved.JobOffer) + } + + // Remove match decision by job offer + err = store.RemoveMatchDecision(decision.ResourceOffer, decision.JobOffer) + if err != nil { + t.Fatalf("Failed to remove match decision: %v", err) + } + + // Verify removal + removed, err := store.GetMatchDecision( + decision.ResourceOffer, + decision.JobOffer, + ) + if err != nil { + t.Fatalf("Error checking removed match decision: %v", err) + } + if removed != nil { + t.Error("Match decision still exists after removal") + } + } + }) + } +} + +func TestMatchDecisionRemove(t *testing.T) { + storeConfigs := setupStores(t) + for _, config := range storeConfigs { + t.Run(config.name, func(t *testing.T) { + getStore, clearStore := config.init() + store := getStore() + defer clearStore() + + matchDecision := data.MatchDecision{ + ResourceOffer: generateCID(), + JobOffer: generateCID(), + Deal: generateCID(), + Result: rand.Intn(2) == 1, // Generate random bool + } + + testCases := []struct { + name string + matchDecision data.MatchDecision + removeBy []string + }{ + { + name: "remove by job offer", + matchDecision: matchDecision, + removeBy: []string{"", matchDecision.JobOffer}, + }, + { + name: "remove by resource offer", + matchDecision: matchDecision, + removeBy: []string{matchDecision.ResourceOffer, ""}, + }, + { + name: "remove by job offer and resource offer", + matchDecision: matchDecision, + removeBy: []string{matchDecision.ResourceOffer, matchDecision.JobOffer}, + }, + } + + // Test removal by job creator, resource provider, or both + for _, tc := range testCases { + // Remove match decision + err := store.RemoveMatchDecision(tc.removeBy[0], tc.removeBy[1]) + if err != nil { + t.Fatalf("Failed to remove match decision: %v", err) + } + + // Check removal + retrieved, err := store.GetMatchDecision(tc.matchDecision.ResourceOffer, tc.matchDecision.JobOffer) + if err != nil { + t.Fatalf("Failed to get match decision: %v", err) + } + if retrieved != nil { + t.Errorf("Match decision %s shouldn't exist but does", + solverstore.GetMatchID(tc.matchDecision.ResourceOffer, tc.matchDecision.JobOffer)) + } + } + }) + } +} + +// Concurrency for all + +func TestConcurrentOps(t *testing.T) { + jobOffers := generateJobOffers(4, 10) + resourceOffers := generateResourceOffers(4, 10) + deals := generateDeals(4, 10) + results := generateResults(4, 10) + matchDecisions := generateMatchDecisions(4, 10) + + storeConfigs := setupStores(t) + for _, config := range storeConfigs { + // Test concurrent adds in a single test run + t.Run(config.name, func(t *testing.T) { + getStore, clearStore := config.init() + store := getStore() + defer clearStore() + + count := len(jobOffers) + len(resourceOffers) + len(deals) + len(results) + len(matchDecisions) + errCh := make(chan error, count) + var wg sync.WaitGroup + + // Add job offers concurrently + for _, offer := range jobOffers { + wg.Add(1) + go func(o data.JobOfferContainer) { + defer wg.Done() + _, err := store.AddJobOffer(o) + if err != nil { + errCh <- fmt.Errorf("job offer error: %v", err) + } + }(offer) + } + + // Add resource offers concurrently + for _, offer := range resourceOffers { + wg.Add(1) + go func(o data.ResourceOfferContainer) { + defer wg.Done() + _, err := store.AddResourceOffer(o) + if err != nil { + errCh <- fmt.Errorf("resource offer error: %v", err) + } + }(offer) + } + + // Add deals concurrently + for _, deal := range deals { + wg.Add(1) + go func(d data.DealContainer) { + defer wg.Done() + _, err := store.AddDeal(d) + if err != nil { + errCh <- fmt.Errorf("deal error: %v", err) + } + }(deal) + } + + // Add results concurrently + for _, result := range results { + wg.Add(1) + go func(r data.Result) { + defer wg.Done() + _, err := store.AddResult(r) + if err != nil { + errCh <- fmt.Errorf("result error: %v", err) + } + }(result) + } + + // Add match decisions concurrently + for _, decision := range matchDecisions { + wg.Add(1) + go func(d data.MatchDecision) { + defer wg.Done() + _, err := store.AddMatchDecision(d.ResourceOffer, d.JobOffer, d.Deal, d.Result) + if err != nil { + errCh <- fmt.Errorf("match decision error: %v", err) + } + }(decision) + } + + wg.Wait() + close(errCh) + + // Check for any errors during concurrent operations + for err := range errCh { + if err != nil { + t.Errorf("Concurrent operation error: %v", err) + } + } + + // Verify all job offers were added + for _, offer := range jobOffers { + retrieved, err := store.GetJobOffer(offer.ID) + if err != nil { + t.Errorf("Failed to get job offer %s: %v", offer.ID, err) + } + if retrieved == nil { + t.Errorf("Job offer %s not found after concurrent add", offer.ID) + } + if retrieved != nil && retrieved.ID != offer.ID { + t.Errorf("Retrieved job offer ID mismatch: expected %s, got %s", offer.ID, retrieved.ID) + } + } + + // Verify all resource offers were added + for _, offer := range resourceOffers { + retrieved, err := store.GetResourceOffer(offer.ID) + if err != nil { + t.Errorf("Failed to get resource offer %s: %v", offer.ID, err) + } + if retrieved == nil { + t.Errorf("Resource offer %s not found after concurrent add", offer.ID) + } + if retrieved != nil && retrieved.ID != offer.ID { + t.Errorf("Retrieved resource offer ID mismatch: expected %s, got %s", offer.ID, retrieved.ID) + } + } + + // Verify all deals were added + for _, deal := range deals { + retrieved, err := store.GetDeal(deal.ID) + if err != nil { + t.Errorf("Failed to get deal %s: %v", deal.ID, err) + } + if retrieved == nil { + t.Errorf("Deal %s not found after concurrent add", deal.ID) + } + if retrieved != nil && retrieved.ID != deal.ID { + t.Errorf("Retrieved deal ID mismatch: expected %s, got %s", deal.ID, retrieved.ID) + } + } + + // Verify all results were added + for _, result := range results { + retrieved, err := store.GetResult(result.DealID) + if err != nil { + t.Errorf("Failed to get result for deal %s: %v", result.DealID, err) + } + if retrieved == nil { + t.Errorf("Result for deal ID %s not found after concurrent add", result.DealID) + } + if retrieved != nil { + if retrieved.DealID != result.DealID { + t.Errorf("Retrieved result DealID mismatch: expected %s, got %s", result.DealID, retrieved.DealID) + } + if retrieved.ID != result.ID { + t.Errorf("Retrieved result ID mismatch: expected %s, got %s", result.ID, retrieved.ID) + } + } + } + + // Verify all match decisions were added + for _, decision := range matchDecisions { + retrieved, err := store.GetMatchDecision(decision.ResourceOffer, decision.JobOffer) + if err != nil { + t.Errorf("Failed to get match decision for resource offer %s and job offer %s: %v", + decision.ResourceOffer, decision.JobOffer, err) + } + if retrieved == nil { + t.Errorf("Match decision for resource offer %s and job offer %s not found after concurrent add", + decision.ResourceOffer, decision.JobOffer) + } + if retrieved != nil { + if retrieved.ResourceOffer != decision.ResourceOffer { + t.Errorf("Retrieved match decision ResourceOffer mismatch: expected %s, got %s", + decision.ResourceOffer, retrieved.ResourceOffer) + } + if retrieved.JobOffer != decision.JobOffer { + t.Errorf("Retrieved match decision JobOffer mismatch: expected %s, got %s", + decision.JobOffer, retrieved.JobOffer) + } + if retrieved.Deal != decision.Deal { + t.Errorf("Retrieved match decision Deal mismatch: expected %s, got %s", + decision.Deal, retrieved.Deal) + } + if retrieved.Result != decision.Result { + t.Errorf("Retrieved match decision Result mismatch: expected %v, got %v", + decision.Result, retrieved.Result) + } + } + } + + }) + } +} + +// Utilities + +type storeConfig struct { + name string + init func() (getStore func() store.SolverStore, clearStore func()) +} + +func setupStores(t *testing.T) []storeConfig { + initMemory := func() (func() store.SolverStore, func()) { + // Get store function creates a new memory store + // which effectively clears data between runs + getStore := func() store.SolverStore { + s, err := memorystore.NewSolverStoreMemory() + if err != nil { + t.Fatalf("Failed to create memory store: %v", err) + } + return s + } + clearStore := func() {} + + return getStore, clearStore + } + + initDatabase := func() (func() store.SolverStore, func()) { + db, err := databasestore.NewSolverStoreDatabase(DB_CONN_STR, "silent") + if err != nil { + t.Fatalf("Failed to create database store: %v", err) + } + + // Clear data at initialization + clearStoreDatabase(t, db) + + // Get store functions clear data and returns + // the same store instance + getStore := func() store.SolverStore { + clearStoreDatabase(t, db) + return db + } + clearStore := func() { clearStoreDatabase(t, db) } + + return getStore, clearStore + } + + return []storeConfig{ + {name: "memory", init: initMemory}, + {name: "database", init: initDatabase}, + } +} + +func clearStoreDatabase(t *testing.T, s store.SolverStore) { + // Delete job offers + jobOffers, err := s.GetJobOffers(store.GetJobOffersQuery{ + IncludeCancelled: true, + }) + if err != nil { + t.Fatalf("Failed to get existing job offers: %v", err) + } + + for _, result := range jobOffers { + err := s.RemoveJobOffer(result.ID) + if err != nil { + t.Fatalf("Failed to remove existing job offer: %v", err) + } + } + + // Delete resource offers + resourceOffers, err := s.GetResourceOffers(store.GetResourceOffersQuery{}) + if err != nil { + t.Fatalf("Failed to get existing resource offers: %v", err) + } + + for _, result := range resourceOffers { + err := s.RemoveResourceOffer(result.ID) + if err != nil { + t.Fatalf("Failed to remove existing resource offer: %v", err) + } + } + + // Delete deals + deals, err := s.GetDealsAll() + if err != nil { + t.Fatalf("Failed to get existing deals: %v", err) + } + + for _, deal := range deals { + err := s.RemoveDeal(deal.ID) + if err != nil { + t.Fatalf("Failed to remove existing deal: %v", err) + } + } + + // Delete results + results, err := s.GetResults() + if err != nil { + t.Fatalf("Failed to get existing results: %v", err) + } + + for _, result := range results { + err := s.RemoveResult(result.DealID) + if err != nil { + t.Fatalf("Failed to remove existing result: %v", err) + } + } + + // Delete match decisions + decisions, err := s.GetMatchDecisions() + if err != nil { + t.Fatalf("Failed to get existing match decisions: %v", err) + } + + for _, decision := range decisions { + err := s.RemoveMatchDecision(decision.ResourceOffer, decision.JobOffer) + if err != nil { + t.Fatalf("Failed to remove existing match decision: %v", err) + } + } +} + +// Generators + +func generateCID() string { + randBytes := make([]byte, 22) + rand.Read(randBytes) + + // Mock CIDv0 + return fmt.Sprintf("Qm%44x", randBytes) +} + +func generateEthAddress() string { + randBytes := make([]byte, 20) + rand.Read(randBytes) + + // Mock eth address + return fmt.Sprintf("0x%40x", randBytes) +} + +func generateEthTxHash() string { + randBytes := make([]byte, 32) + rand.Read(randBytes) + + // Mock eth transaction hash + return fmt.Sprintf("0x%064x", randBytes) +} + +func generateState() uint8 { + return uint8(rand.Intn(len(data.AgreementState))) +} + +func generateJobOffer() data.JobOfferContainer { + return data.JobOfferContainer{ + ID: generateCID(), + } +} + +func generateJobOffers(min int, max int) []data.JobOfferContainer { + count := min + rand.Intn(max-min+1) + offers := make([]data.JobOfferContainer, count) + + for i := 0; i < count; i++ { + offers[i] = generateJobOffer() + } + + return offers +} + +func generateResourceOffer() data.ResourceOfferContainer { + return data.ResourceOfferContainer{ + ID: generateCID(), + ResourceProvider: generateEthAddress(), + } +} + +func generateResourceOffers(min int, max int) []data.ResourceOfferContainer { + count := min + rand.Intn(max-min+1) + offers := make([]data.ResourceOfferContainer, count) + + for i := 0; i < count; i++ { + offers[i] = generateResourceOffer() + } + + return offers +} + +func generateDeal() data.DealContainer { + return data.DealContainer{ + ID: generateCID(), + JobCreator: generateEthAddress(), + ResourceProvider: generateEthAddress(), + Mediator: generateEthAddress(), + State: generateState(), + Transactions: data.DealTransactions{ + JobCreator: data.DealTransactionsJobCreator{}, + ResourceProvider: data.DealTransactionsResourceProvider{}, + Mediator: data.DealTransactionsMediator{}, + }, + } +} + +func generateDeals(min int, max int) []data.DealContainer { + count := min + rand.Intn(max-min+1) + deals := make([]data.DealContainer, count) + + for i := 0; i < count; i++ { + deals[i] = generateDeal() + } + + return deals +} + +func generateResult() data.Result { + return data.Result{ + DealID: generateCID(), + ID: generateCID(), + } +} + +func generateResults(min int, max int) []data.Result { + count := min + rand.Intn(max-min+1) + results := make([]data.Result, count) + + for i := 0; i < count; i++ { + results[i] = generateResult() + } + + return results +} + +func generateMatchDecision() data.MatchDecision { + return data.MatchDecision{ + ResourceOffer: generateCID(), + JobOffer: generateCID(), + Deal: generateCID(), + Result: rand.Intn(2) == 1, // Generate random bool + } +} + +func generateMatchDecisions(min int, max int) []data.MatchDecision { + count := min + rand.Intn(max-min+1) + decisions := make([]data.MatchDecision, count) + + for i := 0; i < count; i++ { + decisions[i] = generateMatchDecision() + } + + return decisions +} diff --git a/stack b/stack index c7ade451..51db9439 100755 --- a/stack +++ b/stack @@ -11,6 +11,9 @@ OS_ARCH=$(uname -m | awk '{if ($0 ~ /arm64|aarch64/) print "arm64"; else if ($0 function compose-env() { export ADMIN_ADDRESS=${@:-"0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"} export DISABLE_TELEMETRY=false + export STORE_TYPE=database + export STORE_CONN_STR=postgres://postgres:postgres@localhost:5432/solver-db?sslmode=disable + export STORE_GORM_LOG_LEVEL=silent } function compose-init() { @@ -201,6 +204,9 @@ function solver() { load-local-env export WEB3_PRIVATE_KEY=${SOLVER_PRIVATE_KEY} export LOG_LEVEL=debug + export STORE_TYPE=database + export STORE_CONN_STR=postgres://postgres:postgres@localhost:5432/solver-db?sslmode=disable + export STORE_GORM_LOG_LEVEL=silent export SERVER_VALIDATION_TOKEN_SECRET=912dd001a6613632c066ca10a19254430db2986a84612882a18f838a6360880e go run . solver --network dev "$@" }