├── .gitignore ├── Dockerfile ├── README.md ├── docker-compose.yml ├── go.mod ├── go.sum ├── gologger └── gologger.go ├── http_server ├── custom_context.go ├── http_server.go └── schedule.go ├── main.go ├── request-sleep.sh ├── request.sh ├── scheduling └── scheduling.go └── utils ├── env.go ├── errors.go └── utils.go /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | .idea -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.21.1-bullseye as build 2 | 3 | WORKDIR /app 4 | 5 | # START GIT PRIVATE SECTION - delete if not using private packages 6 | ARG GIT_INSTEAD_OF=ssh://git@github.com/ 7 | ARG GO_ARGS="" 8 | 9 | # Need ssh for private packages 10 | RUN mkdir /root/.ssh && echo "# github.com\n|1|ljja8g3oSggsnjO9rsrgs7Udx2s=|I6pPqynzf/0nwAnJ3LQ4n9n6Gc8= ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEmKSENjQEezOmxkZMy7opKgwFB9nkt5YRrYMjNuG5N87uRgg6CLrbo5wAdT/y6v0mKV0U2w0WZ2YB/++Tpockg=\n|1|rFMG6UlqGl4xrNGGKf6FYK56sMU=|bLF794kw2BGoKCjiN696DX+dMh4= ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEmKSENjQEezOmxkZMy7opKgwFB9nkt5YRrYMjNuG5N87uRgg6CLrbo5wAdT/y6v0mKV0U2w0WZ2YB/++Tpockg=" >> /root/.ssh/known_hosts 11 | RUN go env -w GOPRIVATE=github.com/ 12 | RUN git config --global url."${GIT_INSTEAD_OF}".insteadOf https://github.com/ 13 | # END GIT PRIVATE SECTION 14 | 15 | COPY go.* /app/ 16 | 17 | RUN --mount=type=cache,target=/go/pkg/mod \ 18 | --mount=type=cache,target=/root/.cache/go-build \ 19 | --mount=type=ssh \ 20 | go mod download 21 | 22 | COPY . . 23 | 24 | RUN --mount=type=cache,target=/go/pkg/mod \ 25 | --mount=type=cache,target=/root/.cache/go-build \ 26 | go build $GO_ARGS -o /app/outbin 27 | 28 | # Need glibc 29 | FROM gcr.io/distroless/base-debian11 30 | 31 | ENTRYPOINT ["/app/outbin"] 32 | COPY --from=build /app/outbin /app/ 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Compute scheduler blog post 2 | 3 | ## CODE QUALITY NOTE: 4 | 5 | There are lots of cases where I have `must` variations of functions that just panic on error, as well as many `logger.Fatal()` calls. Obviously this is a bad idea, and is entirely done to keep the code as terse as possible. 6 | 7 | This code should not be forked for production use, but serve as a point of reference for how this system works. -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.7" 2 | services: 3 | nats: 4 | image: nats 5 | ports: 6 | - "8222:8222" 7 | - "4222:4222" 8 | command: "--cluster_name NATS --cluster nats://0.0.0.0:6222 --http_port 8222" -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/danthegoodman1/GoAPITemplate 2 | 3 | go 1.22 4 | 5 | require ( 6 | github.com/go-playground/validator/v10 v10.11.1 7 | github.com/google/uuid v1.3.1 8 | github.com/jackc/pgtype v1.12.0 9 | github.com/joho/godotenv v1.5.1 10 | github.com/labstack/echo/v4 v4.11.1 11 | github.com/matoous/go-nanoid/v2 v2.0.0 12 | github.com/nats-io/nats.go v1.33.1 13 | github.com/rs/zerolog v1.29.1 14 | github.com/segmentio/ksuid v1.0.4 15 | go.uber.org/atomic v1.6.0 16 | golang.org/x/net v0.17.0 17 | ) 18 | 19 | require ( 20 | github.com/go-playground/locales v0.14.0 // indirect 21 | github.com/go-playground/universal-translator v0.18.0 // indirect 22 | github.com/golang-jwt/jwt v3.2.2+incompatible // indirect 23 | github.com/jackc/pgio v1.0.0 // indirect 24 | github.com/jackc/pgproto3/v2 v2.3.1 // indirect 25 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect 26 | github.com/jackc/pgx/v4 v4.16.1 // indirect 27 | github.com/klauspost/compress v1.17.2 // indirect 28 | github.com/labstack/gommon v0.4.0 // indirect 29 | github.com/leodido/go-urn v1.2.1 // indirect 30 | github.com/lib/pq v1.10.6 // indirect 31 | github.com/mattn/go-colorable v0.1.13 // indirect 32 | github.com/mattn/go-isatty v0.0.19 // indirect 33 | github.com/nats-io/nkeys v0.4.7 // indirect 34 | github.com/nats-io/nuid v1.0.1 // indirect 35 | github.com/stretchr/testify v1.8.4 // indirect 36 | github.com/valyala/bytebufferpool v1.0.0 // indirect 37 | github.com/valyala/fasttemplate v1.2.2 // indirect 38 | golang.org/x/crypto v0.18.0 // indirect 39 | golang.org/x/sys v0.16.0 // indirect 40 | golang.org/x/text v0.14.0 // indirect 41 | golang.org/x/time v0.3.0 // indirect 42 | ) 43 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 2 | github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= 3 | github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= 4 | github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 5 | github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 6 | github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= 7 | github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= 8 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 9 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 11 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 12 | github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= 13 | github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= 14 | github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= 15 | github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 16 | github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU= 17 | github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= 18 | github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho= 19 | github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= 20 | github.com/go-playground/validator/v10 v10.11.1 h1:prmOlTVv+YjZjmRmNSF3VmspqJIxJWXmqUsHwfTRRkQ= 21 | github.com/go-playground/validator/v10 v10.11.1/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU= 22 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 23 | github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 24 | github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= 25 | github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= 26 | github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= 27 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 28 | github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= 29 | github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 30 | github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0= 31 | github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= 32 | github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= 33 | github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= 34 | github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= 35 | github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA= 36 | github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE= 37 | github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s= 38 | github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= 39 | github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY= 40 | github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= 41 | github.com/jackc/pgconn v1.12.1 h1:rsDFzIpRk7xT4B8FufgpCCeyjdNpKyghZeSefViE5W8= 42 | github.com/jackc/pgconn v1.12.1/go.mod h1:ZkhRC59Llhrq3oSfrikvwQ5NaxYExr6twkdkMLaKono= 43 | github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= 44 | github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= 45 | github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= 46 | github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c= 47 | github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak= 48 | github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= 49 | github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= 50 | github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= 51 | github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= 52 | github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= 53 | github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= 54 | github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= 55 | github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= 56 | github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= 57 | github.com/jackc/pgproto3/v2 v2.3.0/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= 58 | github.com/jackc/pgproto3/v2 v2.3.1 h1:nwj7qwf0S+Q7ISFfBndqeLwSwxs+4DPsbRFjECT1Y4Y= 59 | github.com/jackc/pgproto3/v2 v2.3.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= 60 | github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= 61 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= 62 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= 63 | github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= 64 | github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= 65 | github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= 66 | github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM= 67 | github.com/jackc/pgtype v1.11.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= 68 | github.com/jackc/pgtype v1.12.0 h1:Dlq8Qvcch7kiehm8wPGIW0W3KsCCHJnRacKW0UM8n5w= 69 | github.com/jackc/pgtype v1.12.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= 70 | github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= 71 | github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= 72 | github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= 73 | github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs= 74 | github.com/jackc/pgx/v4 v4.16.1 h1:JzTglcal01DrghUqt+PmzWsZx/Yh7SC/CTQmSBMTd0Y= 75 | github.com/jackc/pgx/v4 v4.16.1/go.mod h1:SIhx0D5hoADaiXZVyv+3gSm3LCIIINTVO0PficsvWGQ= 76 | github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 77 | github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 78 | github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 79 | github.com/jackc/puddle v1.2.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 80 | github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= 81 | github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 82 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 83 | github.com/klauspost/compress v1.17.2 h1:RlWWUY/Dr4fL8qk9YG7DTZ7PDgME2V4csBXA8L/ixi4= 84 | github.com/klauspost/compress v1.17.2/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= 85 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 86 | github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 87 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 88 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 89 | github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= 90 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 91 | github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= 92 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 93 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 94 | github.com/labstack/echo/v4 v4.11.1 h1:dEpLU2FLg4UVmvCGPuk/APjlH6GDpbEPti61srUUUs4= 95 | github.com/labstack/echo/v4 v4.11.1/go.mod h1:YuYRTSM3CHs2ybfrL8Px48bO6BAnYIN4l8wSTMP6BDQ= 96 | github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8= 97 | github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM= 98 | github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= 99 | github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= 100 | github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 101 | github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 102 | github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 103 | github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 104 | github.com/lib/pq v1.10.6 h1:jbk+ZieJ0D7EVGJYpL9QTz7/YW6UHbmdnZWYyK5cdBs= 105 | github.com/lib/pq v1.10.6/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 106 | github.com/matoous/go-nanoid v1.5.0/go.mod h1:zyD2a71IubI24efhpvkJz+ZwfwagzgSO6UNiFsZKN7U= 107 | github.com/matoous/go-nanoid/v2 v2.0.0 h1:d19kur2QuLeHmJBkvYkFdhFBzLoo1XVm2GgTpL+9Tj0= 108 | github.com/matoous/go-nanoid/v2 v2.0.0/go.mod h1:FtS4aGPVfEkxKxhdWPAspZpZSh1cOjtM7Ej/So3hR0g= 109 | github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= 110 | github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 111 | github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= 112 | github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= 113 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 114 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 115 | github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 116 | github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 117 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 118 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 119 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 120 | github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= 121 | github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 122 | github.com/nats-io/nats.go v1.33.1 h1:8TxLZZ/seeEfR97qV0/Bl939tpDnt2Z2fK3HkPypj70= 123 | github.com/nats-io/nats.go v1.33.1/go.mod h1:Ubdu4Nh9exXdSz0RVWRFBbRfrbSxOYd26oF0wkWclB8= 124 | github.com/nats-io/nkeys v0.4.7 h1:RwNJbbIdYCoClSDNY7QVKZlyb/wfT6ugvFCiKy6vDvI= 125 | github.com/nats-io/nkeys v0.4.7/go.mod h1:kqXRgRDPlGy7nGaEDMuYzmiJCIAAWDK0IMBtDmGD0nc= 126 | github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= 127 | github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= 128 | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 129 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 130 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 131 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 132 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 133 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 134 | github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= 135 | github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= 136 | github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= 137 | github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= 138 | github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= 139 | github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= 140 | github.com/rs/zerolog v1.29.1 h1:cO+d60CHkknCbvzEWxP0S9K6KqyTjrCNUy1LdQLCGPc= 141 | github.com/rs/zerolog v1.29.1/go.mod h1:Le6ESbR7hc+DP6Lt1THiV8CQSdkkNrd3R0XbEgp3ZBU= 142 | github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= 143 | github.com/segmentio/ksuid v1.0.4 h1:sBo2BdShXjmcugAMwjugoGUdUV0pcxY5mW4xKRn3v4c= 144 | github.com/segmentio/ksuid v1.0.4/go.mod h1:/XUiZBD3kVx5SmUOl55voK5yeAbBNNIed+2O73XgrPE= 145 | github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= 146 | github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= 147 | github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= 148 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 149 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 150 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 151 | github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= 152 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 153 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 154 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 155 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 156 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 157 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 158 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 159 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 160 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 161 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 162 | github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= 163 | github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= 164 | github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= 165 | github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= 166 | go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 167 | go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 168 | go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 169 | go.uber.org/atomic v1.6.0 h1:Ezj3JGmsOnG1MoRWQkPBsKLe9DwWD9QeXzTRzzldNVk= 170 | go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 171 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 172 | go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= 173 | go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= 174 | go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= 175 | go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 176 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 177 | go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= 178 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 179 | golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= 180 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 181 | golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 182 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 183 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 184 | golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= 185 | golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 186 | golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 187 | golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 188 | golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= 189 | golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= 190 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs= 191 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 192 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 193 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 194 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 195 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 196 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 197 | golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 198 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 199 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 200 | golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= 201 | golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= 202 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 203 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 204 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 205 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 206 | golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 207 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 208 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 209 | golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 210 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 211 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 212 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 213 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 214 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 215 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 216 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 217 | golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 218 | golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 219 | golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 220 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 221 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 222 | golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= 223 | golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 224 | golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= 225 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 226 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 227 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 228 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 229 | golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 230 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 231 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 232 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= 233 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 234 | golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= 235 | golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 236 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 237 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 238 | golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 239 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 240 | golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 241 | golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 242 | golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 243 | golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 244 | golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= 245 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 246 | golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 247 | golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 248 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 249 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 250 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 251 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 252 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 253 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 254 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 255 | gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= 256 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 257 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 258 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 259 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 260 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 261 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 262 | -------------------------------------------------------------------------------- /gologger/gologger.go: -------------------------------------------------------------------------------- 1 | package gologger 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "os" 7 | "runtime" 8 | "strconv" 9 | "strings" 10 | "time" 11 | 12 | "github.com/rs/zerolog" 13 | ) 14 | 15 | type ctxKey string 16 | 17 | const ReqIDKey ctxKey = "reqID" 18 | 19 | func init() { 20 | l := NewLogger() 21 | zerolog.DefaultContextLogger = &l 22 | zerolog.CallerMarshalFunc = func(pc uintptr, file string, line int) string { 23 | function := "" 24 | fun := runtime.FuncForPC(pc) 25 | if fun != nil { 26 | funName := fun.Name() 27 | slash := strings.LastIndex(funName, "/") 28 | if slash > 0 { 29 | funName = funName[slash+1:] 30 | } 31 | function = " " + funName + "()" 32 | } 33 | return file + ":" + strconv.Itoa(line) + function 34 | } 35 | } 36 | 37 | func GetEnvOrDefault(env, defaultVal string) string { 38 | e := os.Getenv(env) 39 | if e == "" { 40 | return defaultVal 41 | } else { 42 | return e 43 | } 44 | } 45 | 46 | // Makes context.Canceled errors a warn (for when people abandon requests) 47 | func LvlForErr(err error) zerolog.Level { 48 | if errors.Is(err, context.Canceled) { 49 | return zerolog.WarnLevel 50 | } 51 | return zerolog.ErrorLevel 52 | } 53 | 54 | func NewLogger() zerolog.Logger { 55 | if os.Getenv("LOG_TIME_MS") == "1" { 56 | // Log with milliseconds 57 | zerolog.TimeFieldFormat = zerolog.TimeFormatUnixMs 58 | } else { 59 | zerolog.TimeFieldFormat = time.RFC3339Nano 60 | } 61 | 62 | zerolog.LevelFieldName = GetEnvOrDefault("LOG_LEVEL_KEY", "level") 63 | 64 | zerolog.TimestampFieldName = "time" 65 | 66 | logger := zerolog.New(os.Stdout).With().Timestamp().Logger() 67 | 68 | logger = logger.Hook(CallerHook{}) 69 | 70 | if os.Getenv("PRETTY") == "1" { 71 | logger = logger.Output(zerolog.ConsoleWriter{Out: os.Stderr}) 72 | } 73 | if os.Getenv("TRACE") == "1" { 74 | zerolog.SetGlobalLevel(zerolog.TraceLevel) 75 | } else if os.Getenv("DEBUG") == "1" { 76 | zerolog.SetGlobalLevel(zerolog.DebugLevel) 77 | } else { 78 | zerolog.SetGlobalLevel(zerolog.InfoLevel) 79 | } 80 | 81 | return logger 82 | } 83 | 84 | type CallerHook struct{} 85 | 86 | func (h CallerHook) Run(e *zerolog.Event, _ zerolog.Level, _ string) { 87 | e.Caller(3) 88 | } 89 | -------------------------------------------------------------------------------- /http_server/custom_context.go: -------------------------------------------------------------------------------- 1 | package http_server 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "net/http" 7 | 8 | "github.com/danthegoodman1/GoAPITemplate/gologger" 9 | "github.com/google/uuid" 10 | "github.com/labstack/echo/v4" 11 | "github.com/rs/zerolog" 12 | ) 13 | 14 | type CustomContext struct { 15 | echo.Context 16 | RequestID string 17 | UserID string 18 | } 19 | 20 | func CreateReqContext(next echo.HandlerFunc) echo.HandlerFunc { 21 | return func(c echo.Context) error { 22 | reqID := uuid.NewString() 23 | ctx := context.WithValue(c.Request().Context(), gologger.ReqIDKey, reqID) 24 | ctx = logger.WithContext(ctx) 25 | c.SetRequest(c.Request().WithContext(ctx)) 26 | logger := zerolog.Ctx(ctx) 27 | logger.UpdateContext(func(c zerolog.Context) zerolog.Context { 28 | return c.Str("reqID", reqID) 29 | }) 30 | cc := &CustomContext{ 31 | Context: c, 32 | RequestID: reqID, 33 | } 34 | return next(cc) 35 | } 36 | } 37 | 38 | // Casts to custom context for the handler, so this doesn't have to be done per handler 39 | func ccHandler(h func(*CustomContext) error) echo.HandlerFunc { 40 | // TODO: Include the path? 41 | return func(c echo.Context) error { 42 | return h(c.(*CustomContext)) 43 | } 44 | } 45 | 46 | func (c *CustomContext) internalErrorMessage() string { 47 | return "internal error, request id: " + c.RequestID 48 | } 49 | 50 | func (c *CustomContext) InternalError(err error, msg string) error { 51 | if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { 52 | zerolog.Ctx(c.Request().Context()).Warn().CallerSkipFrame(1).Msg(err.Error()) 53 | } else { 54 | zerolog.Ctx(c.Request().Context()).Error().CallerSkipFrame(1).Err(err).Msg(msg) 55 | } 56 | return c.String(http.StatusInternalServerError, c.internalErrorMessage()) 57 | } 58 | -------------------------------------------------------------------------------- /http_server/http_server.go: -------------------------------------------------------------------------------- 1 | package http_server 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "github.com/nats-io/nats.go" 8 | "net" 9 | "net/http" 10 | "os" 11 | "time" 12 | 13 | "github.com/danthegoodman1/GoAPITemplate/gologger" 14 | "github.com/danthegoodman1/GoAPITemplate/utils" 15 | "github.com/go-playground/validator/v10" 16 | "github.com/labstack/echo/v4" 17 | "github.com/labstack/echo/v4/middleware" 18 | "github.com/rs/zerolog" 19 | "golang.org/x/net/http2" 20 | ) 21 | 22 | var logger = gologger.NewLogger() 23 | 24 | type HTTPServer struct { 25 | Echo *echo.Echo 26 | NatsClient *nats.Conn 27 | } 28 | 29 | type CustomValidator struct { 30 | validator *validator.Validate 31 | NatsConn *nats.Conn 32 | } 33 | 34 | func StartHTTPServer(nc *nats.Conn) *HTTPServer { 35 | listener, err := net.Listen("tcp", fmt.Sprintf(":%s", utils.GetEnvOrDefault("HTTP_PORT", "8080"))) 36 | if err != nil { 37 | logger.Error().Err(err).Msg("error creating tcp listener, exiting") 38 | os.Exit(1) 39 | } 40 | s := &HTTPServer{ 41 | Echo: echo.New(), 42 | } 43 | s.Echo.HideBanner = true 44 | s.Echo.HidePort = true 45 | 46 | s.Echo.Use(CreateReqContext) 47 | s.Echo.Use(LoggerMiddleware) 48 | s.Echo.Use(middleware.CORS()) 49 | s.Echo.Validator = &CustomValidator{validator: validator.New()} 50 | 51 | s.NatsClient = nc 52 | 53 | // technical - no auth 54 | s.Echo.GET("/hc", s.HealthCheck) 55 | s.Echo.POST("/schedule", ccHandler(s.PostSchedule)) 56 | 57 | s.Echo.Listener = listener 58 | go func() { 59 | logger.Info().Msg("starting h2c server on " + listener.Addr().String()) 60 | err := s.Echo.StartH2CServer("", &http2.Server{}) 61 | // stop the broker 62 | if err != nil && !errors.Is(err, http.ErrServerClosed) { 63 | logger.Error().Err(err).Msg("failed to start h2c server, exiting") 64 | os.Exit(1) 65 | } 66 | }() 67 | 68 | return s 69 | } 70 | 71 | func (cv *CustomValidator) Validate(i interface{}) error { 72 | if err := cv.validator.Struct(i); err != nil { 73 | return echo.NewHTTPError(http.StatusBadRequest, err.Error()) 74 | } 75 | return nil 76 | } 77 | 78 | func ValidateRequest(c echo.Context, s interface{}) error { 79 | if err := c.Bind(s); err != nil { 80 | return echo.NewHTTPError(http.StatusBadRequest, err.Error()) 81 | } 82 | if err := c.Validate(s); err != nil { 83 | return err 84 | } 85 | return nil 86 | } 87 | 88 | func (*HTTPServer) HealthCheck(c echo.Context) error { 89 | return c.String(http.StatusOK, "ok") 90 | } 91 | 92 | func (s *HTTPServer) Shutdown(ctx context.Context) error { 93 | err := s.Echo.Shutdown(ctx) 94 | return err 95 | } 96 | 97 | func LoggerMiddleware(next echo.HandlerFunc) echo.HandlerFunc { 98 | return func(c echo.Context) error { 99 | start := time.Now() 100 | if err := next(c); err != nil { 101 | // default handler 102 | c.Error(err) 103 | } 104 | stop := time.Since(start) 105 | // Log otherwise 106 | logger := zerolog.Ctx(c.Request().Context()) 107 | req := c.Request() 108 | res := c.Response() 109 | 110 | p := req.URL.Path 111 | if p == "" { 112 | p = "/" 113 | } 114 | 115 | cl := req.Header.Get(echo.HeaderContentLength) 116 | if cl == "" { 117 | cl = "0" 118 | } 119 | logger.Debug().Str("method", req.Method).Str("remote_ip", c.RealIP()).Str("req_uri", req.RequestURI).Str("handler_path", c.Path()).Str("path", p).Int("status", res.Status).Int64("latency_ns", int64(stop)).Str("protocol", req.Proto).Str("bytes_in", cl).Int64("bytes_out", res.Size).Msg("req recived") 120 | return nil 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /http_server/schedule.go: -------------------------------------------------------------------------------- 1 | package http_server 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "github.com/danthegoodman1/GoAPITemplate/scheduling" 8 | "github.com/danthegoodman1/GoAPITemplate/utils" 9 | "github.com/labstack/echo/v4" 10 | "net/http" 11 | "time" 12 | ) 13 | 14 | type PostScheduleRequest struct { 15 | Task string // what task we are scheduling 16 | Requirements scheduling.Requirements 17 | Payload map[string]any // task payload 18 | } 19 | 20 | func (s *HTTPServer) PostSchedule(c *CustomContext) error { 21 | var body PostScheduleRequest 22 | if err := ValidateRequest(c, &body); err != nil { 23 | return c.String(http.StatusBadRequest, err.Error()) 24 | } 25 | 26 | // Request resources from workers 27 | logger.Debug().Msgf("Asking workers to schedule '%s'", body.Task) 28 | msg, err := s.NatsClient.Request(fmt.Sprintf("scheduling.request.%s", body.Task), utils.JSONMustMarshal(scheduling.ScheduleRequest{ 29 | RequestID: c.RequestID, // use the unique request ID 30 | Task: body.Task, 31 | Requirements: body.Requirements, 32 | }), time.Second*5) 33 | 34 | if errors.Is(err, context.DeadlineExceeded) { 35 | s.mustEmitRelease(c.RequestID, "") // tell all workers to release resources 36 | return echo.NewHTTPError(http.StatusGatewayTimeout, "no workers responded in time") 37 | } else if err != nil { 38 | return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) 39 | } 40 | 41 | var workerRes scheduling.ScheduleResponse 42 | utils.JSONMustUnmarshal(msg.Data, &workerRes) 43 | 44 | logger.Debug().Msgf("Worker %s responded to schedule request", workerRes.WorkerID) 45 | // tell other workers to release resources 46 | s.mustEmitRelease(c.RequestID, workerRes.WorkerID) 47 | 48 | // Tell the worker that they are reserved, and to do the task 49 | msg, err = s.NatsClient.Request(fmt.Sprintf("scheduling.reserve_task.%s", workerRes.WorkerID), utils.JSONMustMarshal(scheduling.ReserveRequest{ 50 | Task: body.Task, 51 | Payload: body.Payload, 52 | RequestID: c.RequestID, 53 | }), time.Second*5) 54 | 55 | var reserveRes scheduling.ReserveResponse 56 | utils.JSONMustUnmarshal(msg.Data, &reserveRes) 57 | 58 | if reserveRes.Error != nil { 59 | c.String(http.StatusInternalServerError, *reserveRes.Error) 60 | } 61 | 62 | return c.JSON(http.StatusOK, reserveRes.Payload) 63 | } 64 | 65 | func (s *HTTPServer) mustEmitRelease(requestID, exemptWorker string) { 66 | // emit special cancel topic 67 | err := s.NatsClient.Publish("scheduling.release", utils.JSONMustMarshal(scheduling.ReleaseResourcesMessage{ 68 | RequestID: requestID, 69 | ExemptWorker: exemptWorker, 70 | })) 71 | if err != nil { 72 | panic(err) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/danthegoodman1/GoAPITemplate/scheduling" 7 | "github.com/danthegoodman1/GoAPITemplate/utils" 8 | "github.com/joho/godotenv" 9 | "github.com/nats-io/nats.go" 10 | "go.uber.org/atomic" 11 | "os" 12 | "os/signal" 13 | "sync" 14 | "syscall" 15 | "time" 16 | 17 | "github.com/danthegoodman1/GoAPITemplate/gologger" 18 | "github.com/danthegoodman1/GoAPITemplate/http_server" 19 | ) 20 | 21 | var logger = gologger.NewLogger() 22 | 23 | func main() { 24 | if _, err := os.Stat(".env"); err == nil { 25 | err = godotenv.Load() 26 | if err != nil { 27 | logger.Error().Err(err).Msg("error loading .env file, exiting") 28 | os.Exit(1) 29 | } 30 | } 31 | 32 | logger.Debug().Msgf("connecting to nats at %s", utils.NATS_URL) 33 | nc, err := nats.Connect(utils.NATS_URL) 34 | if err != nil { 35 | logger.Fatal().Err(err).Msg("failed to connect to nats") 36 | } 37 | defer nc.Close() 38 | 39 | if len(os.Args) > 1 && os.Args[1] == "coordinator" { 40 | startCoordinator(nc) 41 | return 42 | } else if len(os.Args) > 1 && os.Args[1] == "worker" { 43 | startWorkerNode(nc) 44 | return 45 | } 46 | 47 | logger.Fatal().Msg("must specify coordinator or worker as first argument") 48 | } 49 | 50 | func startWorkerNode(nc *nats.Conn) { 51 | logger.Debug().Msgf("starting worker node %s with slots %d", utils.WORKER_ID, utils.SLOTS) 52 | 53 | availableSlots := atomic.NewInt64(utils.SLOTS) 54 | 55 | // We need a sync map to track reservations 56 | reservations := sync.Map{} 57 | 58 | releaseResources := func(requestID string) { 59 | slots, found := reservations.LoadAndDelete(requestID) 60 | if !found { 61 | // We didn't even have it, ignore 62 | return 63 | } 64 | availableSlots.Add(slots.(int64)) 65 | logger.Debug().Msgf("worker %s released resources", utils.WORKER_ID) 66 | } 67 | 68 | // Scheduling loop 69 | _, err := nc.Subscribe("scheduling.request.*", func(msg *nats.Msg) { 70 | logger.Debug().Msgf("Worker %s got scheduling request, reserving resources", utils.WORKER_ID) 71 | var request scheduling.ScheduleRequest 72 | utils.JSONMustUnmarshal(msg.Data, &request) 73 | 74 | // Check whether the region matches (if provided) 75 | if request.Requirements.Region != "" && request.Requirements.Region != utils.REGION { 76 | logger.Debug().Msgf( 77 | "worker %s cannot fulfill request, different region", 78 | utils.WORKER_ID, 79 | ) 80 | return 81 | } 82 | 83 | // Check whether we have enough available slots 84 | if request.Requirements.Slots > availableSlots.Load() { 85 | logger.Debug().Msgf( 86 | "worker %s cannot fulfill request, not enough slots", 87 | utils.WORKER_ID, 88 | ) 89 | return 90 | } 91 | 92 | // Reserve the slots 93 | // Note: would need better handling to protect against going negative in prod 94 | availableSlots.Sub(request.Requirements.Slots) 95 | reservations.Store(request.RequestID, request.Requirements.Slots) 96 | 97 | err := msg.Respond(utils.JSONMustMarshal(scheduling.ScheduleResponse{ 98 | WorkerID: utils.WORKER_ID, 99 | })) 100 | if err != nil { 101 | logger.Fatal().Err(err).Msg("failed to respond to resource request message") 102 | } 103 | 104 | }) 105 | if err != nil { 106 | logger.Fatal().Err(err).Msg("error subscribing to scheduling.request.*") 107 | } 108 | 109 | // Release loop 110 | _, err = nc.Subscribe("scheduling.release", func(msg *nats.Msg) { 111 | var payload scheduling.ReleaseResourcesMessage 112 | utils.JSONMustUnmarshal(msg.Data, &payload) 113 | if payload.ExemptWorker == utils.WORKER_ID { 114 | // We are exempt from this 115 | return 116 | } 117 | 118 | releaseResources(payload.RequestID) 119 | 120 | logger.Debug().Msgf("Worker %s releasing resources", utils.WORKER_ID) 121 | }) 122 | if err != nil { 123 | logger.Fatal().Err(err).Msg("error subscribing to scheduling.release") 124 | } 125 | 126 | _, err = nc.Subscribe(fmt.Sprintf("scheduling.reserve_task.%s", utils.WORKER_ID), func(msg *nats.Msg) { 127 | // Listen for our own reservations 128 | var reservation scheduling.ReserveRequest 129 | utils.JSONMustUnmarshal(msg.Data, &reservation) 130 | logger.Debug().Msgf("Got reservation on worker node %s with payload %+v", utils.WORKER_ID, reservation) 131 | 132 | if sleepSec, ok := reservation.Payload["SleepSec"].(float64); ok { 133 | logger.Debug().Msgf( 134 | "worker %s sleeping for %f seconds", 135 | utils.WORKER_ID, 136 | sleepSec, 137 | ) 138 | time.Sleep(time.Second * time.Duration(sleepSec)) 139 | } 140 | 141 | err = msg.Respond(utils.JSONMustMarshal(scheduling.ReserveResponse{ 142 | Error: nil, 143 | Payload: map[string]any{ // float64 because of JSON 144 | "Num": reservation.Payload["Num"].(float64) + 1, 145 | }, 146 | })) 147 | if err != nil { 148 | logger.Fatal().Err(err).Msg("failed to respond to reservation request") 149 | } 150 | 151 | // we are done, we can release resources 152 | releaseResources(reservation.RequestID) 153 | }) 154 | if err != nil { 155 | logger.Fatal().Err(err).Msgf("error subscribing to scheduling.reserve.%s", utils.WORKER_ID) 156 | } 157 | 158 | c := make(chan os.Signal, 1) 159 | signal.Notify(c, os.Interrupt, syscall.SIGTERM) 160 | <-c 161 | logger.Warn().Msg("received shutdown signal!") 162 | } 163 | 164 | func startCoordinator(nc *nats.Conn) { 165 | logger.Debug().Msg("starting coordinator api") 166 | 167 | httpServer := http_server.StartHTTPServer(nc) 168 | 169 | c := make(chan os.Signal, 1) 170 | signal.Notify(c, os.Interrupt, syscall.SIGTERM) 171 | <-c 172 | logger.Warn().Msg("received shutdown signal!") 173 | 174 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) 175 | defer cancel() 176 | if err := httpServer.Shutdown(ctx); err != nil { 177 | logger.Error().Err(err).Msg("failed to shutdown HTTP server") 178 | } else { 179 | logger.Info().Msg("successfully shutdown HTTP server") 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /request-sleep.sh: -------------------------------------------------------------------------------- 1 | curl -d '{ 2 | "Task": "increment", 3 | "Payload": { 4 | "Num": 1, 5 | "SleepSec": 10 6 | }, 7 | "Requirements": { 8 | "Region": "us-east", 9 | "Slots": 8 10 | } 11 | }' -H 'Content-Type: application/json' http://localhost:8080/schedule -------------------------------------------------------------------------------- /request.sh: -------------------------------------------------------------------------------- 1 | curl -d '{ 2 | "Task": "increment", 3 | "Payload": { 4 | "Num": 1 5 | }, 6 | "Requirements": { 7 | "Slots": 5, 8 | "Region": "us-east" 9 | } 10 | }' -H 'Content-Type: application/json' http://localhost:8080/schedule -------------------------------------------------------------------------------- /scheduling/scheduling.go: -------------------------------------------------------------------------------- 1 | package scheduling 2 | 3 | type ( 4 | Requirements struct { 5 | Region string 6 | 7 | // Arbitrary compute unit 8 | Slots int64 `validate:"required,gte=1"` 9 | } 10 | 11 | ScheduleRequest struct { 12 | RequestID string 13 | Task string 14 | Requirements Requirements 15 | } 16 | 17 | ScheduleResponse struct { 18 | WorkerID string 19 | Payload map[string]any 20 | } 21 | 22 | ReleaseResourcesMessage struct { 23 | RequestID string 24 | // The worker that may ignore this. Set to empty string to force all workers to release 25 | ExemptWorker string 26 | } 27 | 28 | ReserveRequest struct { 29 | Task string 30 | Payload map[string]any 31 | RequestID string 32 | } 33 | 34 | ReserveResponse struct { 35 | Error *string 36 | Payload map[string]any 37 | } 38 | ) 39 | -------------------------------------------------------------------------------- /utils/env.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "os" 4 | 5 | var ( 6 | NATS_URL = os.Getenv("NATS_URL") 7 | WORKER_ID = os.Getenv("WORKER_ID") 8 | REGION = os.Getenv("REGION") 9 | SLOTS = GetEnvOrDefaultInt("SLOTS", 10) 10 | ) 11 | -------------------------------------------------------------------------------- /utils/errors.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | type PermError string 4 | 5 | func (e PermError) Error() string { 6 | return string(e) 7 | } 8 | 9 | func (e PermError) IsPermanent() bool { 10 | return true 11 | } 12 | -------------------------------------------------------------------------------- /utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "os" 8 | "reflect" 9 | "runtime" 10 | "strconv" 11 | "strings" 12 | "time" 13 | 14 | "github.com/jackc/pgtype" 15 | 16 | "github.com/danthegoodman1/GoAPITemplate/gologger" 17 | gonanoid "github.com/matoous/go-nanoid/v2" 18 | "github.com/segmentio/ksuid" 19 | ) 20 | 21 | var logger = gologger.NewLogger() 22 | 23 | func GetEnvOrDefault(env, defaultVal string) string { 24 | e := os.Getenv(env) 25 | if e == "" { 26 | return defaultVal 27 | } else { 28 | return e 29 | } 30 | } 31 | 32 | func GetEnvOrDefaultInt(env string, defaultVal int64) int64 { 33 | e := os.Getenv(env) 34 | if e == "" { 35 | return defaultVal 36 | } else { 37 | intVal, err := strconv.ParseInt(e, 10, 64) 38 | if err != nil { 39 | logger.Error().Msg(fmt.Sprintf("Failed to parse string to int '%s'", env)) 40 | os.Exit(1) 41 | } 42 | 43 | return intVal 44 | } 45 | } 46 | 47 | func GenRandomID(prefix string) string { 48 | return prefix + gonanoid.MustGenerate("abcdefghijklmonpqrstuvwxyzABCDEFGHIJKLMONPQRSTUVWXYZ0123456789", 22) 49 | } 50 | 51 | func GenKSortedID(prefix string) string { 52 | return prefix + ksuid.New().String() 53 | } 54 | 55 | func GenRandomShortID() string { 56 | // reduced character set that's less probable to mis-type 57 | // change for conflicts is still only 1:128 trillion 58 | return gonanoid.MustGenerate("abcdefghikmonpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ0123456789", 8) 59 | } 60 | 61 | func DaysUntil(t time.Time, d time.Weekday) int { 62 | delta := d - t.Weekday() 63 | if delta < 0 { 64 | delta += 7 65 | } 66 | return int(delta) 67 | } 68 | 69 | func Ptr[T any](s T) *T { 70 | return &s 71 | } 72 | 73 | func Deref[T any](ref *T, fallback T) T { 74 | if ref == nil { 75 | return fallback 76 | } 77 | return *ref 78 | } 79 | 80 | func ArrayOrEmpty[T any](ref []T) []T { 81 | if ref == nil { 82 | return make([]T, 0) 83 | } 84 | return ref 85 | } 86 | 87 | var emptyJSON = pgtype.JSONB{Bytes: []byte("{}"), Status: pgtype.Present} 88 | 89 | func OrEmptyJSON(data pgtype.JSONB) pgtype.JSONB { 90 | if data.Status == pgtype.Null { 91 | data = emptyJSON 92 | } 93 | return data 94 | } 95 | 96 | func IfElse[T any](check bool, a T, b T) T { 97 | if check { 98 | return a 99 | } 100 | return b 101 | } 102 | 103 | func OrEmptyArray[T any](a []T) []T { 104 | if a == nil { 105 | return make([]T, 0) 106 | } 107 | return a 108 | } 109 | 110 | func FirstOr[T any](a []T, def T) T { 111 | if len(a) == 0 { 112 | return def 113 | } 114 | return a[0] 115 | } 116 | 117 | var ErrVersionBadFormat = PermError("bad version format") 118 | 119 | // VersionToInt converts a simple semantic version string (e.e. 18.02.66) 120 | func VersionToInt(v string) (int64, error) { 121 | sParts := strings.Split(v, ".") 122 | if len(sParts) > 3 { 123 | return -1, ErrVersionBadFormat 124 | } 125 | var iParts = make([]int64, 3) 126 | for i := range sParts { 127 | vp, err := strconv.ParseInt(sParts[i], 10, 64) 128 | if err != nil { 129 | return -1, fmt.Errorf("error in ParseInt: %s %w", err.Error(), ErrVersionBadFormat) 130 | } 131 | iParts[i] = vp 132 | } 133 | return iParts[0]*10_000*10_000 + iParts[1]*10_000 + iParts[2], nil 134 | } 135 | 136 | // FuncNameFQ returns the fully qualified name of the function. 137 | func FuncNameFQ(f any) string { 138 | return runtime.FuncForPC(reflect.ValueOf(f).Pointer()).Name() 139 | } 140 | 141 | // FuncName returns the name of the function, without the package. 142 | func FuncName(f any) string { 143 | fqName := FuncNameFQ(f) 144 | return fqName[strings.LastIndexByte(fqName, '.')+1:] 145 | } 146 | 147 | func AsErr[T error](err error) (te T, ok bool) { 148 | if err == nil { 149 | return te, false 150 | } 151 | return te, errors.As(err, &te) 152 | } 153 | 154 | // IsErr is useful for check for a class of errors (e.g. *serviceerror.WorkflowExecutionAlreadyStarted) instead of a specific error. 155 | // E.g. Temporal doesn't even expose some errors, only their types 156 | func IsErr[T error](err error) bool { 157 | _, ok := AsErr[T](err) 158 | return ok 159 | } 160 | 161 | func JSONMustMarshal(v any) []byte { 162 | b, err := json.Marshal(v) 163 | if err != nil { 164 | panic(err) 165 | } 166 | return b 167 | } 168 | 169 | func JSONMustUnmarshal(b []byte, v any) { 170 | err := json.Unmarshal(b, v) 171 | if err != nil { 172 | panic(err) 173 | } 174 | } 175 | --------------------------------------------------------------------------------