diff --git a/.env b/.env index bbfa027..81a0ae3 100644 --- a/.env +++ b/.env @@ -9,3 +9,4 @@ REFRESH_TOKEN_DURATION=24h POSTGRES_PASSWORD=secret POSTGRES_USER=root POSTGRES_DB=simple_bank +REDIS_ADDRESS=0.0.0.0:6379 \ No newline at end of file diff --git a/Makefile b/Makefile index d2a3ef4..42311fd 100644 --- a/Makefile +++ b/Makefile @@ -47,4 +47,13 @@ rerun_compose: docker compose down & docker compose up --build -.PHONY: postgres createdb dropdb migrate-up migrate-down sqlc test server mock dbdocs proto rerun_compose \ No newline at end of file +evans: + evans -r repl + +redis: + docker run --name redis -p 6379:6379 -d redis:7.2.4-alpine + +redis_healthcheck: + docker exec -it redis redis-cli ping + +.PHONY: postgres createdb dropdb migrate-up migrate-down sqlc test server mock dbdocs proto rerun_compose redis evans redis_healthcheck \ No newline at end of file diff --git a/go.mod b/go.mod index ef8d908..5e4b14e 100644 --- a/go.mod +++ b/go.mod @@ -28,9 +28,11 @@ require ( github.com/aead/chacha20poly1305 v0.0.0-20201124145622-1a5aba2a8b29 // indirect github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635 // indirect github.com/bytedance/sonic v1.10.2 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect github.com/chenzhuoyu/iasm v0.9.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/gabriel-vasile/mimetype v1.4.3 // indirect github.com/gin-contrib/sse v0.1.0 // indirect @@ -43,6 +45,7 @@ require ( github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/hcl v1.0.0 // indirect + github.com/hibiken/asynq v0.24.1 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.2.6 // indirect github.com/leodido/go-urn v1.3.0 // indirect @@ -55,6 +58,8 @@ require ( github.com/pelletier/go-toml/v2 v2.1.1 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/redis/go-redis/v9 v9.5.1 // indirect + github.com/robfig/cron/v3 v3.0.1 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect @@ -72,6 +77,7 @@ require ( golang.org/x/oauth2 v0.16.0 // indirect golang.org/x/sys v0.17.0 // indirect golang.org/x/text v0.14.0 // indirect + golang.org/x/time v0.5.0 // indirect google.golang.org/appengine v1.6.8 // indirect google.golang.org/genproto v0.0.0-20240116215550-a9fa1716bcac // indirect gopkg.in/ini.v1 v1.67.0 // indirect diff --git a/go.sum b/go.sum index 637efbb..78782ce 100644 --- a/go.sum +++ b/go.sum @@ -9,10 +9,14 @@ github.com/aead/chacha20poly1305 v0.0.0-20201124145622-1a5aba2a8b29 h1:1DcvRPZOd github.com/aead/chacha20poly1305 v0.0.0-20201124145622-1a5aba2a8b29/go.mod h1:UzH9IX1MMqOcwhoNOIjmTQeAxrFgzs50j4golQtXXxU= github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635 h1:52m0LGchQBBVqJRyYYufQuIbVqRawmubW3OFGqK1ekw= github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635/go.mod h1:lmLxL+FV291OopO93Bwf9fQLQeLyt33VJRUg5VJ30us= +github.com/bsm/ginkgo/v2 v2.7.0/go.mod h1:AiKlXPm7ItEHNc/2+OkrNG4E0ITzojb9/xWzvQ9XZ9w= +github.com/bsm/gomega v1.26.0/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM= github.com/bytedance/sonic v1.10.2 h1:GQebETVBxYB7JGWJtLBi07OVzWwt+8dWA00gEVW2ZFE= github.com/bytedance/sonic v1.10.2/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0= @@ -25,6 +29,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dhui/dktest v0.4.0 h1:z05UmuXZHO/bgj/ds2bGMBu8FI4WA+Ag/m3ghL+om7M= github.com/dhui/dktest v0.4.0/go.mod h1:v/Dbz1LgCBOi2Uki2nUqLBGa83hWBGFMu5MrgMDCc78= github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8= @@ -80,6 +86,7 @@ github.com/google/go-github/v39 v39.2.0/go.mod h1:C1s8C5aCC9L+JXIYpJM5GYytdX52vC github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1 h1:/c3QmbOGMGTOumP2iT/rCwB7b0QDGLKzqOmktBjT+Is= @@ -91,14 +98,19 @@ github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+l github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hibiken/asynq v0.24.1 h1:+5iIEAyA9K/lcSPvx3qoPtsKJeKI5u9aOIvUmSsazEw= +github.com/hibiken/asynq v0.24.1/go.mod h1:u5qVeSbrnfT+vtG5Mq8ZPzQu/BmCKMHvTGb91uy9Tts= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc= github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.3.0 h1:jX8FDLfW4ThVXctBNZ+3cIWnCSnrACDV73r76dy0aQQ= @@ -138,6 +150,11 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/redis/go-redis/v9 v9.0.3/go.mod h1:WqMKv5vnQbRuZstUwxQI195wHy+t4PuXDOjzMvcuQHk= +github.com/redis/go-redis/v9 v9.5.1 h1:H1X4D3yHPaYrkL5X06Wh6xNVM/pX0Ft4RV0vMGvLBh8= +github.com/redis/go-redis/v9 v9.5.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= +github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= +github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= @@ -151,6 +168,7 @@ github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9yS github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= +github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= @@ -178,6 +196,7 @@ github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= @@ -192,10 +211,12 @@ golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -217,6 +238,7 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -234,9 +256,14 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= @@ -262,6 +289,8 @@ google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= diff --git a/internal/app/app.go b/internal/app/app.go index 05cc14e..6b3bb3c 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -4,11 +4,13 @@ import ( "database/sql" db "github.com/cukhoaimon/SimpleBank/internal/usecase/sqlc" "github.com/cukhoaimon/SimpleBank/pkg/grpc" + "github.com/cukhoaimon/SimpleBank/pkg/worker" "github.com/cukhoaimon/SimpleBank/utils" "github.com/golang-migrate/migrate/v4/database/postgres" _ "github.com/golang-migrate/migrate/v4/database/postgres" _ "github.com/golang-migrate/migrate/v4/source/file" _ "github.com/golang-migrate/migrate/v4/source/github" + "github.com/hibiken/asynq" _ "github.com/lib/pq" "github.com/rs/zerolog/log" ) @@ -29,7 +31,13 @@ func Run(config utils.Config) { store := db.NewStore(conn) - go grpc.RunGatewayServer(store, config) - grpc.Run(store, config) + redisOpts := asynq.RedisClientOpt{ + Addr: config.RedisAddress, + } + taskDistributor := worker.NewRedisTaskDistributor(redisOpts) + + go grpc.RunTaskProcessor(redisOpts, store) + go grpc.RunGatewayServer(store, config, taskDistributor) + grpc.Run(store, config, taskDistributor) //http2.Run(store, config) } diff --git a/internal/delivery/grpc/gapi/handler.go b/internal/delivery/grpc/gapi/handler.go index ad85d13..35d763e 100644 --- a/internal/delivery/grpc/gapi/handler.go +++ b/internal/delivery/grpc/gapi/handler.go @@ -4,12 +4,14 @@ import ( "github.com/cukhoaimon/SimpleBank/internal/delivery/grpc/pb" db "github.com/cukhoaimon/SimpleBank/internal/usecase/sqlc" "github.com/cukhoaimon/SimpleBank/pkg/token" + "github.com/cukhoaimon/SimpleBank/pkg/worker" "github.com/cukhoaimon/SimpleBank/utils" ) type Handler struct { pb.UnimplementedSimpleBankServer - Config utils.Config - TokenMaker token.Maker - Store db.Store + Config utils.Config + TokenMaker token.Maker + Store db.Store + TaskDistributor worker.TaskDistributor } diff --git a/internal/delivery/grpc/gapi/rpc_create_user.go b/internal/delivery/grpc/gapi/rpc_create_user.go index e33d792..abaa73c 100644 --- a/internal/delivery/grpc/gapi/rpc_create_user.go +++ b/internal/delivery/grpc/gapi/rpc_create_user.go @@ -6,11 +6,14 @@ import ( "github.com/cukhoaimon/SimpleBank/internal/delivery/grpc/pb" db "github.com/cukhoaimon/SimpleBank/internal/usecase/sqlc" "github.com/cukhoaimon/SimpleBank/internal/usecase/val" + "github.com/cukhoaimon/SimpleBank/pkg/worker" "github.com/cukhoaimon/SimpleBank/utils" + "github.com/hibiken/asynq" "github.com/lib/pq" "google.golang.org/genproto/googleapis/rpc/errdetails" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" + "time" ) func (handler *Handler) CreateUser(ctx context.Context, req *pb.CreateUserRequest) (*pb.CreateUserResponse, error) { @@ -24,14 +27,27 @@ func (handler *Handler) CreateUser(ctx context.Context, req *pb.CreateUserReques return nil, status.Errorf(codes.Internal, "fail to hash password: %s", err) } - arg := db.CreateUserParams{ - Username: req.GetUsername(), - HashedPassword: hashedPassword, - FullName: req.GetFullName(), - Email: req.GetEmail(), + arg := db.CreateUserTxParams{ + CreateUserParams: db.CreateUserParams{ + Username: req.GetUsername(), + HashedPassword: hashedPassword, + FullName: req.GetFullName(), + Email: req.GetEmail(), + }, + AfterCreate: func(user db.User) error { + taskPayload := &worker.PayloadVerifyEmail{ + Username: user.Username, + } + opts := []asynq.Option{ + asynq.MaxRetry(10), + asynq.ProcessIn(10 * time.Second), + asynq.Queue(worker.QueueCritical), + } + return handler.TaskDistributor.DistributeTaskSendVerifyEmail(ctx, taskPayload, opts...) + }, } - user, err := handler.Store.CreateUser(ctx, arg) + txResult, err := handler.Store.CreateUserTx(ctx, arg) if err != nil { var pqErr *pq.Error if errors.As(err, &pqErr) { @@ -45,8 +61,9 @@ func (handler *Handler) CreateUser(ctx context.Context, req *pb.CreateUserReques } rsp := &pb.CreateUserResponse{ - User: convertUser(user), + User: convertUser(txResult.User), } + return rsp, nil } diff --git a/internal/delivery/http/mock/store.go b/internal/delivery/http/mock/store.go index 3d4c1fa..088adb2 100644 --- a/internal/delivery/http/mock/store.go +++ b/internal/delivery/http/mock/store.go @@ -126,6 +126,21 @@ func (mr *MockStoreMockRecorder) CreateUser(arg0, arg1 interface{}) *gomock.Call return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateUser", reflect.TypeOf((*MockStore)(nil).CreateUser), arg0, arg1) } +// CreateUserTx mocks base method. +func (m *MockStore) CreateUserTx(arg0 context.Context, arg1 usecase.CreateUserTxParams) (usecase.CreateUserTxResult, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateUserTx", arg0, arg1) + ret0, _ := ret[0].(usecase.CreateUserTxResult) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateUserTx indicates an expected call of CreateUserTx. +func (mr *MockStoreMockRecorder) CreateUserTx(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateUserTx", reflect.TypeOf((*MockStore)(nil).CreateUserTx), arg0, arg1) +} + // DeleteAccount mocks base method. func (m *MockStore) DeleteAccount(arg0 context.Context, arg1 int64) error { m.ctrl.T.Helper() diff --git a/internal/usecase/sqlc/store.go b/internal/usecase/sqlc/store.go index 213036c..3598981 100644 --- a/internal/usecase/sqlc/store.go +++ b/internal/usecase/sqlc/store.go @@ -8,7 +8,8 @@ import ( type Store interface { Querier - TransferTxAccount(ctx context.Context, arg TransferTxParams) (TransferTxResult, error) + TransferTx(ctx context.Context, arg TransferTxParams) (TransferTxResult, error) + CreateUserTx(ctx context.Context, arg CreateUserTxParams) (CreateUserTxResult, error) } type SQLStore struct { @@ -44,90 +45,3 @@ func (store *SQLStore) execTx(ctx context.Context, fn func(*Queries) error) erro return tx.Commit() } - -type TransferTxParams struct { - FromAccountID int64 `json:"from_account_id"` - ToAccountID int64 `json:"to_account_id"` - Amount int64 `json:"amount"` -} - -type TransferTxResult struct { - Transfer Transfer `json:"transfer"` - FromAccount Account `json:"from_account"` - ToAccount Account `json:"to_account"` - FromEntry Entry `json:"from_entry"` - ToEntry Entry `json:"to_entry"` -} - -// TransferTxAccount perform a transfer between two account -// It create transfer record, add account entries, update account balance -func (store *SQLStore) TransferTxAccount(ctx context.Context, arg TransferTxParams) (TransferTxResult, error) { - result := TransferTxResult{} - - err := store.execTx(ctx, func(q *Queries) error { - var err error - - result.Transfer, err = q.CreateTransfer(ctx, CreateTransferParams{ - FromAccountID: arg.FromAccountID, - ToAccountID: arg.ToAccountID, - Amount: arg.Amount, - }) - if err != nil { - return err - } - - result.FromEntry, err = q.CreateEntry(ctx, CreateEntryParams{ - AccountID: arg.FromAccountID, - Amount: -arg.Amount, - }) - if err != nil { - return err - } - - result.ToEntry, err = q.CreateEntry(ctx, CreateEntryParams{ - AccountID: arg.ToAccountID, - Amount: arg.Amount, - }) - if err != nil { - return err - } - - if arg.FromAccountID < arg.ToAccountID { - result.FromAccount, result.ToAccount, err = transferMoney(ctx, q, arg.FromAccountID, -arg.Amount, arg.ToAccountID, arg.Amount) - - } else { - result.ToAccount, result.FromAccount, err = transferMoney(ctx, q, arg.ToAccountID, arg.Amount, arg.FromAccountID, -arg.Amount) - } - - if err != nil { - return err - } - - return nil - }) - - return result, err -} - -func transferMoney( - ctx context.Context, - q *Queries, - fromAccountID int64, - fromAmount int64, - toAccountID int64, - toAmount int64, -) (fromAccount, toAccount Account, err error) { - fromAccount, err = q.AddAccountBalance(ctx, AddAccountBalanceParams{ - ID: fromAccountID, - Amount: fromAmount, - }) - if err != nil { - return - } - - toAccount, err = q.AddAccountBalance(ctx, AddAccountBalanceParams{ - ID: toAccountID, - Amount: toAmount, - }) - return -} diff --git a/internal/usecase/sqlc/tx_create_user.go b/internal/usecase/sqlc/tx_create_user.go new file mode 100644 index 0000000..f4e788d --- /dev/null +++ b/internal/usecase/sqlc/tx_create_user.go @@ -0,0 +1,31 @@ +package usecase + +import "context" + +// CreateUserTxParams contains the input parameters of create user +type CreateUserTxParams struct { + CreateUserParams + AfterCreate func(user User) error +} + +// CreateUserTxResult is the result of the CreateUser +type CreateUserTxResult struct { + User User +} + +// CreateUserTx performs a creation of user +func (store *SQLStore) CreateUserTx(ctx context.Context, arg CreateUserTxParams) (CreateUserTxResult, error) { + var result CreateUserTxResult + + err := store.execTx(ctx, func(q *Queries) error { + var err error + result.User, err = q.CreateUser(ctx, arg.CreateUserParams) + if err != nil { + return err + } + + return arg.AfterCreate(result.User) + }) + + return result, err +} diff --git a/internal/usecase/sqlc/tx_transfer.go b/internal/usecase/sqlc/tx_transfer.go new file mode 100644 index 0000000..b59a361 --- /dev/null +++ b/internal/usecase/sqlc/tx_transfer.go @@ -0,0 +1,87 @@ +package usecase + +import "context" + +// TransferTxParams contains the input parameters of the transfer transaction +type TransferTxParams struct { + FromAccountID int64 `json:"from_account_id"` + ToAccountID int64 `json:"to_account_id"` + Amount int64 `json:"amount"` +} + +// TransferTxResult is the result of the transfer transaction +type TransferTxResult struct { + Transfer Transfer `json:"transfer"` + FromAccount Account `json:"from_account"` + ToAccount Account `json:"to_account"` + FromEntry Entry `json:"from_entry"` + ToEntry Entry `json:"to_entry"` +} + +// TransferTx performs a money transfer from one account to the other. +// It creates the transfer, add account entries, and update accounts' balance within a database transaction +func (store *SQLStore) TransferTx(ctx context.Context, arg TransferTxParams) (TransferTxResult, error) { + var result TransferTxResult + + err := store.execTx(ctx, func(q *Queries) error { + var err error + + result.Transfer, err = q.CreateTransfer(ctx, CreateTransferParams{ + FromAccountID: arg.FromAccountID, + ToAccountID: arg.ToAccountID, + Amount: arg.Amount, + }) + if err != nil { + return err + } + + result.FromEntry, err = q.CreateEntry(ctx, CreateEntryParams{ + AccountID: arg.FromAccountID, + Amount: -arg.Amount, + }) + if err != nil { + return err + } + + result.ToEntry, err = q.CreateEntry(ctx, CreateEntryParams{ + AccountID: arg.ToAccountID, + Amount: arg.Amount, + }) + if err != nil { + return err + } + + if arg.FromAccountID < arg.ToAccountID { + result.FromAccount, result.ToAccount, err = addMoney(ctx, q, arg.FromAccountID, -arg.Amount, arg.ToAccountID, arg.Amount) + } else { + result.ToAccount, result.FromAccount, err = addMoney(ctx, q, arg.ToAccountID, arg.Amount, arg.FromAccountID, -arg.Amount) + } + + return err + }) + + return result, err +} + +func addMoney( + ctx context.Context, + q *Queries, + accountID1 int64, + amount1 int64, + accountID2 int64, + amount2 int64, +) (account1 Account, account2 Account, err error) { + account1, err = q.AddAccountBalance(ctx, AddAccountBalanceParams{ + ID: accountID1, + Amount: amount1, + }) + if err != nil { + return + } + + account2, err = q.AddAccountBalance(ctx, AddAccountBalanceParams{ + ID: accountID2, + Amount: amount2, + }) + return +} diff --git a/pkg/grpc/server.go b/pkg/grpc/server.go index d164b8a..f57cc33 100644 --- a/pkg/grpc/server.go +++ b/pkg/grpc/server.go @@ -6,8 +6,10 @@ import ( "github.com/cukhoaimon/SimpleBank/internal/delivery/grpc/pb" db "github.com/cukhoaimon/SimpleBank/internal/usecase/sqlc" token2 "github.com/cukhoaimon/SimpleBank/pkg/token" + "github.com/cukhoaimon/SimpleBank/pkg/worker" "github.com/cukhoaimon/SimpleBank/utils" "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" + "github.com/hibiken/asynq" "github.com/rs/zerolog/log" "google.golang.org/grpc" "google.golang.org/grpc/reflection" @@ -22,24 +24,26 @@ type Server struct { } // NewServer will return new gRPC server -func NewServer(store db.Store, config utils.Config) (*Server, error) { +func NewServer(store db.Store, config utils.Config, taskDistributor worker.TaskDistributor) (*Server, error) { maker, err := token2.NewPasetoMaker(config.TokenSymmetricKey) if err != nil { return nil, err } handler := &gapi.Handler{ - Store: store, - TokenMaker: maker, - Config: config, + Store: store, + TokenMaker: maker, + Config: config, + TaskDistributor: taskDistributor, } return &Server{Handler: handler}, nil } // Run will run gRPC server with provided store and config -func Run(store db.Store, config utils.Config) { - server, err := NewServer(store, config) +func Run(store db.Store, config utils.Config, taskDistributor worker.TaskDistributor) { + + server, err := NewServer(store, config, taskDistributor) if err != nil { log.Fatal().Err(err).Msg("Cannot create gRPC server: ") } @@ -63,8 +67,8 @@ func Run(store db.Store, config utils.Config) { // RunGatewayServer will run gRPC Gateway with provided store and config // to serve HTTP Request -func RunGatewayServer(store db.Store, config utils.Config) { - server, err := NewServer(store, config) +func RunGatewayServer(store db.Store, config utils.Config, taskDistributor worker.TaskDistributor) { + server, err := NewServer(store, config, taskDistributor) if err != nil { log.Fatal().Err(err).Msg("Cannot create gRPC server: ") } @@ -105,3 +109,12 @@ func RunGatewayServer(store db.Store, config utils.Config) { log.Fatal().Err(err).Msg("cannot HTTP gateway server: ") } } + +func RunTaskProcessor(redisOpts asynq.RedisClientOpt, store db.Store) { + taskProcessor := worker.NewRedisTaskProcessor(redisOpts, store) + log.Info().Msg("start task processor") + + if err := taskProcessor.Start(); err != nil { + log.Fatal().Err(err).Msg("fail to start task processor") + } +} diff --git a/pkg/worker/distributor.go b/pkg/worker/distributor.go new file mode 100644 index 0000000..6dd6a63 --- /dev/null +++ b/pkg/worker/distributor.go @@ -0,0 +1,25 @@ +package worker + +import ( + "context" + "github.com/hibiken/asynq" +) + +type TaskDistributor interface { + DistributeTaskSendVerifyEmail( + ctx context.Context, + payload *PayloadVerifyEmail, + opts ...asynq.Option, + ) error +} + +type RedisTaskDistributor struct { + client *asynq.Client +} + +func NewRedisTaskDistributor(redisOpt asynq.RedisClientOpt) TaskDistributor { + client := asynq.NewClient(redisOpt) + return &RedisTaskDistributor{ + client: client, + } +} diff --git a/pkg/worker/processor.go b/pkg/worker/processor.go new file mode 100644 index 0000000..354743b --- /dev/null +++ b/pkg/worker/processor.go @@ -0,0 +1,45 @@ +package worker + +import ( + "context" + db "github.com/cukhoaimon/SimpleBank/internal/usecase/sqlc" + "github.com/hibiken/asynq" +) + +const ( + QueueCritical = "critical" + QueueDefault = "default" +) + +type TaskProcessor interface { + Start() error + ProcessTaskSendVerifyEmail(ctx context.Context, task *asynq.Task) error +} + +type RedisTaskProcessor struct { + server *asynq.Server + store db.Store +} + +func (r RedisTaskProcessor) Start() error { + mux := asynq.NewServeMux() + mux.HandleFunc(TaskSendVerifyEmail, r.ProcessTaskSendVerifyEmail) + + return r.server.Start(mux) +} + +func NewRedisTaskProcessor(redisOpts asynq.RedisClientOpt, store db.Store) TaskProcessor { + server := asynq.NewServer( + redisOpts, + asynq.Config{ + Queues: map[string]int{ + QueueCritical: 10, + QueueDefault: 5, + }, + }, + ) + return &RedisTaskProcessor{ + server: server, + store: store, + } +} diff --git a/pkg/worker/task_send_verify_email.go b/pkg/worker/task_send_verify_email.go new file mode 100644 index 0000000..f176f5c --- /dev/null +++ b/pkg/worker/task_send_verify_email.go @@ -0,0 +1,64 @@ +package worker + +import ( + "context" + "database/sql" + "encoding/json" + "errors" + "fmt" + "github.com/hibiken/asynq" + "github.com/rs/zerolog/log" +) + +const ( + TaskSendVerifyEmail = "task:send_verify_email" +) + +type PayloadVerifyEmail struct { + Username string `json:"username"` +} + +func (r RedisTaskDistributor) DistributeTaskSendVerifyEmail(ctx context.Context, payload *PayloadVerifyEmail, opts ...asynq.Option) error { + jsonPayload, err := json.Marshal(payload) + if err != nil { + return fmt.Errorf("fail to marshal task payload: %v", payload) + } + task := asynq.NewTask(TaskSendVerifyEmail, jsonPayload, opts...) + + taskInfo, err := r.client.EnqueueContext(ctx, task) + if err != nil { + return err + } + + log.Info(). + Str("task", task.Type()). + Bytes("payload", task.Payload()). + Str("queue", taskInfo.Queue). + Int("max_retry", taskInfo.MaxRetry). + Msg("enqueue task") + + return nil +} + +func (r RedisTaskProcessor) ProcessTaskSendVerifyEmail(ctx context.Context, task *asynq.Task) error { + var payload PayloadVerifyEmail + + if err := json.Unmarshal(task.Payload(), &payload); err != nil { + return fmt.Errorf("fail to unmarshal payload: %w", asynq.SkipRetry) + } + user, err := r.store.GetUser(ctx, payload.Username) + if err != nil { + if errors.Is(sql.ErrNoRows, err) { + return fmt.Errorf("user does not exists: %w", asynq.SkipRetry) + } + return fmt.Errorf("fail to get user: %w", asynq.SkipRetry) + } + + //TODO send real email + log.Info(). + Str("task", task.Type()). + Bytes("payload", task.Payload()). + Str("email", user.Email). + Msg("processed task") + return nil +} diff --git a/utils/config.go b/utils/config.go index c2699ce..962f534 100644 --- a/utils/config.go +++ b/utils/config.go @@ -15,6 +15,7 @@ type Config struct { RefreshTokenDuration time.Duration `mapstructure:"REFRESH_TOKEN_DURATION"` TokenSymmetricKey string `mapstructure:"TOKEN_SYMMETRIC_KEY"` PostgresDB string `mapstructure:"POSTGRES_DB"` + RedisAddress string `mapstructure:"REDIS_ADDRESS"` } func LoadConfig(path string) (config Config, err error) {