diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 30e969f..92c8db4 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -21,6 +21,7 @@ jobs: video_recombiner: ${{ steps.changes.outputs.video_recombiner }} video_upload: ${{ steps.changes.outputs.video_upload }} video_status: ${{ steps.changes.outputs.video_status }} + shared: ${{ steps.changes.outputs.shared }} steps: - uses: actions/checkout@v4 - uses: dorny/paths-filter@v3 @@ -39,6 +40,8 @@ jobs: - 'backend/video-upload/**' video_status: - 'backend/video-status/**' + shared: + - 'backend/shared/**' frontend-ci: needs: path-filter if: ${{ github.event_name == 'pull_request' && needs.path-filter.outputs.frontend == 'true' }} @@ -112,7 +115,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v6 with: - go-version: v1.26.1 + go-version: v1.26.2 cache: 'true' - name: Install ffmpeg dependency @@ -145,7 +148,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v6 with: - go-version: v1.26.1 + go-version: v1.26.2 cache: 'true' - name: Install ffmpeg dependency @@ -178,7 +181,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v6 with: - go-version: v1.26.1 + go-version: v1.26.2 cache: 'true' - name: Download Go modules @@ -206,7 +209,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v6 with: - go-version: v1.26.1 + go-version: v1.26.2 cache: 'true' - name: Download Go modules @@ -223,6 +226,33 @@ jobs: working-directory: ./backend/video-status/cmd run: make test_all + shared-ci: + needs: path-filter + if: ${{ github.event_name == 'pull_request' && needs.path-filter.outputs.shared == 'true' }} + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v6 + with: + go-version: v1.26.2 + cache: 'true' + + - name: Download Go modules + working-directory: ./backend/shared + run: go mod download + + - name: golangci-lint + uses: golangci/golangci-lint-action@v9 + with: + version: v2.11 + working-directory: ./backend/shared + + - name: run tests + working-directory: ./backend/shared + run: make test_all coverage: needs: path-filter @@ -233,7 +263,8 @@ jobs: needs.path-filter.outputs.transcoder_worker == 'true' || needs.path-filter.outputs.video_recombiner == 'true' || needs.path-filter.outputs.video_upload == 'true' || - needs.path-filter.outputs.video_status == 'true' + needs.path-filter.outputs.video_status == 'true' || + needs.path-filter.outputs.shared == 'true' ) steps: - uses: actions/checkout@v4 @@ -269,9 +300,19 @@ jobs: - name: Run video_status coverage working-directory: ./backend/video-status/cmd run: make coverage + + - name: Run shared coverage + working-directory: ./backend/shared/cmd + run: make coverage - name: Upload coverage to Codecov uses: codecov/codecov-action@v4 with: - files: ./backend/transcoder-worker/cmd/coverage.out,./backend/video-recombiner/cmd/coverage.out,./backend/video-upload/cmd/coverage.out,./backend/video-status/cmd/coverage.out,./backend/scene-detector/coverage.xml + files: | + ./backend/transcoder-worker/cmd/coverage.out, + ./backend/video-recombiner/cmd/coverage.out, + ./backend/video-upload/cmd/coverage.out, + ./backend/video-status/cmd/coverage.out, + ./backend/shared/coverage.out, + ./backend/scene-detector/coverage.xml token: ${{ secrets.CODECOV_TOKEN }} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 9ba623c..5c77f24 100644 --- a/.gitignore +++ b/.gitignore @@ -20,10 +20,6 @@ profile.cov # Dependency directories (remove the comment below to include it) # vendor/ -# Go workspace file -go.work -go.work.sum - # env file .env diff --git a/README.md b/README.md index b951148..94530d9 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [![CI](https://github.com/Vchen7629/Splice/actions/workflows/ci.yaml/badge.svg)](https://github.com/Vchen7629/Splice/actions/workflows/ci.yaml) [![codecov](https://codecov.io/gh/Vchen7629/Splice/graph/badge.svg?token=XT7E5YRZEX)](https://codecov.io/gh/Vchen7629/Splice) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) -[![Go](https://img.shields.io/badge/Go-1.26.1-00ADD8)](https://go.dev/) +[![Go](https://img.shields.io/badge/Go-1.26.2-00ADD8)](https://go.dev/) [![Python](https://img.shields.io/badge/Python-3.13-3776AB)](https://www.python.org/) diff --git a/backend/go.work b/backend/go.work new file mode 100644 index 0000000..e4bfc92 --- /dev/null +++ b/backend/go.work @@ -0,0 +1,10 @@ +go 1.26.2 + +use ( + ./shared + ./pipeline-tests + ./transcoder-worker + ./video-recombiner + ./video-status + ./video-upload +) \ No newline at end of file diff --git a/backend/go.work.sum b/backend/go.work.sum new file mode 100644 index 0000000..1273122 --- /dev/null +++ b/backend/go.work.sum @@ -0,0 +1,91 @@ +cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= +cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= +cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI= +github.com/containerd/typeurl/v2 v2.2.0/go.mod h1:8XOOxnyatxSWuG8OfsZXVnAF4iZfedjS/8UHSPJnX4g= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +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/envoyproxy/go-control-plane v0.14.0/go.mod h1:NcS5X47pLl/hfqxU70yPwL9ZMkUlwlKxtAohpi2wBEU= +github.com/envoyproxy/go-control-plane/envoy v1.36.0/go.mod h1:ty89S1YCCVruQAm9OtKeEkQLTb+Lkz0k8v9W0Oxsv98= +github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= +github.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA= +github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/glog v1.2.5/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +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/moby/sys/mount v0.3.4/go.mod h1:KcQJMbQdJHPlq5lcYT+/CjatWM4PuxKe+XLSVS4J6Os= +github.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4= +github.com/moby/sys/reexec v0.1.0/go.mod h1:EqjBg8F3X7iZe5pU6nRZnYCMUTXoxsjiIfHup5wYIN8= +github.com/nats-io/nats.go v1.33.1/go.mod h1:Ubdu4Nh9exXdSz0RVWRFBbRfrbSxOYd26oF0wkWclB8= +github.com/nats-io/nkeys v0.4.7/go.mod h1:kqXRgRDPlGy7nGaEDMuYzmiJCIAAWDK0IMBtDmGD0nc= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= +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/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/russross/blackfriday v1.6.0/go.mod h1:ti0ldHuxg49ri4ksnFxlkCfN+hvslNlmVHqNRXXJNAY= +github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY= +github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/detectors/gcp v1.39.0/go.mod h1:t/OGqzHBa5v6RHZwrDBJ2OirWc+4q/w2fTbLZwAKjTk= +go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= +go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= +go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= +go.opentelemetry.io/otel/sdk/metric v1.41.0/go.mod h1:HNBuSvT7ROaGtGI50ArdRLUnvRTRGniSUZbxiWxSO8Y= +go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= +golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= +golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= +golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= +golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= +golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= +golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/telemetry v0.0.0-20260311193753-579e4da9a98c/go.mod h1:TpUTTEp9frx7rTdLpC9gFG9kdI7zVLFTFFlqaH2Cncw= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= +golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= +golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= +golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/genproto/googleapis/api v0.0.0-20260120221211-b8f7ae30c516/go.mod h1:p3MLuOwURrGBRoEyFHBT3GjUwaCQVKeNqqWxlcISGdw= +google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478/go.mod h1:C6ADNqOxbgdUUeRTU+LCHDPB9ttAMCTff6auwCVa4uc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260120221211-b8f7ae30c516/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= diff --git a/backend/shared/go.mod b/backend/shared/go.mod new file mode 100644 index 0000000..87c6461 --- /dev/null +++ b/backend/shared/go.mod @@ -0,0 +1,64 @@ +module shared + +go 1.26.2 + +require ( + github.com/nats-io/nats.go v1.51.0 + github.com/stretchr/testify v1.11.1 + github.com/testcontainers/testcontainers-go v0.42.0 + github.com/testcontainers/testcontainers-go/modules/nats v0.42.0 +) + +require ( + dario.cat/mergo v1.0.2 // indirect + github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/containerd/errdefs v1.0.0 // indirect + github.com/containerd/errdefs/pkg v0.3.0 // indirect + github.com/containerd/log v0.1.0 // indirect + github.com/containerd/platforms v0.2.1 // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/docker/go-connections v0.7.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/ebitengine/purego v0.10.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/klauspost/compress v1.18.5 // indirect + github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e // indirect + github.com/magiconair/properties v1.8.10 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/go-archive v0.2.0 // indirect + github.com/moby/moby/api v1.54.1 // indirect + github.com/moby/moby/client v0.4.0 // indirect + github.com/moby/patternmatcher v0.6.1 // indirect + github.com/moby/sys/sequential v0.6.0 // indirect + github.com/moby/sys/user v0.4.0 // indirect + github.com/moby/sys/userns v0.1.0 // indirect + github.com/moby/term v0.5.2 // indirect + github.com/nats-io/nkeys v0.4.15 // indirect + github.com/nats-io/nuid v1.0.1 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect + github.com/shirou/gopsutil/v4 v4.26.3 // indirect + github.com/sirupsen/logrus v1.9.4 // indirect + github.com/tklauser/go-sysconf v0.3.16 // indirect + github.com/tklauser/numcpus v0.11.0 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 // indirect + go.opentelemetry.io/otel v1.43.0 // indirect + go.opentelemetry.io/otel/metric v1.43.0 // indirect + go.opentelemetry.io/otel/trace v1.43.0 // indirect + golang.org/x/crypto v0.50.0 // indirect + golang.org/x/sys v0.43.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/backend/shared/go.sum b/backend/shared/go.sum new file mode 100644 index 0000000..71abf55 --- /dev/null +++ b/backend/shared/go.sum @@ -0,0 +1,143 @@ +dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= +dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= +github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= +github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= +github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= +github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf0mN39c= +github.com/docker/go-connections v0.7.0/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU= +github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +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/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= +github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e h1:Q6MvJtQK/iRcRtzAscm/zF23XxJlbECiGPyRicsX+Ak= +github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= +github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= +github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/go-archive v0.2.0 h1:zg5QDUM2mi0JIM9fdQZWC7U8+2ZfixfTYoHL7rWUcP8= +github.com/moby/go-archive v0.2.0/go.mod h1:mNeivT14o8xU+5q1YnNrkQVpK+dnNe/K6fHqnTg4qPU= +github.com/moby/moby/api v1.54.1 h1:TqVzuJkOLsgLDDwNLmYqACUuTehOHRGKiPhvH8V3Nn4= +github.com/moby/moby/api v1.54.1/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs= +github.com/moby/moby/client v0.4.0 h1:S+2XegzHQrrvTCvF6s5HFzcrywWQmuVnhOXe2kiWjIw= +github.com/moby/moby/client v0.4.0/go.mod h1:QWPbvWchQbxBNdaLSpoKpCdf5E+WxFAgNHogCWDoa7g= +github.com/moby/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x084U= +github.com/moby/patternmatcher v0.6.1/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= +github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= +github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= +github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= +github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= +github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= +github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= +github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= +github.com/nats-io/nats.go v1.51.0 h1:ByW84XTz6W03GSSsygsZcA+xgKK8vPGaa/FCAAEHnAI= +github.com/nats-io/nats.go v1.51.0/go.mod h1:26HypzazeOkyO3/mqd1zZd53STJN0EjCYF9Uy2ZOBno= +github.com/nats-io/nkeys v0.4.15 h1:JACV5jRVO9V856KOapQ7x+EY8Jo3qw1vJt/9Jpwzkk4= +github.com/nats-io/nkeys v0.4.15/go.mod h1:CpMchTXC9fxA5zrMo4KpySxNjiDVvr8ANOSZdiNfUrs= +github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/shirou/gopsutil/v4 v4.26.3 h1:2ESdQt90yU3oXF/CdOlRCJxrP+Am1aBYubTMTfxJ1qc= +github.com/shirou/gopsutil/v4 v4.26.3/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ= +github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= +github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= +github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4= +github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/testcontainers/testcontainers-go v0.42.0 h1:He3IhTzTZOygSXLJPMX7n44XtK+qhjat1nI9cneBbUY= +github.com/testcontainers/testcontainers-go v0.42.0/go.mod h1:vZjdY1YmUA1qEForxOIOazfsrdyORJAbhi0bp8plN30= +github.com/testcontainers/testcontainers-go/modules/nats v0.42.0 h1:WQR0+1r4GkM5QgOBoLxlP41empovt5PxtaiDpC0G7ow= +github.com/testcontainers/testcontainers-go/modules/nats v0.42.0/go.mod h1:ZAI9iisjDNJmcRcycQFKSLpiBN9u2g1v9AJRq1afriE= +github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA= +github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI= +github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw= +github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 h1:CqXxU8VOmDefoh0+ztfGaymYbhdB/tT3zs79QaZTNGY= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0/go.mod h1:BuhAPThV8PBHBvg8ZzZ/Ok3idOdhWIodywz2xEcRbJo= +go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= +go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= +go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= +go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= +go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= +go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= +go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= +go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= +go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= +go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= +golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= +golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= +golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY= +golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/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/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= +gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= +pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk= +pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= diff --git a/backend/video-recombiner/internal/service/nat_messages.go b/backend/shared/handler/nats_messages.go similarity index 79% rename from backend/video-recombiner/internal/service/nat_messages.go rename to backend/shared/handler/nats_messages.go index dd58d18..0e6e1f1 100644 --- a/backend/video-recombiner/internal/service/nat_messages.go +++ b/backend/shared/handler/nats_messages.go @@ -1,4 +1,4 @@ -package service +package handler type ChunkCompleteMessage struct { JobID string `json:"job_id"` @@ -7,6 +7,6 @@ type ChunkCompleteMessage struct { StorageURL string `json:"storage_url"` } -type VideoProcessingCompleteMessage struct { +type JobCompleteMessage struct { JobID string `json:"job_id"` } diff --git a/backend/video-upload/internal/handler/publisher.go b/backend/shared/handler/publisher.go similarity index 65% rename from backend/video-upload/internal/handler/publisher.go rename to backend/shared/handler/publisher.go index 11cf23b..1622b45 100644 --- a/backend/video-upload/internal/handler/publisher.go +++ b/backend/shared/handler/publisher.go @@ -4,14 +4,12 @@ import ( "context" "encoding/json" "fmt" - "video-upload/internal/service" "github.com/nats-io/nats.go/jetstream" ) -const pubSubject = "jobs.video.scene-split" - -func PublishVideoMetadata(js jetstream.JetStream, msg service.SceneSplitMessage) error { +// Returns a function that publishes a msg to JetStream +func PublishJobComplete(js jetstream.JetStream, msg any, pubSubject string) error { data, err := json.Marshal(msg) if err != nil { return fmt.Errorf("marshall chunk error: %w", err) @@ -22,5 +20,4 @@ func PublishVideoMetadata(js jetstream.JetStream, msg service.SceneSplitMessage) return err } return nil - } diff --git a/backend/transcoder-worker/internal/handler/publisher_integration_test.go b/backend/shared/handler/publisher_integration_test.go similarity index 83% rename from backend/transcoder-worker/internal/handler/publisher_integration_test.go rename to backend/shared/handler/publisher_integration_test.go index 819f4c6..c0215ff 100644 --- a/backend/transcoder-worker/internal/handler/publisher_integration_test.go +++ b/backend/shared/handler/publisher_integration_test.go @@ -1,15 +1,13 @@ //go:build integration -package handler_test +package handler import ( "context" "encoding/json" + "shared/test" "testing" "time" - "transcoder-worker/internal/handler" - "transcoder-worker/internal/service" - "transcoder-worker/internal/test" "github.com/nats-io/nats.go" "github.com/nats-io/nats.go/jetstream" @@ -29,20 +27,19 @@ func TestPublishChunkCompleteI(t *testing.T) { require.NoError(t, err) t.Cleanup(func() { _ = sub.Unsubscribe() }) - msg := service.ChunkCompleteMessage{ + msg := ChunkCompleteMessage{ JobID: "job-1", ChunkIndex: 2, TotalChunks: 1, StorageURL: "/output/chunk-2.mp4", } - fn := handler.PublishChunkComplete(js) - err = fn(msg) + err = PublishJobComplete(js, msg, "jobs.chunks.complete") require.NoError(t, err) select { case data := <-received: - var got service.ChunkCompleteMessage + var got ChunkCompleteMessage require.NoError(t, json.Unmarshal(data, &got)) assert.Equal(t, msg, got) case <-time.After(3 * time.Second): @@ -72,13 +69,12 @@ func TestPublishChunkCompleteI(t *testing.T) { js, err := jetstream.New(nc) require.NoError(t, err) - fn := handler.PublishChunkComplete(js) - err = fn(service.ChunkCompleteMessage{ + err = PublishJobComplete(js, ChunkCompleteMessage{ JobID: "job-1", ChunkIndex: 0, TotalChunks: 1, StorageURL: "/output/chunk-0.mp4", - }) + }, "jobs.chunks.complete") assert.Error(t, err) }) diff --git a/backend/transcoder-worker/internal/handler/publisher_unit_test.go b/backend/shared/handler/publisher_unit_test.go similarity index 80% rename from backend/transcoder-worker/internal/handler/publisher_unit_test.go rename to backend/shared/handler/publisher_unit_test.go index ae47b3c..0e59010 100644 --- a/backend/transcoder-worker/internal/handler/publisher_unit_test.go +++ b/backend/shared/handler/publisher_unit_test.go @@ -1,13 +1,11 @@ //go:build unit -package handler_test +package handler import ( "context" "errors" "testing" - "transcoder-worker/internal/handler" - "transcoder-worker/internal/service" "github.com/nats-io/nats.go/jetstream" "github.com/stretchr/testify/assert" @@ -28,13 +26,12 @@ func TestPublishChunkComplete(t *testing.T) { publishErr := errors.New("nats publish failed") mock := &mockJetStream{publishErr: publishErr} - fn := handler.PublishChunkComplete(mock) - err := fn(service.ChunkCompleteMessage{ + err := PublishJobComplete(mock, ChunkCompleteMessage{ JobID: "job-1", ChunkIndex: 0, TotalChunks: 0, StorageURL: "/output/chunk-0.mp4", - }) + }, "jobs.chunks.complete") assert.ErrorIs(t, err, publishErr) }) diff --git a/backend/shared/handler/subscriber.go b/backend/shared/handler/subscriber.go new file mode 100644 index 0000000..c1009bf --- /dev/null +++ b/backend/shared/handler/subscriber.go @@ -0,0 +1,40 @@ +package handler + +import ( + "context" + "fmt" + "time" + + "github.com/nats-io/nats.go/jetstream" +) + +// creates a durable consumer to listen to nats subject to consume messages +func CreateDurableConsumer(js jetstream.JetStream, subSubject, consName string) (jetstream.Consumer, error) { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + streamName, err := js.StreamNameBySubject(ctx, subSubject) + if err != nil { + return nil, fmt.Errorf("no stream found for subject: %s: %w", subSubject, err) + } + + stream, err := js.Stream(ctx, streamName) + if err != nil { + return nil, err + } + + cons, err := stream.CreateOrUpdateConsumer(ctx, jetstream.ConsumerConfig{ + Name: consName, + Durable: consName, + FilterSubject: subSubject, + AckPolicy: jetstream.AckExplicitPolicy, + MaxAckPending: 10, // worker wont recieve more than 10 inflight messages + MaxDeliver: 3, + AckWait: 30 * time.Second, + }) + if err != nil { + return nil, err + } + + return cons, nil +} diff --git a/backend/shared/handler/subscriber_integration_test.go b/backend/shared/handler/subscriber_integration_test.go new file mode 100644 index 0000000..4e54623 --- /dev/null +++ b/backend/shared/handler/subscriber_integration_test.go @@ -0,0 +1,34 @@ +//go:build integration + +package handler_test + +import ( + "context" + "shared/handler" + "shared/test" + "testing" + "time" + + "github.com/nats-io/nats.go/jetstream" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestCorrectConfig(t *testing.T) { + ctx := context.Background() + js, _ := test.SetupNats(t) + + cons, err := handler.CreateDurableConsumer(js, "jobs.chunks.complete", "video-recombiner") + require.NoError(t, err) + + info, err := cons.Info(ctx) + require.NoError(t, err) + + assert.Equal(t, "video-recombiner", info.Config.Name) + assert.Equal(t, "video-recombiner", info.Config.Durable) + assert.Equal(t, "jobs.chunks.complete", info.Config.FilterSubject) + assert.Equal(t, jetstream.AckExplicitPolicy, info.Config.AckPolicy) + assert.Equal(t, 10, info.Config.MaxAckPending) + assert.Equal(t, 3, info.Config.MaxDeliver) + assert.Equal(t, 30*time.Second, info.Config.AckWait) +} diff --git a/backend/shared/handler/subscriber_unit_test.go b/backend/shared/handler/subscriber_unit_test.go new file mode 100644 index 0000000..1600618 --- /dev/null +++ b/backend/shared/handler/subscriber_unit_test.go @@ -0,0 +1,50 @@ +//go:build unit + +package handler_test + +import ( + "errors" + "shared/handler" + "shared/test" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestReturnError(t *testing.T) { + streamNameErr := errors.New("no stream") + streamErr := errors.New("stream error") + consumerErr := errors.New("consumer error") + + tests := []struct { + name string + js *test.MockJS + wantErr error + }{ + { + name: "stream name lookup failure returns error", + js: &test.MockJS{JStreamNameErr: streamNameErr}, + wantErr: streamNameErr, + }, + { + name: "stream failure returns error", + js: &test.MockJS{JStreamErr: streamErr}, + wantErr: streamErr, + }, + { + name: "create consumer failure returns error", + js: &test.MockJS{JStream: &test.MockStream{ConsumerErr: consumerErr}}, + wantErr: consumerErr, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + _, err := handler.CreateDurableConsumer(tc.js, "idk", "idk") + + require.Error(t, err) + assert.ErrorIs(t, err, tc.wantErr) + }) + } +} diff --git a/backend/video-recombiner/internal/handler/job_status_kv.go b/backend/shared/kv/job_status_kv.go similarity index 67% rename from backend/video-recombiner/internal/handler/job_status_kv.go rename to backend/shared/kv/job_status_kv.go index cc4904e..405eea5 100644 --- a/backend/video-recombiner/internal/handler/job_status_kv.go +++ b/backend/shared/kv/job_status_kv.go @@ -1,4 +1,4 @@ -package handler +package kv import ( "context" @@ -13,7 +13,7 @@ import ( var osExit = os.Exit // connect to existing job status kv to publishing the processing stage update msgs -func ConnectJobStatusKV(js jetstream.JetStream, logger *slog.Logger) jetstream.KeyValue { +func ConnectJobStatus(js jetstream.JetStream, logger *slog.Logger) jetstream.KeyValue { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() @@ -26,11 +26,14 @@ func ConnectJobStatusKV(js jetstream.JetStream, logger *slog.Logger) jetstream.K return kv } -func UpdateJobStatusKV(jobStatusKV jetstream.KeyValue, JobID string, logger *slog.Logger) error { +// publish a msg to the job status KV with the current processing stage and PROCESSING msg +func UpdateJobStatus( + jobStatusKV jetstream.KeyValue, jobStage, jobID string, logger *slog.Logger, +) error { status, err := json.Marshal(struct { State string `json:"state"` Stage string `json:"stage"` - }{State: "PROCESSING", Stage: "video-recombiner"}) + }{State: "PROCESSING", Stage: jobStage}) if err != nil { logger.Error("error marshalling status text", "err", err) return err @@ -39,9 +42,9 @@ func UpdateJobStatusKV(jobStatusKV jetstream.KeyValue, JobID string, logger *slo ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() - _, err = jobStatusKV.Put(ctx, JobID, status) + _, err = jobStatusKV.Put(ctx, jobID, status) if err != nil { - logger.Error("failed to write job status to jobStatus kv", "job_id", JobID, "err", err) + logger.Error("failed to write job status to jobStatus kv", "job_id", jobID, "err", err) return err } diff --git a/backend/video-recombiner/internal/handler/job_status_kv_integration_test.go b/backend/shared/kv/job_status_kv_integration_test.go similarity index 79% rename from backend/video-recombiner/internal/handler/job_status_kv_integration_test.go rename to backend/shared/kv/job_status_kv_integration_test.go index c1ebf0e..720ac08 100644 --- a/backend/video-recombiner/internal/handler/job_status_kv_integration_test.go +++ b/backend/shared/kv/job_status_kv_integration_test.go @@ -1,12 +1,12 @@ //go:build integration -package handler +package kv import ( "context" "os" + "shared/test" "testing" - "video-recombiner/internal/test" "github.com/nats-io/nats.go/jetstream" "github.com/stretchr/testify/assert" @@ -14,7 +14,7 @@ import ( ) // connects to existing job-status bucket -func TestConnectJobStatusKV(t *testing.T) { +func TestConnectJobStatus(t *testing.T) { t.Run("connects to existing job-status bucket", func(t *testing.T) { js, _ := test.SetupNats(t) @@ -23,7 +23,7 @@ func TestConnectJobStatusKV(t *testing.T) { }) require.NoError(t, err) - kv := ConnectJobStatusKV(js, test.SilentLogger()) + kv := ConnectJobStatus(js, test.SilentLogger()) assert.NotNil(t, kv) }) @@ -35,7 +35,7 @@ func TestConnectJobStatusKV(t *testing.T) { osExit = func(c int) { code = c } t.Cleanup(func() { osExit = os.Exit }) - ConnectJobStatusKV(js, test.SilentLogger()) + ConnectJobStatus(js, test.SilentLogger()) assert.Equal(t, 1, code) }) diff --git a/backend/video-recombiner/internal/handler/job_status_kv_unit_test.go b/backend/shared/kv/job_status_kv_unit_test.go similarity index 77% rename from backend/video-recombiner/internal/handler/job_status_kv_unit_test.go rename to backend/shared/kv/job_status_kv_unit_test.go index dbd6268..ab1c46c 100644 --- a/backend/video-recombiner/internal/handler/job_status_kv_unit_test.go +++ b/backend/shared/kv/job_status_kv_unit_test.go @@ -1,18 +1,18 @@ //go:build unit -package handler_test +package kv_test import ( "errors" + "shared/kv" + "shared/test" "testing" - "video-recombiner/internal/handler" - "video-recombiner/internal/test" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func TestUpdateJobStatusKV(t *testing.T) { +func TestUpdateJobStatus(t *testing.T) { tests := []struct { name string kv *test.MockKV @@ -34,7 +34,7 @@ func TestUpdateJobStatusKV(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - err := handler.UpdateJobStatusKV(tc.kv, "job-1", test.SilentLogger()) + err := kv.UpdateJobStatus(tc.kv, "video-upload", "job-1", test.SilentLogger()) if tc.wantErr { require.Error(t, err) diff --git a/backend/transcoder-worker/internal/handler/msg_processed_kv.go b/backend/shared/kv/msg_processed_kv.go similarity index 91% rename from backend/transcoder-worker/internal/handler/msg_processed_kv.go rename to backend/shared/kv/msg_processed_kv.go index 600c9ce..ec282b4 100644 --- a/backend/transcoder-worker/internal/handler/msg_processed_kv.go +++ b/backend/shared/kv/msg_processed_kv.go @@ -1,20 +1,17 @@ -package handler +package kv import ( "context" "errors" "fmt" "log/slog" - "os" "time" "github.com/nats-io/nats.go/jetstream" ) -var osExit = os.Exit - // Create the Msg Processed KV store for idempotency -func CreateMsgProcessedKV(js jetstream.JetStream, logger *slog.Logger) jetstream.KeyValue { +func CreateMsgProcessedKV(bucketName string, js jetstream.JetStream, logger *slog.Logger) jetstream.KeyValue { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() diff --git a/backend/transcoder-worker/internal/handler/msg_processed_kv_integration_test.go b/backend/shared/kv/msg_processed_kv_integration_test.go similarity index 74% rename from backend/transcoder-worker/internal/handler/msg_processed_kv_integration_test.go rename to backend/shared/kv/msg_processed_kv_integration_test.go index 69f464c..6e98af9 100644 --- a/backend/transcoder-worker/internal/handler/msg_processed_kv_integration_test.go +++ b/backend/shared/kv/msg_processed_kv_integration_test.go @@ -1,10 +1,10 @@ //go:build integration -package handler +package kv import ( + "shared/test" "testing" - "transcoder-worker/internal/test" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -14,7 +14,7 @@ import ( func TestCreateMsgProcessedKV(t *testing.T) { js, _ := test.SetupNats(t) - kv := CreateMsgProcessedKV(js, test.SilentLogger()) + kv := CreateMsgProcessedKV("transcode-chunk-job-processed", js, test.SilentLogger()) require.NotNil(t, kv) assert.Equal(t, "transcode-chunk-job-processed", kv.Bucket()) diff --git a/backend/transcoder-worker/internal/handler/msg_processed_kv_unit_test.go b/backend/shared/kv/msg_processed_kv_unit_test.go similarity index 59% rename from backend/transcoder-worker/internal/handler/msg_processed_kv_unit_test.go rename to backend/shared/kv/msg_processed_kv_unit_test.go index e7df2d4..c1fd784 100644 --- a/backend/transcoder-worker/internal/handler/msg_processed_kv_unit_test.go +++ b/backend/shared/kv/msg_processed_kv_unit_test.go @@ -1,12 +1,12 @@ //go:build unit -package handler_test +package kv_test import ( "errors" + "shared/kv" + "shared/test" "testing" - "transcoder-worker/internal/handler" - "transcoder-worker/internal/test" "github.com/nats-io/nats.go/jetstream" "github.com/stretchr/testify/assert" @@ -15,36 +15,36 @@ import ( func TestCheckChunkProcessed(t *testing.T) { t.Run("returns false when key not found", func(t *testing.T) { - kv := &test.MockKV{GetFound: false} + mockKV := &test.MockKV{GetFound: false} - processed, err := handler.CheckChunkProcessed(kv, "job-1", 0) + processed, err := kv.CheckChunkProcessed(mockKV, "job-1", 0) require.NoError(t, err) assert.False(t, processed) }) t.Run("returns true when key exists", func(t *testing.T) { - kv := &test.MockKV{GetFound: true} + mockKV := &test.MockKV{GetFound: true} - processed, err := handler.CheckChunkProcessed(kv, "job-1", 0) + processed, err := kv.CheckChunkProcessed(mockKV, "job-1", 0) require.NoError(t, err) assert.True(t, processed) }) t.Run("returns error on unexpected kv failure", func(t *testing.T) { - kv := &test.MockKV{GetErr: errors.New("kv unavailable")} + mockKV := &test.MockKV{GetErr: errors.New("kv unavailable")} - _, err := handler.CheckChunkProcessed(kv, "job-1", 0) + _, err := kv.CheckChunkProcessed(mockKV, "job-1", 0) require.Error(t, err) assert.ErrorContains(t, err, "failed") }) t.Run("does not return error for ErrKeyNotFound", func(t *testing.T) { - kv := &test.MockKV{GetErr: jetstream.ErrKeyNotFound} + mockKV := &test.MockKV{GetErr: jetstream.ErrKeyNotFound} - processed, err := handler.CheckChunkProcessed(kv, "job-1", 0) + processed, err := kv.CheckChunkProcessed(mockKV, "job-1", 0) require.NoError(t, err) assert.False(t, processed) @@ -53,9 +53,9 @@ func TestCheckChunkProcessed(t *testing.T) { t.Run("uses correct key format job_id.chunk_index", func(t *testing.T) { // Key lookup for job "abc" chunk 3 must use "abc.3". // We verify by having GetFound=true and confirming no error path is hit. - kv := &test.MockKV{GetFound: true} + mockKV := &test.MockKV{GetFound: true} - processed, err := handler.CheckChunkProcessed(kv, "abc", 3) + processed, err := kv.CheckChunkProcessed(mockKV, "abc", 3) require.NoError(t, err) assert.True(t, processed) @@ -64,26 +64,26 @@ func TestCheckChunkProcessed(t *testing.T) { func TestAddChunkProcessed(t *testing.T) { t.Run("returns nil on success", func(t *testing.T) { - kv := &test.MockKV{} + mockKV := &test.MockKV{} - err := handler.AddChunkProcessed(kv, "job-1", 0) + err := kv.AddChunkProcessed(mockKV, "job-1", 0) require.NoError(t, err) }) t.Run("writes correct key job_id.chunk_index", func(t *testing.T) { - kv := &test.MockKV{} + mockKV := &test.MockKV{} - err := handler.AddChunkProcessed(kv, "job-abc", 2) + err := kv.AddChunkProcessed(mockKV, "job-abc", 2) require.NoError(t, err) - assert.Equal(t, "job-abc.2", kv.PutKey) + assert.Equal(t, "job-abc.2", mockKV.PutKey) }) t.Run("returns error on kv failure", func(t *testing.T) { - kv := &test.MockKV{PutErr: errors.New("put failed")} + mockKV := &test.MockKV{PutErr: errors.New("put failed")} - err := handler.AddChunkProcessed(kv, "job-1", 0) + err := kv.AddChunkProcessed(mockKV, "job-1", 0) require.Error(t, err) assert.ErrorContains(t, err, "failed") diff --git a/backend/shared/makefile b/backend/shared/makefile new file mode 100644 index 0000000..751c953 --- /dev/null +++ b/backend/shared/makefile @@ -0,0 +1,20 @@ +test_all: integration unit + +PKGS := ./handler/... ./kv/... ./middleware/... ./storage/... + +format: + go fmt ${PKGS} . + go fmt ./test/... . + +lint: + golangci-lint run ${PKGS} + golangci-lint run ./test/... + +coverage: + go test -tags=unit,integration ${PKGS} -v -coverprofile=coverage.out -covermode=atomic + +integration: + go test -tags integration ${PKGS} + +unit: + go test -tags unit ${PKGS} \ No newline at end of file diff --git a/backend/video-upload/internal/middleware/cors.go b/backend/shared/middleware/cors.go similarity index 100% rename from backend/video-upload/internal/middleware/cors.go rename to backend/shared/middleware/cors.go diff --git a/backend/video-upload/internal/middleware/cors_integration_test.go b/backend/shared/middleware/cors_integration_test.go similarity index 83% rename from backend/video-upload/internal/middleware/cors_integration_test.go rename to backend/shared/middleware/cors_integration_test.go index 8b672a1..b584198 100644 --- a/backend/video-upload/internal/middleware/cors_integration_test.go +++ b/backend/shared/middleware/cors_integration_test.go @@ -4,25 +4,15 @@ package middleware_test import ( "net/http" - "net/http/httptest" + "shared/test" "testing" - "video-upload/internal/middleware" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func newCorsServer(t *testing.T) *httptest.Server { - t.Helper() - mux := http.NewServeMux() - mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - }) - return httptest.NewServer(middleware.Cors(mux)) -} - func TestCorsIntegration(t *testing.T) { - ts := newCorsServer(t) + ts := test.NewCorsServer(t) defer ts.Close() tests := []struct { diff --git a/backend/video-status/internal/middleware/cors_unit_test.go b/backend/shared/middleware/cors_unit_test.go similarity index 98% rename from backend/video-status/internal/middleware/cors_unit_test.go rename to backend/shared/middleware/cors_unit_test.go index 75ea06b..153299b 100644 --- a/backend/video-status/internal/middleware/cors_unit_test.go +++ b/backend/shared/middleware/cors_unit_test.go @@ -5,8 +5,8 @@ package middleware_test import ( "net/http" "net/http/httptest" + "shared/middleware" "testing" - "video-status/internal/middleware" "github.com/stretchr/testify/assert" ) diff --git a/backend/video-status/internal/middleware/logging.go b/backend/shared/middleware/logging.go similarity index 78% rename from backend/video-status/internal/middleware/logging.go rename to backend/shared/middleware/logging.go index e375c0f..1399578 100644 --- a/backend/video-status/internal/middleware/logging.go +++ b/backend/shared/middleware/logging.go @@ -8,35 +8,35 @@ import ( "time" ) +// General Structured logger for code +func StructuredLogger(prodMode bool, serviceName string) *slog.Logger { + level := slog.LevelDebug + if prodMode { + level = slog.LevelInfo + } + h := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: level}) + + return slog.New(h).With("service", serviceName) +} + // wrapper to extend http response writer to expose // the status codes -type WrappedWriter struct { +type wrappedWriter struct { http.ResponseWriter StatusCode int } -func (w *WrappedWriter) WriteHeader(statuscode int) { +func (w *wrappedWriter) WriteHeader(statuscode int) { w.ResponseWriter.WriteHeader(statuscode) w.StatusCode = statuscode } -// General Structured logger for code -func StructuredLogger(prodMode bool) *slog.Logger { - level := slog.LevelDebug - if prodMode { - level = slog.LevelInfo - } - h := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: level}) - - return slog.New(h).With("service", "video-status") -} - // logging middleware to track status codes, the url path, and response latency func ApiRequestLogging(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { start := time.Now() - wrapped := &WrappedWriter{ + wrapped := &wrappedWriter{ ResponseWriter: w, StatusCode: http.StatusOK, } diff --git a/backend/video-upload/internal/middleware/logging_unit_test.go b/backend/shared/middleware/logging_unit_test.go similarity index 89% rename from backend/video-upload/internal/middleware/logging_unit_test.go rename to backend/shared/middleware/logging_unit_test.go index d0b9ccc..6c0443a 100644 --- a/backend/video-upload/internal/middleware/logging_unit_test.go +++ b/backend/shared/middleware/logging_unit_test.go @@ -1,6 +1,6 @@ //go:build unit -package middleware_test +package middleware import ( "bytes" @@ -11,15 +11,30 @@ import ( "net/http/httptest" "strings" "testing" - "video-upload/internal/middleware" "github.com/stretchr/testify/assert" ) +func TestStructuredLogger(t *testing.T) { + + t.Run("prod mode set to false should enable debug level", func(t *testing.T) { + logger := StructuredLogger(false, "transcoder-worker") + + assert.True(t, logger.Enabled(context.Background(), slog.LevelDebug)) + }) + + t.Run("prod mode set to true should disable debug level", func(t *testing.T) { + logger := StructuredLogger(true, "transcoder-worker") + + assert.False(t, logger.Enabled(context.Background(), slog.LevelDebug)) + assert.True(t, logger.Enabled(context.Background(), slog.LevelInfo)) + }) +} + func TestWriteHeader(t *testing.T) { t.Run("Captures status code properly", func(t *testing.T) { recorder := httptest.NewRecorder() - wrapped := &middleware.WrappedWriter{ + wrapped := &wrappedWriter{ ResponseWriter: recorder, StatusCode: http.StatusOK, } @@ -31,7 +46,7 @@ func TestWriteHeader(t *testing.T) { t.Run("Forwards to responsewriter", func(t *testing.T) { recorder := httptest.NewRecorder() - wrapped := &middleware.WrappedWriter{ + wrapped := &wrappedWriter{ ResponseWriter: recorder, StatusCode: http.StatusOK, } @@ -43,7 +58,7 @@ func TestWriteHeader(t *testing.T) { t.Run("Starts at 200 status code", func(t *testing.T) { recorder := httptest.NewRecorder() - wrapped := &middleware.WrappedWriter{ + wrapped := &wrappedWriter{ ResponseWriter: recorder, StatusCode: http.StatusOK, } @@ -60,7 +75,7 @@ func TestApiRequestLogging(t *testing.T) { w.WriteHeader(http.StatusOK) }) - logging := middleware.ApiRequestLogging(mockHandler) + logging := ApiRequestLogging(mockHandler) req := httptest.NewRequest(http.MethodGet, "/test", nil) recorder := httptest.NewRecorder() @@ -79,7 +94,7 @@ func TestApiRequestLogging(t *testing.T) { w.WriteHeader(http.StatusOK) }) - logging := middleware.ApiRequestLogging(mockHandler) + logging := ApiRequestLogging(mockHandler) req := httptest.NewRequest(http.MethodPost, "/api/products", nil) recorder := httptest.NewRecorder() @@ -99,7 +114,7 @@ func TestApiRequestLogging(t *testing.T) { w.WriteHeader(http.StatusNotFound) }) - logging := middleware.ApiRequestLogging(mockHandler) + logging := ApiRequestLogging(mockHandler) req := httptest.NewRequest(http.MethodGet, "/test-path", nil) recorder := httptest.NewRecorder() @@ -115,19 +130,3 @@ func TestApiRequestLogging(t *testing.T) { }) } - -func TestStructuredLogger(t *testing.T) { - - t.Run("prod mode set to false should enable debug level", func(t *testing.T) { - logger := middleware.StructuredLogger(false) - - assert.True(t, logger.Enabled(context.Background(), slog.LevelDebug)) - }) - - t.Run("prod mode set to true should disable debug level", func(t *testing.T) { - logger := middleware.StructuredLogger(true) - - assert.False(t, logger.Enabled(context.Background(), slog.LevelDebug)) - assert.True(t, logger.Enabled(context.Background(), slog.LevelInfo)) - }) -} diff --git a/backend/transcoder-worker/internal/storage/health_check.go b/backend/shared/storage/health_check.go similarity index 100% rename from backend/transcoder-worker/internal/storage/health_check.go rename to backend/shared/storage/health_check.go diff --git a/backend/video-recombiner/internal/storage/health_check_integration_test.go b/backend/shared/storage/health_check_integration_test.go similarity index 90% rename from backend/video-recombiner/internal/storage/health_check_integration_test.go rename to backend/shared/storage/health_check_integration_test.go index 10d3878..6e8ac47 100644 --- a/backend/video-recombiner/internal/storage/health_check_integration_test.go +++ b/backend/shared/storage/health_check_integration_test.go @@ -4,9 +4,9 @@ package storage_test import ( "os" + "shared/storage" + "shared/test" "testing" - "video-recombiner/internal/storage" - "video-recombiner/internal/test" "github.com/stretchr/testify/assert" ) diff --git a/backend/transcoder-worker/internal/storage/queries.go b/backend/shared/storage/queries.go similarity index 79% rename from backend/transcoder-worker/internal/storage/queries.go rename to backend/shared/storage/queries.go index 050f838..e679365 100644 --- a/backend/transcoder-worker/internal/storage/queries.go +++ b/backend/shared/storage/queries.go @@ -10,14 +10,11 @@ import ( "strings" ) -// save the video chunk transcoded to a target resolution back onto seaweedfs storage -func SaveTranscodedVideoChunk(baseStorageURL, filePath, jobID string) (string, error) { - fileName := filepath.Base(filePath) - url := fmt.Sprintf("%s/%s/processed/%s", baseStorageURL, jobID, fileName) - +// save the video chunk to seaweedfs storage +func UploadVideoChunk(url, filePath string) (string, error) { file, err := os.Open(filePath) if err != nil { - return "", fmt.Errorf("error opening transcoded video file: %w", err) + return "", fmt.Errorf("error opening video file: %w", err) } defer func() { err := file.Close() @@ -52,8 +49,8 @@ func SaveTranscodedVideoChunk(baseStorageURL, filePath, jobID string) (string, e var removeAll = os.RemoveAll -// fetch the unprocessed video chunk seaweedfs storage -func GetUnprocessedVideoChunk(storageURL, jobID string) (string, error) { +// fetch the video chunk seaweedfs storage +func GetVideoChunk(storageURL, fileName string) (string, error) { resp, err := http.Get(storageURL) if err != nil { return "", fmt.Errorf("error connecting to seedweedfs, %w", err) @@ -75,7 +72,7 @@ func GetUnprocessedVideoChunk(storageURL, jobID string) (string, error) { } filename := storageURL[strings.LastIndex(storageURL, "/")+1:] - jobDir := filepath.Join("/tmp/temp-unprocessed-" + jobID) + jobDir := filepath.Join("/tmp/" + fileName) err = os.MkdirAll(jobDir, 0755) if err != nil { diff --git a/backend/video-recombiner/internal/storage/queries_integration_test.go b/backend/shared/storage/queries_integration_test.go similarity index 60% rename from backend/video-recombiner/internal/storage/queries_integration_test.go rename to backend/shared/storage/queries_integration_test.go index 463e64a..1428e0a 100644 --- a/backend/video-recombiner/internal/storage/queries_integration_test.go +++ b/backend/shared/storage/queries_integration_test.go @@ -7,15 +7,16 @@ import ( "io" "net/http" "os" + "path/filepath" + "shared/storage" + "shared/test" "testing" - "video-recombiner/internal/storage" - "video-recombiner/internal/test" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func TestGetProcessedVideoChunkIntegration(t *testing.T) { +func TestGetVideoChunkIntegration(t *testing.T) { t.Run("fetches video and writes to correct local path with matching content", func(t *testing.T) { jobID := "job-fetch" filename := "testvideo.mp4" @@ -23,13 +24,13 @@ func TestGetProcessedVideoChunkIntegration(t *testing.T) { require.NoError(t, err) test.SeedProcessedVideo(t, sharedFilerUrl, jobID, filename, videoContent) - storageURL := fmt.Sprintf("%s/%s/%s/processed", sharedFilerUrl, jobID, filename) - t.Cleanup(func() { os.RemoveAll("/tmp/processed_chunk-" + jobID) }) + storageURL := fmt.Sprintf("%s/%s/processed/%s", sharedFilerUrl, jobID, filename) + t.Cleanup(func() { os.RemoveAll("/tmp/" + jobID) }) - filePath, err := storage.GetProcessedVideoChunk(storageURL, jobID) + filePath, err := storage.GetVideoChunk(storageURL, jobID) require.NoError(t, err) - assert.Equal(t, "/tmp/processed_chunk-"+jobID+"/"+filename, filePath) + assert.Equal(t, "/tmp/"+jobID+"/"+filename, filePath) assert.FileExists(t, filePath) got, err := os.ReadFile(filePath) @@ -39,32 +40,31 @@ func TestGetProcessedVideoChunkIntegration(t *testing.T) { t.Run("nonexistent file returns error", func(t *testing.T) { jobID := "job-missing" - storageURL := sharedFilerUrl + "/" + jobID + "/nonexistent.mp4" - t.Cleanup(func() { os.RemoveAll("/tmp/processed_chunk-" + jobID) }) + storageURL := fmt.Sprintf("%s/%s/processed/nonexistent.mp4", sharedFilerUrl, jobID) + t.Cleanup(func() { os.RemoveAll("/tmp/" + jobID) }) - filePath, err := storage.GetProcessedVideoChunk(storageURL, jobID) + filePath, err := storage.GetVideoChunk(storageURL, jobID) require.Error(t, err) assert.Empty(t, filePath) }) } -func TestUploadRecombinedVideo(t *testing.T) { +func TestUploadVideoChunk(t *testing.T) { t.Run("uploads file properly", func(t *testing.T) { videoFile := test.OpenTestVideo(t) + fileName := filepath.Base(videoFile.Name()) + uploadURL := fmt.Sprintf("%s/job-upload/processed/%s", sharedFilerUrl, fileName) - url, err := storage.UploadRecombinedVideo(sharedFilerUrl, videoFile.Name(), "job-upload") + url, err := storage.UploadVideoChunk(uploadURL, videoFile.Name()) require.NoError(t, err) - - expectedURL := fmt.Sprintf("%s/job-upload/testvideo.mp4/processed", sharedFilerUrl) - assert.Equal(t, expectedURL, url) + assert.Equal(t, uploadURL, url) resp, err := http.Get(url) require.NoError(t, err) defer func() { err := resp.Body.Close() - require.NoError(t, err) }() require.Equal(t, http.StatusOK, resp.StatusCode) diff --git a/backend/transcoder-worker/internal/storage/queries_unit_test.go b/backend/shared/storage/queries_unit_test.go similarity index 79% rename from backend/transcoder-worker/internal/storage/queries_unit_test.go rename to backend/shared/storage/queries_unit_test.go index cc17f37..4d37108 100644 --- a/backend/transcoder-worker/internal/storage/queries_unit_test.go +++ b/backend/shared/storage/queries_unit_test.go @@ -15,7 +15,7 @@ import ( "github.com/stretchr/testify/require" ) -func TestSaveTranscodedVideoChunkFileErrors(t *testing.T) { +func TestUploadVideoChunkFileErrors(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) })) @@ -29,7 +29,7 @@ func TestSaveTranscodedVideoChunkFileErrors(t *testing.T) { { name: "nonexistent file returns error", filePath: "/nonexistent/path/chunk.mp4", - errContains: "error opening transcoded video file", + errContains: "error opening video file", }, { name: "directory instead of file returns error", @@ -40,7 +40,7 @@ func TestSaveTranscodedVideoChunkFileErrors(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - url, err := SaveTranscodedVideoChunk(srv.URL, tc.filePath, "job-123") + url, err := UploadVideoChunk(srv.URL, tc.filePath) require.Error(t, err) assert.Empty(t, url) @@ -49,7 +49,7 @@ func TestSaveTranscodedVideoChunkFileErrors(t *testing.T) { } } -func TestSaveTranscodedVideoChunkHTTPErrors(t *testing.T) { +func TestUploadVideoChunkHTTPErrors(t *testing.T) { validFile := filepath.Join(t.TempDir(), "chunk.mp4") require.NoError(t, os.WriteFile(validFile, []byte("fake video"), 0644)) @@ -90,7 +90,7 @@ func TestSaveTranscodedVideoChunkHTTPErrors(t *testing.T) { })) t.Cleanup(srv.Close) - url, err := SaveTranscodedVideoChunk(srv.URL, validFile, "job-123") + url, err := UploadVideoChunk(srv.URL, validFile) if tc.wantErr { require.Error(t, err) @@ -104,7 +104,7 @@ func TestSaveTranscodedVideoChunkHTTPErrors(t *testing.T) { } } -func TestGetUnprocessedVideoChunkHTTPErrors(t *testing.T) { +func TestGetVideoChunkHTTPErrors(t *testing.T) { tests := []struct { name string status int @@ -135,18 +135,18 @@ func TestGetUnprocessedVideoChunkHTTPErrors(t *testing.T) { t.Cleanup(srv.Close) jobID := "job-123" - filePath, err := GetUnprocessedVideoChunk(srv.URL+"/"+jobID+"/chunk.mp4", jobID) + filePath, err := GetVideoChunk(srv.URL+"/"+jobID+"/processed/chunk.mp4", jobID) require.Error(t, err) assert.Empty(t, filePath) assert.Contains(t, err.Error(), tc.errContains) - t.Cleanup(func() { os.RemoveAll("/tmp/temp-unprocessed-" + jobID) }) + t.Cleanup(func() { os.RemoveAll("/tmp/" + jobID) }) }) } } -func TestGetUnprocessedVideoChunkWritesFile(t *testing.T) { +func TestGetVideoChunkWritesFile(t *testing.T) { videoContent := []byte("fake video content") jobID := "job-write" filename := "chunk_001.mp4" @@ -156,15 +156,15 @@ func TestGetUnprocessedVideoChunkWritesFile(t *testing.T) { w.Write(videoContent) })) t.Cleanup(srv.Close) - t.Cleanup(func() { os.RemoveAll("/tmp/temp-unprocessed-" + jobID) }) + t.Cleanup(func() { os.RemoveAll("/tmp/" + jobID) }) - storageURL := srv.URL + "/" + jobID + "/" + filename + storageURL := srv.URL + "/" + jobID + "/processed/" + filename - filePath, err := GetUnprocessedVideoChunk(storageURL, jobID) + filePath, err := GetVideoChunk(storageURL, jobID) require.NoError(t, err) assert.True(t, strings.HasSuffix(filePath, filename), "filePath %q should end with %q", filePath, filename) - assert.DirExists(t, "/tmp/temp-unprocessed-"+jobID) + assert.DirExists(t, "/tmp/"+jobID) assert.FileExists(t, filePath) got, err := os.ReadFile(filePath) @@ -172,10 +172,10 @@ func TestGetUnprocessedVideoChunkWritesFile(t *testing.T) { assert.Equal(t, videoContent, got) } -func TestGetUnprocessedVideoChunkIoCopyError(t *testing.T) { +func TestGetVideoChunkIoCopyError(t *testing.T) { t.Run("io.Copy failure cleans up job dir and returns error", func(t *testing.T) { jobID := "job-copy-err" - t.Cleanup(func() { os.RemoveAll("/tmp/temp-unprocessed-" + jobID) }) + t.Cleanup(func() { os.RemoveAll("/tmp/" + jobID) }) // Hijack the connection and close it mid-response to force io.Copy to fail srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -188,18 +188,18 @@ func TestGetUnprocessedVideoChunkIoCopyError(t *testing.T) { })) t.Cleanup(srv.Close) - _, err := GetUnprocessedVideoChunk(srv.URL+"/"+jobID+"/chunk.mp4", jobID) + _, err := GetVideoChunk(srv.URL+"/"+jobID+"/processed/chunk.mp4", jobID) require.Error(t, err) assert.Contains(t, err.Error(), "error writing video to file") - assert.NoDirExists(t, "/tmp/temp-unprocessed-"+jobID) + assert.NoDirExists(t, "/tmp/"+jobID) }) t.Run("io.Copy failure with removeAll error returns removeAll error", func(t *testing.T) { jobID := "job-copy-removall-err" t.Cleanup(func() { removeAll = os.RemoveAll - os.RemoveAll("/tmp/temp-unprocessed-" + jobID) + os.RemoveAll("/tmp/" + jobID) }) removeAll = func(_ string) error { return errors.New("remove failed") } @@ -214,7 +214,7 @@ func TestGetUnprocessedVideoChunkIoCopyError(t *testing.T) { })) t.Cleanup(srv.Close) - _, err := GetUnprocessedVideoChunk(srv.URL+"/"+jobID+"/chunk.mp4", jobID) + _, err := GetVideoChunk(srv.URL+"/"+jobID+"/processed/chunk.mp4", jobID) require.Error(t, err) assert.Contains(t, err.Error(), "error removing all files") diff --git a/backend/shared/test/jetstream_mocks.go b/backend/shared/test/jetstream_mocks.go new file mode 100644 index 0000000..a26de84 --- /dev/null +++ b/backend/shared/test/jetstream_mocks.go @@ -0,0 +1,66 @@ +package test + +import ( + "context" + + "github.com/nats-io/nats.go/jetstream" +) + +// MockKV stubs jetstream.KeyValue for unit tests. +type MockKV struct { + jetstream.KeyValue + GetErr error + GetFound bool // if true, Get returns a non-nil entry; if false, returns ErrKeyNotFound + PutErr error + PutKey string +} + +func (m *MockKV) Get(_ context.Context, key string) (jetstream.KeyValueEntry, error) { + if m.GetErr != nil { + return nil, m.GetErr + } + if !m.GetFound { + return nil, jetstream.ErrKeyNotFound + } + return &mockKVEntry{key: key}, nil +} + +func (m *MockKV) Put(_ context.Context, key string, _ []byte) (uint64, error) { + m.PutKey = key + return 0, m.PutErr +} + +type mockKVEntry struct { + jetstream.KeyValueEntry + key string +} + +func (e *mockKVEntry) Key() string { return e.key } + +// MockJS stubs jetstream.JetStream. Set StreamNameErr to simulate a lookup failure. +type MockJS struct { + jetstream.JetStream + JStreamNameErr error + JStreamErr error + JStream jetstream.Stream +} + +func (m *MockJS) StreamNameBySubject(_ context.Context, _ string) (string, error) { + return "jobs", m.JStreamNameErr +} + +func (m *MockJS) Stream(_ context.Context, _ string) (jetstream.Stream, error) { + return m.JStream, m.JStreamErr +} + +// MockStream stubs jetstream.Stream. +// The consumer field is named Cons to avoid conflicting with the Consumer() method promoted by the embedded interface. +type MockStream struct { + jetstream.Stream + ConsumerErr error + Cons jetstream.Consumer +} + +func (m *MockStream) CreateOrUpdateConsumer(_ context.Context, _ jetstream.ConsumerConfig) (jetstream.Consumer, error) { + return m.Cons, m.ConsumerErr +} diff --git a/backend/shared/test/middleware_helpers.go b/backend/shared/test/middleware_helpers.go new file mode 100644 index 0000000..465f56d --- /dev/null +++ b/backend/shared/test/middleware_helpers.go @@ -0,0 +1,25 @@ +//go:build unit || integration + +package test + +import ( + "io" + "log/slog" + "net/http" + "net/http/httptest" + "shared/middleware" + "testing" +) + +func NewCorsServer(t *testing.T) *httptest.Server { + t.Helper() + mux := http.NewServeMux() + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }) + return httptest.NewServer(middleware.Cors(mux)) +} + +func SilentLogger() *slog.Logger { + return slog.New(slog.NewTextHandler(io.Discard, nil)) +} diff --git a/backend/shared/test/nats_helpers.go b/backend/shared/test/nats_helpers.go new file mode 100644 index 0000000..64ac97a --- /dev/null +++ b/backend/shared/test/nats_helpers.go @@ -0,0 +1,46 @@ +//go:build integration + +package test + +import ( + "context" + "testing" + "time" + + "github.com/nats-io/nats.go" + "github.com/nats-io/nats.go/jetstream" + "github.com/stretchr/testify/require" + natstc "github.com/testcontainers/testcontainers-go/modules/nats" +) + +// fixture for setting up nats container for testing +func SetupNats(t *testing.T) (jetstream.JetStream, *nats.Conn) { + t.Helper() + ctx := context.Background() + + container, err := natstc.Run(ctx, "nats:2.10-alpine") + require.NoError(t, err) + t.Cleanup(func() { _ = container.Terminate(ctx) }) + + url, err := container.ConnectionString(ctx) + require.NoError(t, err) + + nc, err := nats.Connect(url, + nats.RetryOnFailedConnect(true), + nats.MaxReconnects(10), + nats.ReconnectWait(200*time.Millisecond), + ) + require.NoError(t, err) + t.Cleanup(nc.Close) + + js, err := jetstream.New(nc) + require.NoError(t, err) + + _, err = js.CreateStream(ctx, jetstream.StreamConfig{ + Name: "jobs", + Subjects: []string{"jobs.>"}, + }) + require.NoError(t, err) + + return js, nc +} diff --git a/backend/shared/test/storage_helpers.go b/backend/shared/test/storage_helpers.go new file mode 100644 index 0000000..3121372 --- /dev/null +++ b/backend/shared/test/storage_helpers.go @@ -0,0 +1,77 @@ +package test + +import ( + "bytes" + "context" + "fmt" + "net/http" + "os" + "testing" + + "github.com/stretchr/testify/require" + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/wait" +) + +// StartSeaweedFSFiler starts a SeaweedFS filer container and returns the filer +// URL and a cleanup function. Use this when t.Cleanup is not available (e.g. TestMain). +func StartSeaweedFSFiler() (string, func()) { + ctx := context.Background() + + req := testcontainers.ContainerRequest{ + Image: "chrislusf/seaweedfs", + Cmd: []string{"server", "-dir=/data", "-master.port=9333", "-volume.port=8080", "-filer"}, + ExposedPorts: []string{"9333/tcp", "8888/tcp"}, + WaitingFor: wait.ForAll( + wait.ForHTTP("/dir/status").WithPort("9333/tcp").WithStatusCodeMatcher(func(status int) bool { return status < 500 }), + wait.ForHTTP("/").WithPort("8888/tcp").WithStatusCodeMatcher(func(status int) bool { return status < 500 }), + ), + } + + container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ + ContainerRequest: req, + Started: true, + }) + if err != nil { + panic("failed to start SeaweedFS container: " + err.Error()) + } + + endpoint, err := container.PortEndpoint(ctx, "8888/tcp", "http") + if err != nil { + panic("failed to get SeaweedFS filer endpoint: " + err.Error()) + } + + return endpoint, func() { _ = container.Terminate(ctx) } +} + +const testVideoPath = "../test/testvideo.mp4" + +// helper to open the test video +func OpenTestVideo(t *testing.T) *os.File { + t.Helper() + f, err := os.Open(testVideoPath) + require.NoError(t, err) + t.Cleanup(func() { + err := f.Close() + require.NoError(t, err) + }) + return f +} + +func SeedProcessedVideo(t *testing.T, filerURL, jobID, fileName string, content []byte) { + t.Helper() + + url := fmt.Sprintf("%s/%s/processed/%s", filerURL, jobID, fileName) + + req, err := http.NewRequest(http.MethodPut, url, bytes.NewReader(content)) + require.NoError(t, err) + + req.Header.Set("Content-Type", "application/octet-stream") + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + + err = resp.Body.Close() + require.NoError(t, err) + + require.Less(t, resp.StatusCode, 400) +} diff --git a/backend/shared/test/testvideo.mp4 b/backend/shared/test/testvideo.mp4 new file mode 100644 index 0000000..6aca024 Binary files /dev/null and b/backend/shared/test/testvideo.mp4 differ diff --git a/backend/transcoder-worker/cmd/main.go b/backend/transcoder-worker/cmd/main.go index 73badb1..38d2c4a 100644 --- a/backend/transcoder-worker/cmd/main.go +++ b/backend/transcoder-worker/cmd/main.go @@ -6,11 +6,12 @@ import ( "log/slog" "os" "os/signal" + "shared/kv" + "shared/middleware" "syscall" + "shared/storage" "transcoder-worker/internal/handler" - "transcoder-worker/internal/observability" - "transcoder-worker/internal/storage" "github.com/joho/godotenv" "github.com/kelseyhightower/envconfig" @@ -34,7 +35,7 @@ func main() { log.Fatalf("failed to load config values: %v", err) } - logger := observability.StructuredLogger(cfg.ProdMode) + logger := middleware.StructuredLogger(cfg.ProdMode, "transcoder-worker") err = storage.CheckHealth(cfg.BaseStorageURL, logger) if err != nil { @@ -57,8 +58,8 @@ func main() { return } - processedKV := handler.CreateMsgProcessedKV(js, logger) - jobStatusKV := handler.ConnectJobStatusKV(js, logger) + processedKV := kv.CreateMsgProcessedKV("transcode-chunk-job-processed", js, logger) + jobStatusKV := kv.ConnectJobStatus(js, logger) quit := make(chan os.Signal, 1) signal.Notify(quit, os.Interrupt, syscall.SIGTERM) diff --git a/backend/transcoder-worker/cmd/main_integration_test.go b/backend/transcoder-worker/cmd/main_integration_test.go index a68b275..7ef2ad0 100644 --- a/backend/transcoder-worker/cmd/main_integration_test.go +++ b/backend/transcoder-worker/cmd/main_integration_test.go @@ -8,6 +8,7 @@ import ( "fmt" "os" "os/exec" + "shared/handler" "syscall" "testing" "time" @@ -105,7 +106,7 @@ func TestRunProcessingI(t *testing.T) { select { case data := <-received: - var msg service.ChunkCompleteMessage + var msg handler.ChunkCompleteMessage require.NoError(t, json.Unmarshal(data, &msg)) assert.Equal(t, jobID, msg.JobID) assert.Equal(t, 0, msg.ChunkIndex) diff --git a/backend/transcoder-worker/cmd/makefile b/backend/transcoder-worker/cmd/makefile index 03b3db0..2165c96 100644 --- a/backend/transcoder-worker/cmd/makefile +++ b/backend/transcoder-worker/cmd/makefile @@ -1,6 +1,6 @@ test_all: integration unit -PKGS := . ../internal/handler/... ../internal/service/... ../internal/storage +PKGS := . ../internal/handler/... ../internal/service/... format: go fmt ${PKGS} . diff --git a/backend/transcoder-worker/go.mod b/backend/transcoder-worker/go.mod index 70bc6e7..730d627 100644 --- a/backend/transcoder-worker/go.mod +++ b/backend/transcoder-worker/go.mod @@ -1,14 +1,14 @@ module transcoder-worker -go 1.26.1 +go 1.26.2 require ( github.com/joho/godotenv v1.5.1 github.com/kelseyhightower/envconfig v1.4.0 - github.com/nats-io/nats.go v1.50.0 + github.com/nats-io/nats.go v1.51.0 github.com/stretchr/testify v1.11.1 - github.com/testcontainers/testcontainers-go v0.41.0 - github.com/testcontainers/testcontainers-go/modules/nats v0.41.0 + github.com/testcontainers/testcontainers-go v0.42.0 + github.com/testcontainers/testcontainers-go/modules/nats v0.42.0 ) require ( @@ -24,47 +24,43 @@ require ( github.com/cpuguy83/dockercfg v0.3.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/distribution/reference v0.6.0 // indirect - github.com/docker/docker v28.5.2+incompatible // indirect - github.com/docker/go-connections v0.6.0 // indirect + github.com/docker/go-connections v0.7.0 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/ebitengine/purego v0.10.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-ole/go-ole v1.2.6 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect github.com/klauspost/compress v1.18.5 // indirect - github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect + github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e // indirect github.com/magiconair/properties v1.8.10 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/go-archive v0.2.0 // indirect - github.com/moby/patternmatcher v0.6.0 // indirect + github.com/moby/moby/api v1.54.1 // indirect + github.com/moby/moby/client v0.4.0 // indirect + github.com/moby/patternmatcher v0.6.1 // indirect github.com/moby/sys/sequential v0.6.0 // indirect github.com/moby/sys/user v0.4.0 // indirect github.com/moby/sys/userns v0.1.0 // indirect github.com/moby/term v0.5.2 // indirect - github.com/morikuni/aec v1.0.0 // indirect github.com/nats-io/nkeys v0.4.15 // indirect github.com/nats-io/nuid v1.0.1 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect - github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect - github.com/shirou/gopsutil/v4 v4.26.2 // indirect - github.com/sirupsen/logrus v1.9.3 // indirect + github.com/shirou/gopsutil/v4 v4.26.3 // indirect + github.com/sirupsen/logrus v1.9.4 // indirect github.com/tklauser/go-sysconf v0.3.16 // indirect github.com/tklauser/numcpus v0.11.0 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect - go.opentelemetry.io/otel v1.41.0 // indirect - go.opentelemetry.io/otel/metric v1.41.0 // indirect - go.opentelemetry.io/otel/trace v1.41.0 // indirect - golang.org/x/crypto v0.49.0 // indirect - golang.org/x/sys v0.42.0 // indirect - google.golang.org/grpc v1.79.1 // indirect - google.golang.org/protobuf v1.36.11 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 // indirect + go.opentelemetry.io/otel v1.43.0 // indirect + go.opentelemetry.io/otel/metric v1.43.0 // indirect + go.opentelemetry.io/otel/trace v1.43.0 // indirect + golang.org/x/crypto v0.50.0 // indirect + golang.org/x/sys v0.43.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/backend/transcoder-worker/go.sum b/backend/transcoder-worker/go.sum index 3a3bcfa..ba1a9ea 100644 --- a/backend/transcoder-worker/go.sum +++ b/backend/transcoder-worker/go.sum @@ -8,8 +8,6 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= -github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= -github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= @@ -22,17 +20,14 @@ github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpS github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= -github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= -github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= +github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= -github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM= -github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= -github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= +github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf0mN39c= +github.com/docker/go-connections v0.7.0/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU= @@ -44,15 +39,13 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -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/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 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.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= @@ -63,18 +56,20 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= -github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= +github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e h1:Q6MvJtQK/iRcRtzAscm/zF23XxJlbECiGPyRicsX+Ak= +github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/go-archive v0.2.0 h1:zg5QDUM2mi0JIM9fdQZWC7U8+2ZfixfTYoHL7rWUcP8= github.com/moby/go-archive v0.2.0/go.mod h1:mNeivT14o8xU+5q1YnNrkQVpK+dnNe/K6fHqnTg4qPU= -github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= -github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= -github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= -github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs= +github.com/moby/moby/api v1.54.1 h1:TqVzuJkOLsgLDDwNLmYqACUuTehOHRGKiPhvH8V3Nn4= +github.com/moby/moby/api v1.54.1/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs= +github.com/moby/moby/client v0.4.0 h1:S+2XegzHQrrvTCvF6s5HFzcrywWQmuVnhOXe2kiWjIw= +github.com/moby/moby/client v0.4.0/go.mod h1:QWPbvWchQbxBNdaLSpoKpCdf5E+WxFAgNHogCWDoa7g= +github.com/moby/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x084U= +github.com/moby/patternmatcher v0.6.1/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= @@ -83,10 +78,8 @@ github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= -github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= -github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= -github.com/nats-io/nats.go v1.50.0 h1:5zAeQrTvyrKrWLJ0fu02W3br8ym57qf7csDzgLOpcds= -github.com/nats-io/nats.go v1.50.0/go.mod h1:26HypzazeOkyO3/mqd1zZd53STJN0EjCYF9Uy2ZOBno= +github.com/nats-io/nats.go v1.51.0 h1:ByW84XTz6W03GSSsygsZcA+xgKK8vPGaa/FCAAEHnAI= +github.com/nats-io/nats.go v1.51.0/go.mod h1:26HypzazeOkyO3/mqd1zZd53STJN0EjCYF9Uy2ZOBno= github.com/nats-io/nkeys v0.4.15 h1:JACV5jRVO9V856KOapQ7x+EY8Jo3qw1vJt/9Jpwzkk4= github.com/nats-io/nkeys v0.4.15/go.mod h1:CpMchTXC9fxA5zrMo4KpySxNjiDVvr8ANOSZdiNfUrs= github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= @@ -95,28 +88,24 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= -github.com/shirou/gopsutil/v4 v4.26.2 h1:X8i6sicvUFih4BmYIGT1m2wwgw2VG9YgrDTi7cIRGUI= -github.com/shirou/gopsutil/v4 v4.26.2/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ= -github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= -github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/shirou/gopsutil/v4 v4.26.3 h1:2ESdQt90yU3oXF/CdOlRCJxrP+Am1aBYubTMTfxJ1qc= +github.com/shirou/gopsutil/v4 v4.26.3/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ= +github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= +github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= +github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4= +github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/testcontainers/testcontainers-go v0.41.0 h1:mfpsD0D36YgkxGj2LrIyxuwQ9i2wCKAD+ESsYM1wais= -github.com/testcontainers/testcontainers-go v0.41.0/go.mod h1:pdFrEIfaPl24zmBjerWTTYaY0M6UHsqA1YSvsoU40MI= -github.com/testcontainers/testcontainers-go/modules/nats v0.41.0 h1:ONiEuMwUgOLL3DiIHDgS4NvjwsSND4zkAhFlxkXWdb0= -github.com/testcontainers/testcontainers-go/modules/nats v0.41.0/go.mod h1:XSx4gGxmUaj3EHx3+ySOjge3ZCfOeV6yq/SrNhoYmso= +github.com/testcontainers/testcontainers-go v0.42.0 h1:He3IhTzTZOygSXLJPMX7n44XtK+qhjat1nI9cneBbUY= +github.com/testcontainers/testcontainers-go v0.42.0/go.mod h1:vZjdY1YmUA1qEForxOIOazfsrdyORJAbhi0bp8plN30= +github.com/testcontainers/testcontainers-go/modules/nats v0.42.0 h1:WQR0+1r4GkM5QgOBoLxlP41empovt5PxtaiDpC0G7ow= +github.com/testcontainers/testcontainers-go/modules/nats v0.42.0/go.mod h1:ZAI9iisjDNJmcRcycQFKSLpiBN9u2g1v9AJRq1afriE= github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA= github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI= github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw= @@ -125,52 +114,34 @@ github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= -go.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c= -go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.41.0 h1:inYW9ZhgqiDqh6BioM7DVHHzEGVq76Db5897WLGZ5Go= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.41.0/go.mod h1:Izur+Wt8gClgMJqO/cZ8wdeeMryJ/xxiOVgFSSfpDTY= -go.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ= -go.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps= -go.opentelemetry.io/otel/sdk v1.41.0 h1:YPIEXKmiAwkGl3Gu1huk1aYWwtpRLeskpV+wPisxBp8= -go.opentelemetry.io/otel/sdk v1.41.0/go.mod h1:ahFdU0G5y8IxglBf0QBJXgSe7agzjE4GiTJ6HT9ud90= -go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0= -go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis= -go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= -go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= -golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= -golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= -golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= -golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 h1:CqXxU8VOmDefoh0+ztfGaymYbhdB/tT3zs79QaZTNGY= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0/go.mod h1:BuhAPThV8PBHBvg8ZzZ/Ok3idOdhWIodywz2xEcRbJo= +go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= +go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= +go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= +go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= +go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= +go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= +go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= +go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= +go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= +go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= +golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= +golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= -golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU= -golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= -golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= -golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= -golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= -golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 h1:JLQynH/LBHfCTSbDWl+py8C+Rg/k1OVH3xfcaiANuF0= -google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:kSJwQxqmFXeo79zOmbrALdflXQeAYcUbgS7PbpMknCY= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= -google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY= -google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= -google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= -google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= +golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY= +golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/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/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= +pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk= +pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= diff --git a/backend/transcoder-worker/internal/handler/http.go b/backend/transcoder-worker/internal/handler/http.go index 95ac727..aade3ff 100644 --- a/backend/transcoder-worker/internal/handler/http.go +++ b/backend/transcoder-worker/internal/handler/http.go @@ -6,9 +6,12 @@ import ( "fmt" "log/slog" "net/http" + "os" "time" ) +var osExit = os.Exit + // starts the http server with /health endpoint func StartHttpServer(logger *slog.Logger, httpPort string) *http.Server { router := http.NewServeMux() diff --git a/backend/transcoder-worker/internal/handler/job_status_kv.go b/backend/transcoder-worker/internal/handler/job_status_kv.go deleted file mode 100644 index 63f00a2..0000000 --- a/backend/transcoder-worker/internal/handler/job_status_kv.go +++ /dev/null @@ -1,47 +0,0 @@ -package handler - -import ( - "context" - "encoding/json" - "log/slog" - "time" - - "github.com/nats-io/nats.go/jetstream" -) - -// connect to existing job status kv to publishing the processing stage update msgs -func ConnectJobStatusKV(js jetstream.JetStream, logger *slog.Logger) jetstream.KeyValue { - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - - kv, err := js.KeyValue(ctx, "job-status") - if err != nil { - logger.Error("failed to create recombine-chunk-recieved kv bucket", "err", err) - osExit(1) - return nil - } - - return kv -} - -func UpdateJobStatusKV(jobStatusKV jetstream.KeyValue, JobID string, logger *slog.Logger) error { - status, err := json.Marshal(struct { - State string `json:"state"` - Stage string `json:"stage"` - }{State: "PROCESSING", Stage: "transcoder"}) - if err != nil { - logger.Error("error marshalling status text", "err", err) - return err - } - - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - - _, err = jobStatusKV.Put(ctx, JobID, status) - if err != nil { - logger.Error("failed to write job status to jobStatus kv", "job_id", JobID, "err", err) - return err - } - - return nil -} diff --git a/backend/transcoder-worker/internal/handler/job_status_kv_integration_test.go b/backend/transcoder-worker/internal/handler/job_status_kv_integration_test.go deleted file mode 100644 index eda7f9a..0000000 --- a/backend/transcoder-worker/internal/handler/job_status_kv_integration_test.go +++ /dev/null @@ -1,42 +0,0 @@ -//go:build integration - -package handler - -import ( - "context" - "os" - "testing" - "transcoder-worker/internal/test" - - "github.com/nats-io/nats.go/jetstream" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -// connects to existing job-status bucket -func TestConnectJobStatusKV(t *testing.T) { - t.Run("connects to existing job-status bucket", func(t *testing.T) { - js, _ := test.SetupNats(t) - - _, err := js.CreateOrUpdateKeyValue(context.Background(), jetstream.KeyValueConfig{ - Bucket: "job-status", - }) - require.NoError(t, err) - - kv := ConnectJobStatusKV(js, test.SilentLogger()) - - assert.NotNil(t, kv) - }) - - t.Run("exits when job-status bucket does not exist", func(t *testing.T) { - js, _ := test.SetupNats(t) - - code := -1 - osExit = func(c int) { code = c } - t.Cleanup(func() { osExit = os.Exit }) - - ConnectJobStatusKV(js, test.SilentLogger()) - - assert.Equal(t, 1, code) - }) -} diff --git a/backend/transcoder-worker/internal/handler/job_status_kv_unit_test.go b/backend/transcoder-worker/internal/handler/job_status_kv_unit_test.go deleted file mode 100644 index 3f57120..0000000 --- a/backend/transcoder-worker/internal/handler/job_status_kv_unit_test.go +++ /dev/null @@ -1,47 +0,0 @@ -//go:build unit - -package handler_test - -import ( - "errors" - "testing" - "transcoder-worker/internal/handler" - "transcoder-worker/internal/test" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestUpdateJobStatusKV(t *testing.T) { - tests := []struct { - name string - kv *test.MockKV - wantErr bool - wantKey string - }{ - { - name: "success returns nil and writes job_id as key", - kv: &test.MockKV{}, - wantErr: false, - wantKey: "job-1", - }, - { - name: "KV Put error returns error", - kv: &test.MockKV{PutErr: errors.New("kv unavailable")}, - wantErr: true, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - err := handler.UpdateJobStatusKV(tc.kv, "job-1", test.SilentLogger()) - - if tc.wantErr { - require.Error(t, err) - } else { - require.NoError(t, err) - assert.Equal(t, tc.wantKey, tc.kv.PutKey) - } - }) - } -} diff --git a/backend/transcoder-worker/internal/handler/publisher.go b/backend/transcoder-worker/internal/handler/publisher.go deleted file mode 100644 index b25889c..0000000 --- a/backend/transcoder-worker/internal/handler/publisher.go +++ /dev/null @@ -1,29 +0,0 @@ -package handler - -import ( - "context" - "encoding/json" - "fmt" - "transcoder-worker/internal/service" - - "github.com/nats-io/nats.go/jetstream" -) - -const pubSubject = "jobs.chunks.complete" - -// Returns a function that publishes a ChunkCompleteMessage to JetStream -// injected into TranscoderService.OnComplete so it triggers and pubs -func PublishChunkComplete(js jetstream.JetStream) func(service.ChunkCompleteMessage) error { - return func(msg service.ChunkCompleteMessage) error { - data, err := json.Marshal(msg) - if err != nil { - return fmt.Errorf("marshall chunk error: %w", err) - } - - _, err = js.Publish(context.Background(), pubSubject, data) - if err != nil { - return err - } - return nil - } -} diff --git a/backend/transcoder-worker/internal/handler/subscriber.go b/backend/transcoder-worker/internal/handler/subscriber.go index 06eefb4..1c2c2f2 100644 --- a/backend/transcoder-worker/internal/handler/subscriber.go +++ b/backend/transcoder-worker/internal/handler/subscriber.go @@ -1,14 +1,15 @@ package handler import ( - "context" "encoding/json" "fmt" "log/slog" "os" - "time" + "path/filepath" + "shared/handler" + "shared/kv" + "shared/storage" "transcoder-worker/internal/service" - "transcoder-worker/internal/storage" "github.com/nats-io/nats.go/jetstream" ) @@ -22,28 +23,7 @@ var removeAll = os.RemoveAll func ConsumeVideoChunk( baseStorageURL string, js jetstream.JetStream, processedKV, jobStatusKV jetstream.KeyValue, logger *slog.Logger, ) (jetstream.ConsumeContext, error) { - ctx := context.Background() - - streamName, err := js.StreamNameBySubject(ctx, subSubject) - if err != nil { - return nil, fmt.Errorf("no stream found for subject: %s: %w", subSubject, err) - } - - stream, err := js.Stream(ctx, streamName) - if err != nil { - return nil, err - } - - cons, err := stream.CreateOrUpdateConsumer(ctx, jetstream.ConsumerConfig{ - Name: "transcoder-worker", - Durable: "transcoder-worker", - Description: "takes in nats msgs with job metadata and transcodes the video chunk", - FilterSubject: subSubject, - AckPolicy: jetstream.AckExplicitPolicy, - MaxAckPending: 10, // worker wont recieve more than 10 inflight messages - MaxDeliver: 3, - AckWait: 30 * time.Second, - }) + cons, err := handler.CreateDurableConsumer(js, subSubject, "transcoder-worker") if err != nil { return nil, err } @@ -62,7 +42,7 @@ func ConsumeVideoChunk( return } - exists, err := CheckChunkProcessed(processedKV, payload.JobID, payload.ChunkIndex) + exists, err := kv.CheckChunkProcessed(processedKV, payload.JobID, payload.ChunkIndex) if err != nil { logger.Error("failed to check chunk processed", "err", err) return @@ -78,12 +58,14 @@ func ConsumeVideoChunk( return } - err = UpdateJobStatusKV(jobStatusKV, payload.JobID, logger) + err = kv.UpdateJobStatus(jobStatusKV, "transcoder", payload.JobID, logger) if err != nil { logger.Error("failed to update job_status stage", "job_id", payload.JobID, "err", err) } - filePath, err := storage.GetUnprocessedVideoChunk(payload.StorageURL, payload.JobID) + fileName := fmt.Sprintf("temp-unprocessed-%s", payload.JobID) + + filePath, err := storage.GetVideoChunk(payload.StorageURL, fileName) if err != nil { logger.Error("error fetching unprocessed video chunk", "job_id", payload.JobID, "err", err) err := msg.Nak() @@ -105,7 +87,10 @@ func ConsumeVideoChunk( return } - url, err := storage.SaveTranscodedVideoChunk(baseStorageURL, outputPath, payload.JobID) + fileName = filepath.Base(outputPath) + url := fmt.Sprintf("%s/%s/processed/%s", baseStorageURL, payload.JobID, fileName) + + storageUrl, err := storage.UploadVideoChunk(url, outputPath) if err != nil { logger.Error( "error saving transcoded video chunk to seaweedfs storage", @@ -121,12 +106,14 @@ func ConsumeVideoChunk( return } - err = PublishChunkComplete(js)(service.ChunkCompleteMessage{ + const pubSubject = "jobs.chunks.complete" + + err = handler.PublishJobComplete(js, handler.ChunkCompleteMessage{ JobID: payload.JobID, ChunkIndex: payload.ChunkIndex, TotalChunks: payload.TotalChunks, - StorageURL: url, - }) + StorageURL: storageUrl, + }, pubSubject) if err != nil { logger.Error("failed to pub chunk complete msg", "job_id", payload.JobID, "chunk_index", payload.ChunkIndex, "err", err) err := msg.Nak() @@ -143,7 +130,7 @@ func ConsumeVideoChunk( return } - err = AddChunkProcessed(processedKV, payload.JobID, payload.ChunkIndex) + err = kv.AddChunkProcessed(processedKV, payload.JobID, payload.ChunkIndex) if err != nil { logger.Error("failed to mark job chunk as processed", "err", err) return diff --git a/backend/transcoder-worker/internal/handler/subscriber_integration_test.go b/backend/transcoder-worker/internal/handler/subscriber_integration_test.go index a463461..5b7f1df 100644 --- a/backend/transcoder-worker/internal/handler/subscriber_integration_test.go +++ b/backend/transcoder-worker/internal/handler/subscriber_integration_test.go @@ -8,6 +8,7 @@ import ( "errors" "fmt" "os" + shandler "shared/handler" "testing" "time" "transcoder-worker/internal/service" @@ -33,40 +34,6 @@ func TestMain(m *testing.M) { } func TestConsumeVideoChunk(t *testing.T) { - t.Run("no stream for subject returns error", func(t *testing.T) { - ctx := context.Background() - container, err := natstc.Run(ctx, "nats:2.10-alpine") - require.NoError(t, err) - t.Cleanup(func() { _ = container.Terminate(ctx) }) - - url, err := container.ConnectionString(ctx) - require.NoError(t, err) - - nc, err := nats.Connect(url) - require.NoError(t, err) - t.Cleanup(nc.Close) - - js, err := jetstream.New(nc) - require.NoError(t, err) - kv := test.SetupKV(t, js) - jobStatusKV := test.SetupJobStatusKV(t, js) - - _, err = ConsumeVideoChunk(sharedFilerURL, js, kv, jobStatusKV, test.SilentLogger()) - - assert.Error(t, err) - }) - - t.Run("returns non-nil consume context", func(t *testing.T) { - js, _ := test.SetupNats(t) - kv := test.SetupKV(t, js) - jobStatusKV := test.SetupJobStatusKV(t, js) - - consCtx, err := ConsumeVideoChunk(sharedFilerURL, js, kv, jobStatusKV, test.SilentLogger()) - - require.NoError(t, err) - assert.NotNil(t, consCtx) - }) - t.Run("consumer is created with correct config", func(t *testing.T) { ctx := context.Background() js, _ := test.SetupNats(t) @@ -146,7 +113,7 @@ func TestConsumeVideoChunk(t *testing.T) { select { case data := <-received: - var msg service.ChunkCompleteMessage + var msg shandler.ChunkCompleteMessage require.NoError(t, json.Unmarshal(data, &msg)) assert.Equal(t, jobID, msg.JobID) assert.Equal(t, 0, msg.ChunkIndex) diff --git a/backend/transcoder-worker/internal/handler/subscriber_unit_test.go b/backend/transcoder-worker/internal/handler/subscriber_unit_test.go index 2cb3e83..6ab79b4 100644 --- a/backend/transcoder-worker/internal/handler/subscriber_unit_test.go +++ b/backend/transcoder-worker/internal/handler/subscriber_unit_test.go @@ -27,47 +27,15 @@ func validPayload(t *testing.T, jobID string) []byte { return data } -func TestReturnError(t *testing.T) { - streamNameErr := errors.New("no stream") - streamErr := errors.New("stream error") - consumerErr := errors.New("consumer error") +func TestConsumeFailReturnError(t *testing.T) { consumeErr := errors.New("consume error") - tests := []struct { - name string - js *test.MockJS - wantErr error - }{ - { - name: "stream name lookup failure returns error", - js: &test.MockJS{JStreamNameErr: streamNameErr}, - wantErr: streamNameErr, - }, - { - name: "stream failure returns error", - js: &test.MockJS{JStreamErr: streamErr}, - wantErr: streamErr, - }, - { - name: "create consumer failure returns error", - js: &test.MockJS{JStream: &test.MockStream{ConsumerErr: consumerErr}}, - wantErr: consumerErr, - }, - { - name: "consume failure returns error", - js: &test.MockJS{JStream: &test.MockStream{Cons: &test.MockConsumer{ConsumeErr: consumeErr}}}, - wantErr: consumeErr, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - _, err := handler.ConsumeVideoChunk("http://storage", tc.js, &test.MockKV{}, &test.MockKV{}, test.SilentLogger()) - - require.Error(t, err) - assert.ErrorIs(t, err, tc.wantErr) - }) - } + js := &test.MockJS{JStream: &test.MockStream{Cons: &test.MockConsumer{ConsumeErr: consumeErr}}} + + _, err := handler.ConsumeVideoChunk("http://storage", js, &test.MockKV{}, &test.MockKV{}, test.SilentLogger()) + + require.Error(t, err) + assert.ErrorIs(t, err, consumeErr) } func TestAckAndNacking(t *testing.T) { diff --git a/backend/transcoder-worker/internal/observability/logging.go b/backend/transcoder-worker/internal/observability/logging.go deleted file mode 100644 index ef5af93..0000000 --- a/backend/transcoder-worker/internal/observability/logging.go +++ /dev/null @@ -1,17 +0,0 @@ -package observability - -import ( - "log/slog" - "os" -) - -// General Structured logger for code -func StructuredLogger(prodMode bool) *slog.Logger { - level := slog.LevelDebug - if prodMode { - level = slog.LevelInfo - } - h := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: level}) - - return slog.New(h).With("service", "transcoder-worker") -} diff --git a/backend/transcoder-worker/internal/observability/logging_unit_test.go b/backend/transcoder-worker/internal/observability/logging_unit_test.go deleted file mode 100644 index 8987b52..0000000 --- a/backend/transcoder-worker/internal/observability/logging_unit_test.go +++ /dev/null @@ -1,28 +0,0 @@ -//go:build unit - -package observability_test - -import ( - "context" - "log/slog" - "testing" - "transcoder-worker/internal/observability" - - "github.com/stretchr/testify/assert" -) - -func TestStructuredLogger(t *testing.T) { - - t.Run("prod mode set to false should enable debug level", func(t *testing.T) { - logger := observability.StructuredLogger(false) - - assert.True(t, logger.Enabled(context.Background(), slog.LevelDebug)) - }) - - t.Run("prod mode set to true should disable debug level", func(t *testing.T) { - logger := observability.StructuredLogger(true) - - assert.False(t, logger.Enabled(context.Background(), slog.LevelDebug)) - assert.True(t, logger.Enabled(context.Background(), slog.LevelInfo)) - }) -} diff --git a/backend/transcoder-worker/internal/service/nat_messages.go b/backend/transcoder-worker/internal/service/nat_messages.go index 89376b8..4f68dc5 100644 --- a/backend/transcoder-worker/internal/service/nat_messages.go +++ b/backend/transcoder-worker/internal/service/nat_messages.go @@ -8,10 +8,3 @@ type VideoChunkMessage struct { StorageURL string `json:"storage_url"` TargetResolution string `json:"target_resolution"` } - -type ChunkCompleteMessage struct { - JobID string `json:"job_id"` - ChunkIndex int `json:"chunk_index"` - TotalChunks int `json:"total_chunks"` - StorageURL string `json:"storage_url"` -} diff --git a/backend/transcoder-worker/internal/storage/health_check_integration_test.go b/backend/transcoder-worker/internal/storage/health_check_integration_test.go deleted file mode 100644 index 0210443..0000000 --- a/backend/transcoder-worker/internal/storage/health_check_integration_test.go +++ /dev/null @@ -1,38 +0,0 @@ -//go:build integration - -package storage_test - -import ( - "os" - "testing" - "transcoder-worker/internal/storage" - "transcoder-worker/internal/test" - - "github.com/stretchr/testify/assert" -) - -var sharedFilerUrl string - -func TestMain(m *testing.M) { - filerURL, cleanup := test.StartSeaweedFSFiler() - sharedFilerUrl = filerURL - - code := m.Run() - - cleanup() - os.Exit(code) -} - -func TestCheckHealth(t *testing.T) { - t.Run("storage health check fails when seedweedfs is unreachable", func(t *testing.T) { - err := storage.CheckHealth("http://localhost:1", test.SilentLogger()) - - assert.Error(t, err) - }) - - t.Run("storage health check passes when seedweedfs is reachable", func(t *testing.T) { - err := storage.CheckHealth(sharedFilerUrl, test.SilentLogger()) - - assert.NoError(t, err) - }) -} diff --git a/backend/transcoder-worker/internal/storage/queries_integration_test.go b/backend/transcoder-worker/internal/storage/queries_integration_test.go deleted file mode 100644 index c72788c..0000000 --- a/backend/transcoder-worker/internal/storage/queries_integration_test.go +++ /dev/null @@ -1,78 +0,0 @@ -//go:build integration - -package storage_test - -import ( - "fmt" - "io" - "net/http" - "os" - "testing" - "transcoder-worker/internal/storage" - "transcoder-worker/internal/test" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestGetUnprocessedVideoChunkIntegration(t *testing.T) { - t.Run("fetches video and writes to correct local path with matching content", func(t *testing.T) { - jobID := "job-fetch" - filename := "test_video.mp4" - videoContent, err := os.ReadFile("../test/test_video.mp4") - require.NoError(t, err) - - storageURL := test.SeedUnprocessedVideo(t, sharedFilerUrl, jobID, filename, videoContent) - t.Cleanup(func() { os.RemoveAll("/tmp/temp-unprocessed-" + jobID) }) - - filePath, err := storage.GetUnprocessedVideoChunk(storageURL, jobID) - - require.NoError(t, err) - assert.Equal(t, "/tmp/temp-unprocessed-"+jobID+"/"+filename, filePath) - assert.FileExists(t, filePath) - - got, err := os.ReadFile(filePath) - require.NoError(t, err) - assert.Equal(t, videoContent, got) - }) - - t.Run("nonexistent file returns error", func(t *testing.T) { - jobID := "job-missing" - storageURL := sharedFilerUrl + "/" + jobID + "/nonexistent.mp4" - t.Cleanup(func() { os.RemoveAll("/tmp/temp-unprocessed-" + jobID) }) - - filePath, err := storage.GetUnprocessedVideoChunk(storageURL, jobID) - - require.Error(t, err) - assert.Empty(t, filePath) - }) -} - -func TestSaveTranscodedVideoChunk(t *testing.T) { - t.Run("uploads file properly", func(t *testing.T) { - videoFile := test.OpenTestVideo(t) - - url, err := storage.SaveTranscodedVideoChunk(sharedFilerUrl, videoFile.Name(), "job-upload") - - require.NoError(t, err) - - expectedURL := fmt.Sprintf("%s/job-upload/processed/test_video.mp4", sharedFilerUrl) - assert.Equal(t, expectedURL, url) - - resp, err := http.Get(url) - require.NoError(t, err) - defer func() { - err := resp.Body.Close() - - require.NoError(t, err) - }() - require.Equal(t, http.StatusOK, resp.StatusCode) - - body, err := io.ReadAll(resp.Body) - require.NoError(t, err) - - expected, err := io.ReadAll(test.OpenTestVideo(t)) - require.NoError(t, err) - assert.Equal(t, expected, body) - }) -} diff --git a/backend/video-recombiner/cmd/main.go b/backend/video-recombiner/cmd/main.go index f22102a..2c1fc44 100644 --- a/backend/video-recombiner/cmd/main.go +++ b/backend/video-recombiner/cmd/main.go @@ -6,10 +6,11 @@ import ( "log/slog" "os" "os/signal" + "shared/kv" + "shared/middleware" + "shared/storage" "syscall" "video-recombiner/internal/handler" - "video-recombiner/internal/observability" - "video-recombiner/internal/storage" "github.com/joho/godotenv" "github.com/kelseyhightower/envconfig" @@ -32,7 +33,7 @@ func main() { log.Fatalf("failed to load config values: %v", err) } - logger := observability.StructuredLogger(cfg.ProdMode) + logger := middleware.StructuredLogger(cfg.ProdMode, "video-recombiner") err = storage.CheckHealth(cfg.BaseStorageURL, logger) if err != nil { @@ -55,8 +56,8 @@ func main() { return } - msgRecievedKV := handler.CreateMsgRecievedKV(js, logger) - jobStatusKV := handler.ConnectJobStatusKV(js, logger) + msgRecievedKV := kv.CreateMsgProcessedKV("recombine-chunk-recieved", js, logger) + jobStatusKV := kv.ConnectJobStatus(js, logger) quit := make(chan os.Signal, 1) signal.Notify(quit, os.Interrupt, syscall.SIGTERM) diff --git a/backend/video-recombiner/cmd/main_integration_test.go b/backend/video-recombiner/cmd/main_integration_test.go index 13e60d3..7bb714b 100644 --- a/backend/video-recombiner/cmd/main_integration_test.go +++ b/backend/video-recombiner/cmd/main_integration_test.go @@ -8,10 +8,10 @@ import ( "fmt" "os" "os/exec" + "shared/handler" "syscall" "testing" "time" - "video-recombiner/internal/service" "video-recombiner/internal/test" "github.com/nats-io/nats.go" @@ -120,7 +120,7 @@ func TestRunCombinerI(t *testing.T) { ctx := context.Background() for i, fileName := range []string{"chunk-0.mp4", "chunk-1.mp4"} { storageURL := fmt.Sprintf("%s/%s/%s/processed", sharedFilerURL, jobID, fileName) - payload, err := json.Marshal(service.ChunkCompleteMessage{ + payload, err := json.Marshal(handler.ChunkCompleteMessage{ JobID: jobID, ChunkIndex: i, TotalChunks: 2, @@ -133,7 +133,7 @@ func TestRunCombinerI(t *testing.T) { select { case data := <-received: - var msg service.VideoProcessingCompleteMessage + var msg handler.JobCompleteMessage require.NoError(t, json.Unmarshal(data, &msg)) assert.Equal(t, jobID, msg.JobID) case <-time.After(30 * time.Second): diff --git a/backend/video-recombiner/cmd/makefile b/backend/video-recombiner/cmd/makefile index 46db019..77206e2 100644 --- a/backend/video-recombiner/cmd/makefile +++ b/backend/video-recombiner/cmd/makefile @@ -1,6 +1,6 @@ test_all: integration unit -PKGS := . ../internal/handler/... ../internal/observability/... ../internal/service/... ../internal/storage/... +PKGS := . ../internal/handler/... ../internal/service/... format: go fmt ${PKGS} . diff --git a/backend/video-recombiner/go.mod b/backend/video-recombiner/go.mod index 9a3f125..8e0cf8b 100644 --- a/backend/video-recombiner/go.mod +++ b/backend/video-recombiner/go.mod @@ -1,14 +1,14 @@ module video-recombiner -go 1.26.1 +go 1.26.2 require ( github.com/joho/godotenv v1.5.1 github.com/kelseyhightower/envconfig v1.4.0 - github.com/nats-io/nats.go v1.50.0 + github.com/nats-io/nats.go v1.51.0 github.com/stretchr/testify v1.11.1 - github.com/testcontainers/testcontainers-go v0.41.0 - github.com/testcontainers/testcontainers-go/modules/nats v0.41.0 + github.com/testcontainers/testcontainers-go v0.42.0 + github.com/testcontainers/testcontainers-go/modules/nats v0.42.0 ) require ( @@ -24,47 +24,43 @@ require ( github.com/cpuguy83/dockercfg v0.3.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/distribution/reference v0.6.0 // indirect - github.com/docker/docker v28.5.2+incompatible // indirect - github.com/docker/go-connections v0.6.0 // indirect + github.com/docker/go-connections v0.7.0 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/ebitengine/purego v0.10.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-ole/go-ole v1.2.6 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect github.com/klauspost/compress v1.18.5 // indirect - github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect + github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e // indirect github.com/magiconair/properties v1.8.10 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/go-archive v0.2.0 // indirect - github.com/moby/patternmatcher v0.6.0 // indirect + github.com/moby/moby/api v1.54.1 // indirect + github.com/moby/moby/client v0.4.0 // indirect + github.com/moby/patternmatcher v0.6.1 // indirect github.com/moby/sys/sequential v0.6.0 // indirect github.com/moby/sys/user v0.4.0 // indirect github.com/moby/sys/userns v0.1.0 // indirect github.com/moby/term v0.5.2 // indirect - github.com/morikuni/aec v1.0.0 // indirect github.com/nats-io/nkeys v0.4.15 // indirect github.com/nats-io/nuid v1.0.1 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect - github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect - github.com/shirou/gopsutil/v4 v4.26.2 // indirect - github.com/sirupsen/logrus v1.9.3 // indirect + github.com/shirou/gopsutil/v4 v4.26.3 // indirect + github.com/sirupsen/logrus v1.9.4 // indirect github.com/tklauser/go-sysconf v0.3.16 // indirect github.com/tklauser/numcpus v0.11.0 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect - go.opentelemetry.io/otel v1.41.0 // indirect - go.opentelemetry.io/otel/metric v1.41.0 // indirect - go.opentelemetry.io/otel/trace v1.41.0 // indirect - golang.org/x/crypto v0.49.0 // indirect - golang.org/x/sys v0.42.0 // indirect - google.golang.org/grpc v1.79.1 // indirect - google.golang.org/protobuf v1.36.11 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 // indirect + go.opentelemetry.io/otel v1.43.0 // indirect + go.opentelemetry.io/otel/metric v1.43.0 // indirect + go.opentelemetry.io/otel/trace v1.43.0 // indirect + golang.org/x/crypto v0.50.0 // indirect + golang.org/x/sys v0.43.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/backend/video-recombiner/go.sum b/backend/video-recombiner/go.sum index 3a3bcfa..ba1a9ea 100644 --- a/backend/video-recombiner/go.sum +++ b/backend/video-recombiner/go.sum @@ -8,8 +8,6 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= -github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= -github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= @@ -22,17 +20,14 @@ github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpS github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= -github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= -github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= +github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= -github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM= -github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= -github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= +github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf0mN39c= +github.com/docker/go-connections v0.7.0/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU= @@ -44,15 +39,13 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -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/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 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.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= @@ -63,18 +56,20 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= -github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= +github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e h1:Q6MvJtQK/iRcRtzAscm/zF23XxJlbECiGPyRicsX+Ak= +github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/go-archive v0.2.0 h1:zg5QDUM2mi0JIM9fdQZWC7U8+2ZfixfTYoHL7rWUcP8= github.com/moby/go-archive v0.2.0/go.mod h1:mNeivT14o8xU+5q1YnNrkQVpK+dnNe/K6fHqnTg4qPU= -github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= -github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= -github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= -github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs= +github.com/moby/moby/api v1.54.1 h1:TqVzuJkOLsgLDDwNLmYqACUuTehOHRGKiPhvH8V3Nn4= +github.com/moby/moby/api v1.54.1/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs= +github.com/moby/moby/client v0.4.0 h1:S+2XegzHQrrvTCvF6s5HFzcrywWQmuVnhOXe2kiWjIw= +github.com/moby/moby/client v0.4.0/go.mod h1:QWPbvWchQbxBNdaLSpoKpCdf5E+WxFAgNHogCWDoa7g= +github.com/moby/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x084U= +github.com/moby/patternmatcher v0.6.1/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= @@ -83,10 +78,8 @@ github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= -github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= -github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= -github.com/nats-io/nats.go v1.50.0 h1:5zAeQrTvyrKrWLJ0fu02W3br8ym57qf7csDzgLOpcds= -github.com/nats-io/nats.go v1.50.0/go.mod h1:26HypzazeOkyO3/mqd1zZd53STJN0EjCYF9Uy2ZOBno= +github.com/nats-io/nats.go v1.51.0 h1:ByW84XTz6W03GSSsygsZcA+xgKK8vPGaa/FCAAEHnAI= +github.com/nats-io/nats.go v1.51.0/go.mod h1:26HypzazeOkyO3/mqd1zZd53STJN0EjCYF9Uy2ZOBno= github.com/nats-io/nkeys v0.4.15 h1:JACV5jRVO9V856KOapQ7x+EY8Jo3qw1vJt/9Jpwzkk4= github.com/nats-io/nkeys v0.4.15/go.mod h1:CpMchTXC9fxA5zrMo4KpySxNjiDVvr8ANOSZdiNfUrs= github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= @@ -95,28 +88,24 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= -github.com/shirou/gopsutil/v4 v4.26.2 h1:X8i6sicvUFih4BmYIGT1m2wwgw2VG9YgrDTi7cIRGUI= -github.com/shirou/gopsutil/v4 v4.26.2/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ= -github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= -github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/shirou/gopsutil/v4 v4.26.3 h1:2ESdQt90yU3oXF/CdOlRCJxrP+Am1aBYubTMTfxJ1qc= +github.com/shirou/gopsutil/v4 v4.26.3/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ= +github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= +github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= +github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4= +github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/testcontainers/testcontainers-go v0.41.0 h1:mfpsD0D36YgkxGj2LrIyxuwQ9i2wCKAD+ESsYM1wais= -github.com/testcontainers/testcontainers-go v0.41.0/go.mod h1:pdFrEIfaPl24zmBjerWTTYaY0M6UHsqA1YSvsoU40MI= -github.com/testcontainers/testcontainers-go/modules/nats v0.41.0 h1:ONiEuMwUgOLL3DiIHDgS4NvjwsSND4zkAhFlxkXWdb0= -github.com/testcontainers/testcontainers-go/modules/nats v0.41.0/go.mod h1:XSx4gGxmUaj3EHx3+ySOjge3ZCfOeV6yq/SrNhoYmso= +github.com/testcontainers/testcontainers-go v0.42.0 h1:He3IhTzTZOygSXLJPMX7n44XtK+qhjat1nI9cneBbUY= +github.com/testcontainers/testcontainers-go v0.42.0/go.mod h1:vZjdY1YmUA1qEForxOIOazfsrdyORJAbhi0bp8plN30= +github.com/testcontainers/testcontainers-go/modules/nats v0.42.0 h1:WQR0+1r4GkM5QgOBoLxlP41empovt5PxtaiDpC0G7ow= +github.com/testcontainers/testcontainers-go/modules/nats v0.42.0/go.mod h1:ZAI9iisjDNJmcRcycQFKSLpiBN9u2g1v9AJRq1afriE= github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA= github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI= github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw= @@ -125,52 +114,34 @@ github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= -go.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c= -go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.41.0 h1:inYW9ZhgqiDqh6BioM7DVHHzEGVq76Db5897WLGZ5Go= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.41.0/go.mod h1:Izur+Wt8gClgMJqO/cZ8wdeeMryJ/xxiOVgFSSfpDTY= -go.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ= -go.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps= -go.opentelemetry.io/otel/sdk v1.41.0 h1:YPIEXKmiAwkGl3Gu1huk1aYWwtpRLeskpV+wPisxBp8= -go.opentelemetry.io/otel/sdk v1.41.0/go.mod h1:ahFdU0G5y8IxglBf0QBJXgSe7agzjE4GiTJ6HT9ud90= -go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0= -go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis= -go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= -go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= -golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= -golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= -golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= -golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 h1:CqXxU8VOmDefoh0+ztfGaymYbhdB/tT3zs79QaZTNGY= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0/go.mod h1:BuhAPThV8PBHBvg8ZzZ/Ok3idOdhWIodywz2xEcRbJo= +go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= +go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= +go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= +go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= +go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= +go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= +go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= +go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= +go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= +go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= +golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= +golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= -golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU= -golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= -golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= -golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= -golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= -golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 h1:JLQynH/LBHfCTSbDWl+py8C+Rg/k1OVH3xfcaiANuF0= -google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:kSJwQxqmFXeo79zOmbrALdflXQeAYcUbgS7PbpMknCY= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= -google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY= -google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= -google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= -google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= +golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY= +golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/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/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= +pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk= +pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= diff --git a/backend/video-recombiner/internal/handler/http.go b/backend/video-recombiner/internal/handler/http.go index 95ac727..aade3ff 100644 --- a/backend/video-recombiner/internal/handler/http.go +++ b/backend/video-recombiner/internal/handler/http.go @@ -6,9 +6,12 @@ import ( "fmt" "log/slog" "net/http" + "os" "time" ) +var osExit = os.Exit + // starts the http server with /health endpoint func StartHttpServer(logger *slog.Logger, httpPort string) *http.Server { router := http.NewServeMux() diff --git a/backend/video-recombiner/internal/handler/msg_processed_kv_integration_test.go b/backend/video-recombiner/internal/handler/msg_processed_kv_integration_test.go deleted file mode 100644 index 82d2fde..0000000 --- a/backend/video-recombiner/internal/handler/msg_processed_kv_integration_test.go +++ /dev/null @@ -1,21 +0,0 @@ -//go:build integration - -package handler - -import ( - "testing" - "video-recombiner/internal/test" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -// creates bucket with correct name and TTL -func TestCreateMsgRecievedKV(t *testing.T) { - js, _ := test.SetupNats(t) - - kv := CreateMsgRecievedKV(js, test.SilentLogger()) - - require.NotNil(t, kv) - assert.Equal(t, "recombine-chunk-recieved", kv.Bucket()) -} diff --git a/backend/video-recombiner/internal/handler/msg_recieved_kv.go b/backend/video-recombiner/internal/handler/msg_recieved_kv.go deleted file mode 100644 index d0d89ed..0000000 --- a/backend/video-recombiner/internal/handler/msg_recieved_kv.go +++ /dev/null @@ -1,58 +0,0 @@ -package handler - -import ( - "context" - "errors" - "fmt" - "log/slog" - "time" - - "github.com/nats-io/nats.go/jetstream" -) - -// Create the Msg Recieved KV store for idempotency -func CreateMsgRecievedKV(js jetstream.JetStream, logger *slog.Logger) jetstream.KeyValue { - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - - kv, err := js.CreateOrUpdateKeyValue(ctx, jetstream.KeyValueConfig{ - Bucket: "recombine-chunk-recieved", - Description: "tracks video chunk for the jobID is already recieved for idempotency", - TTL: 3 * time.Hour, - }) - if err != nil { - logger.Error("failed to create recombine-chunk-recieved kv bucket", "err", err) - osExit(1) - } - - return kv -} - -// check if a jobID chunk already is recieved, returns a bool based on if it exists in the KV -func CheckChunkRecieved(kv jetstream.KeyValue, jobID string, chunkIndex int) (bool, error) { - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() - - _, err := kv.Get(ctx, fmt.Sprintf("%s.%d", jobID, chunkIndex)) - if err != nil { - if errors.Is(err, jetstream.ErrKeyNotFound) { - return false, nil - } - return false, fmt.Errorf("failed: %w", err) - } - - return true, nil -} - -// add a recieved job chunk to the KV for idempotency -func AddChunkRecieved(kv jetstream.KeyValue, jobID string, chunkIndex int) error { - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() - - _, err := kv.Put(ctx, fmt.Sprintf("%s.%d", jobID, chunkIndex), []byte("processed")) - if err != nil { - return fmt.Errorf("failed: %w", err) - } - - return nil -} diff --git a/backend/video-recombiner/internal/handler/msg_recieved_kv_unit_test.go b/backend/video-recombiner/internal/handler/msg_recieved_kv_unit_test.go deleted file mode 100644 index f1bfba1..0000000 --- a/backend/video-recombiner/internal/handler/msg_recieved_kv_unit_test.go +++ /dev/null @@ -1,91 +0,0 @@ -//go:build unit - -package handler_test - -import ( - "errors" - "testing" - "video-recombiner/internal/handler" - "video-recombiner/internal/test" - - "github.com/nats-io/nats.go/jetstream" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestCheckChunkRecieved(t *testing.T) { - t.Run("returns false when key not found", func(t *testing.T) { - kv := &test.MockKV{GetFound: false} - - processed, err := handler.CheckChunkRecieved(kv, "job-1", 0) - - require.NoError(t, err) - assert.False(t, processed) - }) - - t.Run("returns true when key exists", func(t *testing.T) { - kv := &test.MockKV{GetFound: true} - - processed, err := handler.CheckChunkRecieved(kv, "job-1", 0) - - require.NoError(t, err) - assert.True(t, processed) - }) - - t.Run("returns error on unexpected kv failure", func(t *testing.T) { - kv := &test.MockKV{GetErr: errors.New("kv unavailable")} - - _, err := handler.CheckChunkRecieved(kv, "job-1", 0) - - require.Error(t, err) - assert.ErrorContains(t, err, "failed") - }) - - t.Run("does not return error for ErrKeyNotFound", func(t *testing.T) { - kv := &test.MockKV{GetErr: jetstream.ErrKeyNotFound} - - processed, err := handler.CheckChunkRecieved(kv, "job-1", 0) - - require.NoError(t, err) - assert.False(t, processed) - }) - - t.Run("uses correct key format job_id.chunk_index", func(t *testing.T) { - // Key lookup for job "abc" chunk 3 must use "abc.3". - // We verify by having GetFound=true and confirming no error path is hit. - kv := &test.MockKV{GetFound: true} - - processed, err := handler.CheckChunkRecieved(kv, "abc", 3) - - require.NoError(t, err) - assert.True(t, processed) - }) -} - -func TestAddChunkRecieved(t *testing.T) { - t.Run("returns nil on success", func(t *testing.T) { - kv := &test.MockKV{} - - err := handler.AddChunkRecieved(kv, "job-1", 0) - - require.NoError(t, err) - }) - - t.Run("writes correct key job_id.chunk_index", func(t *testing.T) { - kv := &test.MockKV{} - - err := handler.AddChunkRecieved(kv, "job-abc", 2) - - require.NoError(t, err) - assert.Equal(t, "job-abc.2", kv.PutKey) - }) - - t.Run("returns error on kv failure", func(t *testing.T) { - kv := &test.MockKV{PutErr: errors.New("put failed")} - - err := handler.AddChunkRecieved(kv, "job-1", 0) - - require.Error(t, err) - assert.ErrorContains(t, err, "failed") - }) -} diff --git a/backend/video-recombiner/internal/handler/publisher.go b/backend/video-recombiner/internal/handler/publisher.go deleted file mode 100644 index c981b7a..0000000 --- a/backend/video-recombiner/internal/handler/publisher.go +++ /dev/null @@ -1,30 +0,0 @@ -package handler - -import ( - "encoding/json" - "fmt" - "video-recombiner/internal/service" - - "github.com/nats-io/nats.go/jetstream" -) - -const pubSubject = "jobs.complete" - -// publish a message saying the video processing is -// finished so frontend can update -func PublishVideoProcessingComplete( - js jetstream.JetStream, - msg service.VideoProcessingCompleteMessage, -) error { - data, err := json.Marshal(msg) - if err != nil { - return fmt.Errorf("marshall chunk error: %w", err) - } - - _, err = js.PublishAsync(pubSubject, data) - if err != nil { - return err - } - - return nil -} diff --git a/backend/video-recombiner/internal/handler/publisher_integration_test.go b/backend/video-recombiner/internal/handler/publisher_integration_test.go deleted file mode 100644 index 53b06d4..0000000 --- a/backend/video-recombiner/internal/handler/publisher_integration_test.go +++ /dev/null @@ -1,45 +0,0 @@ -//go:build integration - -package handler_test - -import ( - "encoding/json" - "testing" - "time" - "video-recombiner/internal/handler" - "video-recombiner/internal/service" - "video-recombiner/internal/test" - - "github.com/nats-io/nats.go" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestPublishVideoProcessingCompleteI(t *testing.T) { - t.Run("publishes correct payload to jobs.complete", func(t *testing.T) { - js, nc := test.SetupNats(t) - - received := make(chan []byte, 1) - sub, err := nc.Subscribe("jobs.complete", func(msg *nats.Msg) { - received <- msg.Data - }) - require.NoError(t, err) - t.Cleanup(func() { _ = sub.Unsubscribe() }) - - msg := service.VideoProcessingCompleteMessage{ - JobID: "job-1", - } - - err = handler.PublishVideoProcessingComplete(js, msg) - require.NoError(t, err) - - select { - case data := <-received: - var got service.VideoProcessingCompleteMessage - require.NoError(t, json.Unmarshal(data, &got)) - assert.Equal(t, msg, got) - case <-time.After(3 * time.Second): - t.Fatal("timed out waiting for message") - } - }) -} diff --git a/backend/video-recombiner/internal/handler/publisher_unit_test.go b/backend/video-recombiner/internal/handler/publisher_unit_test.go deleted file mode 100644 index 1d2d36a..0000000 --- a/backend/video-recombiner/internal/handler/publisher_unit_test.go +++ /dev/null @@ -1,35 +0,0 @@ -//go:build unit - -package handler_test - -import ( - "errors" - "testing" - "video-recombiner/internal/handler" - "video-recombiner/internal/service" - - "github.com/nats-io/nats.go/jetstream" - "github.com/stretchr/testify/assert" -) - -type mockJetStream struct { - jetstream.JetStream - publishErr error -} - -func (m *mockJetStream) PublishAsync(_ string, _ []byte, _ ...jetstream.PublishOpt) (jetstream.PubAckFuture, error) { - return nil, m.publishErr -} - -func TestPublishVideoProcessingComplete(t *testing.T) { - t.Run("returns wrapped error when publish fails", func(t *testing.T) { - publishErr := errors.New("nats publish failed") - mock := &mockJetStream{publishErr: publishErr} - - err := handler.PublishVideoProcessingComplete(mock, service.VideoProcessingCompleteMessage{ - JobID: "job-1", - }) - - assert.ErrorIs(t, err, publishErr) - }) -} diff --git a/backend/video-recombiner/internal/handler/subscriber.go b/backend/video-recombiner/internal/handler/subscriber.go index a94c4b2..24c195c 100644 --- a/backend/video-recombiner/internal/handler/subscriber.go +++ b/backend/video-recombiner/internal/handler/subscriber.go @@ -1,13 +1,14 @@ package handler import ( - "context" "encoding/json" "fmt" "log/slog" - "time" + "path/filepath" + "shared/handler" + "shared/kv" + "shared/storage" "video-recombiner/internal/service" - "video-recombiner/internal/storage" "github.com/nats-io/nats.go/jetstream" ) @@ -18,28 +19,7 @@ const subSubject = "jobs.chunks.complete" func RecombineVideo( js jetstream.JetStream, msgRecievedKV, jobStatusKV jetstream.KeyValue, logger *slog.Logger, baseStorageURL string, ) (jetstream.ConsumeContext, error) { - ctx := context.Background() - - streamName, err := js.StreamNameBySubject(ctx, subSubject) - if err != nil { - return nil, fmt.Errorf("no stream found for subject: %s: %w", subSubject, err) - } - - stream, err := js.Stream(ctx, streamName) - if err != nil { - return nil, err - } - - cons, err := stream.CreateOrUpdateConsumer(ctx, jetstream.ConsumerConfig{ - Name: "video-recombiner", - Durable: "video-recombiner", - Description: "takes in nats msgs with video chunks and recombines it once it gathered all chunks", - FilterSubject: subSubject, - AckPolicy: jetstream.AckExplicitPolicy, - MaxAckPending: 10, // worker wont recieve more than 10 inflight messages - MaxDeliver: 3, - AckWait: 30 * time.Second, - }) + cons, err := handler.CreateDurableConsumer(js, subSubject, "video-recombiner") if err != nil { return nil, err } @@ -47,7 +27,7 @@ func RecombineVideo( tracker := service.NewJobTracker() consCtx, err := cons.Consume(func(msg jetstream.Msg) { - var payload service.ChunkCompleteMessage + var payload handler.ChunkCompleteMessage err := json.Unmarshal(msg.Data(), &payload) if err != nil { @@ -58,7 +38,7 @@ func RecombineVideo( return } - recieved, err := CheckChunkRecieved(msgRecievedKV, payload.JobID, payload.ChunkIndex) + recieved, err := kv.CheckChunkProcessed(msgRecievedKV, payload.JobID, payload.ChunkIndex) if err != nil { logger.Error("failed to check chunk recieved", "err", err) return @@ -74,7 +54,7 @@ func RecombineVideo( return } - err = UpdateJobStatusKV(jobStatusKV, payload.JobID, logger) + err = kv.UpdateJobStatus(jobStatusKV, "video-recombiner", payload.JobID, logger) if err != nil { logger.Error("failed to update job_status stage", "job_id", payload.JobID, "err", err) } @@ -87,7 +67,7 @@ func RecombineVideo( return } - err = AddChunkRecieved(msgRecievedKV, payload.JobID, payload.ChunkIndex) + err = kv.AddChunkProcessed(msgRecievedKV, payload.JobID, payload.ChunkIndex) if err != nil { logger.Error("failed to mark job chunk as recieved", "err", err) return @@ -98,7 +78,9 @@ func RecombineVideo( failed := false for idx, storageURL := range chunks { - localPath, err := storage.GetProcessedVideoChunk(storageURL, payload.JobID) + fileName := fmt.Sprintf("processed_chunk-%s", payload.JobID) + + localPath, err := storage.GetVideoChunk(storageURL, fileName) if err != nil { logger.Error("failed to download chunk", "job_id", payload.JobID, "chunk_index", idx, "err", err) failed = true @@ -116,7 +98,10 @@ func RecombineVideo( return } - _, err = storage.UploadRecombinedVideo(baseStorageURL, outputPath, payload.JobID) + fileName := filepath.Base(outputPath) + url := fmt.Sprintf("%s/%s/%s/processed", baseStorageURL, payload.JobID, fileName) + + _, err = storage.UploadVideoChunk(url, outputPath) if err != nil { logger.Error("failed to upload recombined video", "job_id", payload.JobID, "err", err) return @@ -125,7 +110,9 @@ func RecombineVideo( service.CleanUpTempFolders(payload.JobID, logger) logger.Debug("job complete", "job_id", payload.JobID, "output_path", outputPath) - err = PublishVideoProcessingComplete(js, service.VideoProcessingCompleteMessage{JobID: payload.JobID}) + + const pubSubject = "jobs.complete" + err = handler.PublishJobComplete(js, handler.JobCompleteMessage{JobID: payload.JobID}, pubSubject) if err != nil { logger.Error("failed to pub msg for video processing complete", "job_id", payload.JobID, "err", err) } diff --git a/backend/video-recombiner/internal/handler/subscriber_integration_test.go b/backend/video-recombiner/internal/handler/subscriber_integration_test.go index 4f50834..48916dc 100644 --- a/backend/video-recombiner/internal/handler/subscriber_integration_test.go +++ b/backend/video-recombiner/internal/handler/subscriber_integration_test.go @@ -7,17 +7,16 @@ import ( "encoding/json" "fmt" "os" + shandler "shared/handler" "testing" "time" "video-recombiner/internal/handler" - "video-recombiner/internal/service" "video-recombiner/internal/test" "github.com/nats-io/nats.go" "github.com/nats-io/nats.go/jetstream" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - natstc "github.com/testcontainers/testcontainers-go/modules/nats" ) var sharedFilerURL string @@ -32,66 +31,32 @@ func TestMain(m *testing.M) { os.Exit(code) } -func TestRecombineVideo(t *testing.T) { - t.Run("no stream returns error", func(t *testing.T) { - ctx := context.Background() - - container, err := natstc.Run(ctx, "nats:2.10-alpine") - require.NoError(t, err) - t.Cleanup(func() { _ = container.Terminate(ctx) }) - - url, err := container.ConnectionString(ctx) - require.NoError(t, err) - - nc, err := nats.Connect(url) - require.NoError(t, err) - t.Cleanup(nc.Close) - - js, err := jetstream.New(nc) - require.NoError(t, err) - - _, err = handler.RecombineVideo(js, nil, nil, test.SilentLogger(), t.TempDir()) +// it should create consumer with correct config +func TestReturnCorrectConfig(t *testing.T) { + ctx := context.Background() + js, _ := test.SetupNats(t) + kv := test.SetupKV(t, js) + jobStatusKV := test.SetupJobStatusKV(t, js) - assert.Error(t, err) - }) + _, err := handler.RecombineVideo(js, kv, jobStatusKV, test.SilentLogger(), t.TempDir()) + require.NoError(t, err) - t.Run("returns consume context", func(t *testing.T) { - js, _ := test.SetupNats(t) - kv := test.SetupKV(t, js) - jobStatusKV := test.SetupJobStatusKV(t, js) + stream, err := js.Stream(ctx, "jobs") + require.NoError(t, err) - consCtx, err := handler.RecombineVideo(js, kv, jobStatusKV, test.SilentLogger(), t.TempDir()) + cons, err := stream.Consumer(ctx, "video-recombiner") + require.NoError(t, err) - require.NoError(t, err) - assert.NotNil(t, consCtx) - }) + info, err := cons.Info(ctx) + require.NoError(t, err) - t.Run("creates consumer with correct config", func(t *testing.T) { - ctx := context.Background() - js, _ := test.SetupNats(t) - kv := test.SetupKV(t, js) - jobStatusKV := test.SetupJobStatusKV(t, js) - - _, err := handler.RecombineVideo(js, kv, jobStatusKV, test.SilentLogger(), t.TempDir()) - require.NoError(t, err) - - stream, err := js.Stream(ctx, "jobs") - require.NoError(t, err) - - cons, err := stream.Consumer(ctx, "video-recombiner") - require.NoError(t, err) - - info, err := cons.Info(ctx) - require.NoError(t, err) - - assert.Equal(t, "video-recombiner", info.Config.Name) - assert.Equal(t, "video-recombiner", info.Config.Durable) - assert.Equal(t, "jobs.chunks.complete", info.Config.FilterSubject) - assert.Equal(t, jetstream.AckExplicitPolicy, info.Config.AckPolicy) - assert.Equal(t, 10, info.Config.MaxAckPending) - assert.Equal(t, 3, info.Config.MaxDeliver) - assert.Equal(t, 30*time.Second, info.Config.AckWait) - }) + assert.Equal(t, "video-recombiner", info.Config.Name) + assert.Equal(t, "video-recombiner", info.Config.Durable) + assert.Equal(t, "jobs.chunks.complete", info.Config.FilterSubject) + assert.Equal(t, jetstream.AckExplicitPolicy, info.Config.AckPolicy) + assert.Equal(t, 10, info.Config.MaxAckPending) + assert.Equal(t, 3, info.Config.MaxDeliver) + assert.Equal(t, 30*time.Second, info.Config.AckWait) } func TestMessageHandlingI(t *testing.T) { @@ -135,7 +100,7 @@ func TestMessageHandlingI(t *testing.T) { require.NoError(t, err) t.Cleanup(func() { _ = sub.Unsubscribe() }) - payload, err := json.Marshal(service.ChunkCompleteMessage{ + payload, err := json.Marshal(shandler.ChunkCompleteMessage{ JobID: "job-partial", ChunkIndex: 0, TotalChunks: 2, @@ -179,7 +144,7 @@ func TestMessageHandlingI(t *testing.T) { ctx := context.Background() for i, fileName := range []string{"chunk-0.mp4", "chunk-1.mp4"} { storageURL := fmt.Sprintf("%s/job-combine/%s/processed", sharedFilerURL, fileName) - payload, err := json.Marshal(service.ChunkCompleteMessage{ + payload, err := json.Marshal(shandler.ChunkCompleteMessage{ JobID: "job-combine", ChunkIndex: i, TotalChunks: 2, @@ -221,7 +186,7 @@ func TestRecombineVideoIdempotency(t *testing.T) { require.NoError(t, err) t.Cleanup(func() { _ = sub.Unsubscribe() }) - payload, err := json.Marshal(service.ChunkCompleteMessage{ + payload, err := json.Marshal(shandler.ChunkCompleteMessage{ JobID: jobID, ChunkIndex: 0, TotalChunks: 1, @@ -249,7 +214,7 @@ func TestRecombineVideoIdempotency(t *testing.T) { require.NoError(t, err) // Partial chunk (TotalChunks:2) so combine never fires — KV write still happens after ack. - payload, err := json.Marshal(service.ChunkCompleteMessage{ + payload, err := json.Marshal(shandler.ChunkCompleteMessage{ JobID: jobID, ChunkIndex: 0, TotalChunks: 2, diff --git a/backend/video-recombiner/internal/handler/subscriber_unit_test.go b/backend/video-recombiner/internal/handler/subscriber_unit_test.go index a4bd66f..563b768 100644 --- a/backend/video-recombiner/internal/handler/subscriber_unit_test.go +++ b/backend/video-recombiner/internal/handler/subscriber_unit_test.go @@ -6,9 +6,9 @@ import ( "encoding/json" "errors" "fmt" + shandler "shared/handler" "testing" "video-recombiner/internal/handler" - "video-recombiner/internal/service" "video-recombiner/internal/test" "github.com/stretchr/testify/assert" @@ -17,7 +17,7 @@ import ( func validPayload(t *testing.T, jobID string) []byte { t.Helper() - data, err := json.Marshal(service.ChunkCompleteMessage{ + data, err := json.Marshal(shandler.ChunkCompleteMessage{ JobID: jobID, ChunkIndex: 0, TotalChunks: 2, // not ready — combine never runs @@ -86,7 +86,7 @@ func TestMessageHandling(t *testing.T) { t.Run("partial chunk acks without combining", func(t *testing.T) { // Only the first of two chunks arrives — tracker not yet ready. - payload, err := json.Marshal(service.ChunkCompleteMessage{ + payload, err := json.Marshal(shandler.ChunkCompleteMessage{ JobID: "job-1", ChunkIndex: 0, TotalChunks: 2, @@ -109,7 +109,7 @@ func TestMessageHandling(t *testing.T) { t.Run("all chunks ready acks and triggers combine even if download fails", func(t *testing.T) { // TotalChunks=1, ChunkIndex=0 — immediately ready. // HTTP download will fail on invalid URL, but msg must be acked before that. - payload, err := json.Marshal(service.ChunkCompleteMessage{ + payload, err := json.Marshal(shandler.ChunkCompleteMessage{ JobID: "job-1", ChunkIndex: 0, TotalChunks: 1, @@ -131,7 +131,7 @@ func TestMessageHandling(t *testing.T) { t.Run("ack failure does not trigger combine or write kv", func(t *testing.T) { // When Ack returns an error the handler returns early before downloading chunks. - payload, err := json.Marshal(service.ChunkCompleteMessage{ + payload, err := json.Marshal(shandler.ChunkCompleteMessage{ JobID: "job-1", ChunkIndex: 0, TotalChunks: 1, @@ -194,7 +194,7 @@ func TestIdempotency(t *testing.T) { }) t.Run("writes kv with correct key after ack", func(t *testing.T) { - payload, err := json.Marshal(service.ChunkCompleteMessage{ + payload, err := json.Marshal(shandler.ChunkCompleteMessage{ JobID: "job-abc", ChunkIndex: 2, TotalChunks: 3, // not ready — combine never runs, but KV write still happens diff --git a/backend/video-recombiner/internal/observability/logging.go b/backend/video-recombiner/internal/observability/logging.go deleted file mode 100644 index 9948aba..0000000 --- a/backend/video-recombiner/internal/observability/logging.go +++ /dev/null @@ -1,17 +0,0 @@ -package observability - -import ( - "log/slog" - "os" -) - -// General Structured logger for code -func StructuredLogger(prodMode bool) *slog.Logger { - level := slog.LevelDebug - if prodMode { - level = slog.LevelInfo - } - h := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: level}) - - return slog.New(h).With("service", "video-recombiner") -} diff --git a/backend/video-recombiner/internal/observability/logging_unit_test.go b/backend/video-recombiner/internal/observability/logging_unit_test.go deleted file mode 100644 index 938632b..0000000 --- a/backend/video-recombiner/internal/observability/logging_unit_test.go +++ /dev/null @@ -1,28 +0,0 @@ -//go:build unit - -package observability_test - -import ( - "context" - "log/slog" - "testing" - "video-recombiner/internal/observability" - - "github.com/stretchr/testify/assert" -) - -func TestStructuredLogger(t *testing.T) { - - t.Run("prod mode set to false should enable debug level", func(t *testing.T) { - logger := observability.StructuredLogger(false) - - assert.True(t, logger.Enabled(context.Background(), slog.LevelDebug)) - }) - - t.Run("prod mode set to true should disable debug level", func(t *testing.T) { - logger := observability.StructuredLogger(true) - - assert.False(t, logger.Enabled(context.Background(), slog.LevelDebug)) - assert.True(t, logger.Enabled(context.Background(), slog.LevelInfo)) - }) -} diff --git a/backend/video-recombiner/internal/storage/health_check.go b/backend/video-recombiner/internal/storage/health_check.go deleted file mode 100644 index f93d7e7..0000000 --- a/backend/video-recombiner/internal/storage/health_check.go +++ /dev/null @@ -1,29 +0,0 @@ -package storage - -import ( - "fmt" - "log/slog" - "net/http" -) - -// send an http request to shared to storage to see if its reachable -func CheckHealth(storageURL string, logger *slog.Logger) error { - resp, err := http.Get(storageURL + "/dir/status") - - if err != nil { - return fmt.Errorf("error connecting to seedweedfs: %w", err) - } - - defer func() { - err := resp.Body.Close() - if err != nil { - logger.Warn("failed to close resp body for check health", "err", err) - } - }() - - if resp.StatusCode >= 500 { - return fmt.Errorf("seedweedfs returned status %d", resp.StatusCode) - } - - return nil -} diff --git a/backend/video-recombiner/internal/storage/queries.go b/backend/video-recombiner/internal/storage/queries.go deleted file mode 100644 index 7532f2e..0000000 --- a/backend/video-recombiner/internal/storage/queries.go +++ /dev/null @@ -1,108 +0,0 @@ -package storage - -import ( - "fmt" - "io" - "log" - "net/http" - "os" - "path/filepath" - "strings" -) - -// upload the combined video from all its chunks back to seaweedfs storage -func UploadRecombinedVideo(baseStorageURL, filePath, jobID string) (string, error) { - fileName := filepath.Base(filePath) - url := fmt.Sprintf("%s/%s/%s/processed", baseStorageURL, jobID, fileName) - - file, err := os.Open(filePath) - if err != nil { - return "", fmt.Errorf("error opening transcoded video chunk file: %w", err) - } - defer func() { - err := file.Close() - if err != nil { - log.Printf("error closing the file: %v", err) - } - }() - - req, err := http.NewRequest(http.MethodPut, url, file) - if err != nil { - return "", fmt.Errorf("error creating upload request: %w", err) - } - req.Header.Set("Content-Type", "application/octet-stream") - - resp, err := http.DefaultClient.Do(req) - if err != nil { - return "", fmt.Errorf("error connecting to seaweedfs storage: %w", err) - } - defer func() { - err := resp.Body.Close() - if err != nil { - log.Printf("error closing the response body: %v", err) - } - }() - - if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusOK { - return "", fmt.Errorf("seaweedfs upload failed with status: %d", resp.StatusCode) - } - - return url, err -} - -var removeAll = os.RemoveAll - -// fetch the processed video chunk from seaweedfs storage -func GetProcessedVideoChunk(storageURL, jobID string) (string, error) { - resp, err := http.Get(storageURL) - if err != nil { - return "", fmt.Errorf("error connecting to seedweedfs, %w", err) - } - defer func() { - err := resp.Body.Close() - if err != nil { - log.Printf("error closing the response body, %v", err) - } - }() - - switch resp.StatusCode { - case http.StatusNotFound: - return "", fmt.Errorf("video not found") - case http.StatusForbidden: - return "", fmt.Errorf("access denied") - case http.StatusInternalServerError: - return "", fmt.Errorf("error accessing seedweedfs") - } - - trimmed := strings.TrimSuffix(storageURL, "/processed") - filename := trimmed[strings.LastIndex(trimmed, "/")+1:] - jobDir := filepath.Join("/tmp/processed_chunk-" + jobID) - - err = os.MkdirAll(jobDir, 0755) - if err != nil { - return "", fmt.Errorf("error created job temp dir: %w", err) - } - - filePath := filepath.Join(jobDir, filename) - outFile, err := os.Create(filePath) - if err != nil { - return "", fmt.Errorf("error creating video file: %w", err) - } - defer func() { - err := outFile.Close() - if err != nil { - log.Printf("error closing the file, %v", err) - } - }() - - _, err = io.Copy(outFile, resp.Body) - if err != nil { - err := removeAll(jobDir) - if err != nil { - return "", fmt.Errorf("error removing all files: %w", err) - } - return "", fmt.Errorf("error writing video to file: %w", err) - } - - return filePath, nil -} diff --git a/backend/video-recombiner/internal/storage/queries_unit_test.go b/backend/video-recombiner/internal/storage/queries_unit_test.go deleted file mode 100644 index 73cf4c7..0000000 --- a/backend/video-recombiner/internal/storage/queries_unit_test.go +++ /dev/null @@ -1,222 +0,0 @@ -//go:build unit - -package storage - -import ( - "errors" - "net/http" - "net/http/httptest" - "os" - "path/filepath" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestUploadRecombinedVideoChunkFileErrors(t *testing.T) { - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - })) - t.Cleanup(srv.Close) - - tests := []struct { - name string - filePath string - errContains string - }{ - { - name: "nonexistent file returns error", - filePath: "/nonexistent/path/chunk.mp4", - errContains: "error opening transcoded video chunk file", - }, - { - name: "directory instead of file returns error", - filePath: t.TempDir(), - errContains: "error connecting to seaweedfs", - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - url, err := UploadRecombinedVideo(srv.URL, tc.filePath, "job-123") - - require.Error(t, err) - assert.Empty(t, url) - assert.Contains(t, err.Error(), tc.errContains) - }) - } -} - -func TestUploadRecombinedVideoChunkHTTPErrors(t *testing.T) { - validFile := filepath.Join(t.TempDir(), "chunk.mp4") - require.NoError(t, os.WriteFile(validFile, []byte("fake video"), 0644)) - - tests := []struct { - name string - status int - wantErr bool - errContains string - }{ - { - name: "500 returns error", - status: http.StatusInternalServerError, - wantErr: true, - errContains: "seaweedfs upload failed", - }, - { - name: "403 returns error", - status: http.StatusForbidden, - wantErr: true, - errContains: "seaweedfs upload failed", - }, - { - name: "200 returns url and no error", - status: http.StatusOK, - wantErr: false, - }, - { - name: "201 returns url and no error", - status: http.StatusCreated, - wantErr: false, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(tc.status) - })) - t.Cleanup(srv.Close) - - url, err := UploadRecombinedVideo(srv.URL, validFile, "job-123") - - if tc.wantErr { - require.Error(t, err) - assert.Empty(t, url) - assert.Contains(t, err.Error(), tc.errContains) - } else { - require.NoError(t, err) - assert.NotEmpty(t, url) - } - }) - } -} - -func TestGetProcessedVideoChunkHTTPErrors(t *testing.T) { - tests := []struct { - name string - status int - errContains string - }{ - { - name: "404 returns video not found error", - status: http.StatusNotFound, - errContains: "video not found", - }, - { - name: "403 returns access denied error", - status: http.StatusForbidden, - errContains: "access denied", - }, - { - name: "500 returns error", - status: http.StatusInternalServerError, - errContains: "error accessing seedweedfs", - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(tc.status) - })) - t.Cleanup(srv.Close) - - jobID := "job-123" - filePath, err := GetProcessedVideoChunk(srv.URL+"/"+jobID+"/chunk.mp4/processed", jobID) - - require.Error(t, err) - assert.Empty(t, filePath) - assert.Contains(t, err.Error(), tc.errContains) - - t.Cleanup(func() { os.RemoveAll("/tmp/processed_chunk-" + jobID) }) - }) - } -} - -func TestGetProcessedVideoChunkWritesFile(t *testing.T) { - videoContent := []byte("fake video content") - jobID := "job-write" - filename := "chunk_001.mp4" - - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - w.Write(videoContent) - })) - t.Cleanup(srv.Close) - t.Cleanup(func() { os.RemoveAll("/tmp/processed_chunk-" + jobID) }) - - storageURL := srv.URL + "/" + jobID + "/" + filename + "/processed" - - filePath, err := GetProcessedVideoChunk(storageURL, jobID) - - require.NoError(t, err) - assert.True(t, strings.HasSuffix(filePath, filename), "filePath %q should end with %q", filePath, filename) - assert.DirExists(t, "/tmp/processed_chunk-"+jobID) - assert.FileExists(t, filePath) - - got, err := os.ReadFile(filePath) - require.NoError(t, err) - assert.Equal(t, videoContent, got) -} - -func TestGetProcessedVideoChunkIoCopyError(t *testing.T) { - t.Run("io.Copy failure cleans up job dir and returns error", func(t *testing.T) { - jobID := "job-copy-err" - t.Cleanup(func() { os.RemoveAll("/tmp/processed_chunk-" + jobID) }) - - // Hijack the connection and close it mid-response to force io.Copy to fail - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - hj, ok := w.(http.Hijacker) - require.True(t, ok) - conn, _, err := hj.Hijack() - require.NoError(t, err) - conn.Write([]byte("HTTP/1.1 200 OK\r\nContent-Length: 1000\r\n\r\npartial")) - conn.Close() - })) - t.Cleanup(srv.Close) - - _, err := GetProcessedVideoChunk(srv.URL+"/"+jobID+"/chunk.mp4/processed", jobID) - - require.Error(t, err) - assert.Contains(t, err.Error(), "error writing video to file") - assert.NoDirExists(t, "/tmp/processed_chunk-"+jobID) - }) - - t.Run("io.Copy failure with removeAll error returns removeAll error", func(t *testing.T) { - jobID := "job-copy-removall-err" - t.Cleanup(func() { - removeAll = os.RemoveAll - os.RemoveAll("/tmp/processed_chunk-" + jobID) - }) - - removeAll = func(_ string) error { return errors.New("remove failed") } - - srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - hj, ok := w.(http.Hijacker) - require.True(t, ok) - conn, _, err := hj.Hijack() - require.NoError(t, err) - conn.Write([]byte("HTTP/1.1 200 OK\r\nContent-Length: 1000\r\n\r\npartial")) - conn.Close() - })) - t.Cleanup(srv.Close) - - _, err := GetProcessedVideoChunk(srv.URL+"/"+jobID+"/chunk.mp4/processed", jobID) - - require.Error(t, err) - assert.Contains(t, err.Error(), "error removing all files") - }) -} diff --git a/backend/video-status/cmd/main.go b/backend/video-status/cmd/main.go index dd8a7e4..397e8cd 100644 --- a/backend/video-status/cmd/main.go +++ b/backend/video-status/cmd/main.go @@ -8,10 +8,10 @@ import ( "net/http" "os" "os/signal" + "shared/middleware" "syscall" "time" "video-status/internal/handler" - "video-status/internal/middleware" "github.com/kelseyhightower/envconfig" "github.com/nats-io/nats.go" @@ -35,7 +35,7 @@ func main() { log.Fatalf("failed to load config values: %v", err) } - logger := middleware.StructuredLogger(cfg.ProdMode) + logger := middleware.StructuredLogger(cfg.ProdMode, "video-status") nc, err := nats.Connect(cfg.NatsURL) if err != nil { diff --git a/backend/video-status/cmd/makefile b/backend/video-status/cmd/makefile index 59db9a8..ad7b013 100644 --- a/backend/video-status/cmd/makefile +++ b/backend/video-status/cmd/makefile @@ -1,6 +1,6 @@ test_all: integration unit -PKGS := . ../internal/handler/... ../internal/middleware/... +PKGS := . ../internal/handler/... format: go fmt ${PKGS} . diff --git a/backend/video-status/go.mod b/backend/video-status/go.mod index 23e9312..53d0bf6 100644 --- a/backend/video-status/go.mod +++ b/backend/video-status/go.mod @@ -1,10 +1,10 @@ module video-status -go 1.25.0 +go 1.26.2 require ( github.com/kelseyhightower/envconfig v1.4.0 - github.com/nats-io/nats.go v1.50.0 + github.com/nats-io/nats.go v1.51.0 github.com/stretchr/testify v1.11.1 github.com/testcontainers/testcontainers-go/modules/nats v0.42.0 ) @@ -22,16 +22,16 @@ require ( github.com/cpuguy83/dockercfg v0.3.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/distribution/reference v0.6.0 // indirect - github.com/docker/go-connections v0.6.0 // indirect + github.com/docker/go-connections v0.7.0 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/ebitengine/purego v0.10.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-ole/go-ole v1.2.6 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/klauspost/compress v1.18.5 // indirect - github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect + github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e // indirect github.com/magiconair/properties v1.8.10 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/go-archive v0.2.0 // indirect @@ -55,13 +55,13 @@ require ( github.com/tklauser/numcpus v0.11.0 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect - go.opentelemetry.io/otel v1.41.0 // indirect - go.opentelemetry.io/otel/metric v1.41.0 // indirect - go.opentelemetry.io/otel/sdk v1.39.0 // indirect - go.opentelemetry.io/otel/sdk/metric v1.39.0 // indirect - go.opentelemetry.io/otel/trace v1.41.0 // indirect - golang.org/x/crypto v0.49.0 // indirect - golang.org/x/sys v0.42.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 // indirect + go.opentelemetry.io/otel v1.43.0 // indirect + go.opentelemetry.io/otel/metric v1.43.0 // indirect + go.opentelemetry.io/otel/sdk v1.43.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect + go.opentelemetry.io/otel/trace v1.43.0 // indirect + golang.org/x/crypto v0.50.0 // indirect + golang.org/x/sys v0.43.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/backend/video-status/go.sum b/backend/video-status/go.sum index a1ffdb3..e631ed4 100644 --- a/backend/video-status/go.sum +++ b/backend/video-status/go.sum @@ -28,6 +28,8 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= +github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf0mN39c= +github.com/docker/go-connections v0.7.0/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU= @@ -41,6 +43,8 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 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-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= @@ -56,6 +60,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= +github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e h1:Q6MvJtQK/iRcRtzAscm/zF23XxJlbECiGPyRicsX+Ak= +github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= @@ -78,6 +84,8 @@ github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= github.com/nats-io/nats.go v1.50.0 h1:5zAeQrTvyrKrWLJ0fu02W3br8ym57qf7csDzgLOpcds= github.com/nats-io/nats.go v1.50.0/go.mod h1:26HypzazeOkyO3/mqd1zZd53STJN0EjCYF9Uy2ZOBno= +github.com/nats-io/nats.go v1.51.0 h1:ByW84XTz6W03GSSsygsZcA+xgKK8vPGaa/FCAAEHnAI= +github.com/nats-io/nats.go v1.51.0/go.mod h1:26HypzazeOkyO3/mqd1zZd53STJN0EjCYF9Uy2ZOBno= github.com/nats-io/nkeys v0.4.15 h1:JACV5jRVO9V856KOapQ7x+EY8Jo3qw1vJt/9Jpwzkk4= github.com/nats-io/nkeys v0.4.15/go.mod h1:CpMchTXC9fxA5zrMo4KpySxNjiDVvr8ANOSZdiNfUrs= github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= @@ -114,25 +122,43 @@ go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 h1:CqXxU8VOmDefoh0+ztfGaymYbhdB/tT3zs79QaZTNGY= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0/go.mod h1:BuhAPThV8PBHBvg8ZzZ/Ok3idOdhWIodywz2xEcRbJo= go.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c= go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE= +go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= +go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= go.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ= go.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps= +go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= +go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= +go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= +go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= +go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= +go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0= go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis= +go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= +go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= +golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= +golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= +golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU= golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= +golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= diff --git a/backend/video-status/internal/middleware/cors.go b/backend/video-status/internal/middleware/cors.go deleted file mode 100644 index d054c91..0000000 --- a/backend/video-status/internal/middleware/cors.go +++ /dev/null @@ -1,34 +0,0 @@ -package middleware - -import ( - "net/http" - "slices" -) - -var allowedOrigins = []string{ - "http://localhost:5173", -} - -func Cors(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - origin := r.Header.Get("Origin") - allowed := slices.Contains(allowedOrigins, origin) - - if allowed { - w.Header().Set("Access-Control-Allow-Origin", origin) - w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS") - w.Header().Set("Access-Control-Allow-Headers", "Content-Type") - } - - if r.Method == http.MethodOptions { - if allowed { - w.WriteHeader(http.StatusNoContent) - } else { - w.WriteHeader(http.StatusForbidden) - } - return - } - - next.ServeHTTP(w, r) - }) -} diff --git a/backend/video-status/internal/middleware/cors_integration_test.go b/backend/video-status/internal/middleware/cors_integration_test.go deleted file mode 100644 index 4c69269..0000000 --- a/backend/video-status/internal/middleware/cors_integration_test.go +++ /dev/null @@ -1,77 +0,0 @@ -//go:build integration - -package middleware_test - -import ( - "net/http" - "net/http/httptest" - "testing" - "video-status/internal/middleware" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func newCorsServer(t *testing.T) *httptest.Server { - t.Helper() - mux := http.NewServeMux() - mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - }) - return httptest.NewServer(middleware.Cors(mux)) -} - -func TestCorsIntegration(t *testing.T) { - ts := newCorsServer(t) - defer ts.Close() - - tests := []struct { - name string - method string - origin string - expectStatus int - expectAllowOrigin string - }{ - { - name: "Allowed origin GET receives CORS headers", - method: http.MethodGet, - origin: "http://localhost:5173", - expectStatus: http.StatusOK, - expectAllowOrigin: "http://localhost:5173", - }, - { - name: "Disallowed origin GET receives no CORS headers", - method: http.MethodGet, - origin: "http://evil.com", - expectStatus: http.StatusOK, - }, - { - name: "Allowed origin OPTIONS preflight returns 204", - method: http.MethodOptions, - origin: "http://localhost:5173", - expectStatus: http.StatusNoContent, - expectAllowOrigin: "http://localhost:5173", - }, - { - name: "Disallowed origin OPTIONS preflight returns 403", - method: http.MethodOptions, - origin: "http://evil.com", - expectStatus: http.StatusForbidden, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - req, err := http.NewRequest(tt.method, ts.URL+"/", nil) - require.NoError(t, err) - req.Header.Set("Origin", tt.origin) - - resp, err := http.DefaultClient.Do(req) - require.NoError(t, err) - defer resp.Body.Close() - - assert.Equal(t, tt.expectStatus, resp.StatusCode) - assert.Equal(t, tt.expectAllowOrigin, resp.Header.Get("Access-Control-Allow-Origin")) - }) - } -} diff --git a/backend/video-status/internal/middleware/logging_unit_test.go b/backend/video-status/internal/middleware/logging_unit_test.go deleted file mode 100644 index 69a88ae..0000000 --- a/backend/video-status/internal/middleware/logging_unit_test.go +++ /dev/null @@ -1,133 +0,0 @@ -//go:build unit - -package middleware_test - -import ( - "bytes" - "context" - "log" - "log/slog" - "net/http" - "net/http/httptest" - "strings" - "testing" - "video-status/internal/middleware" - - "github.com/stretchr/testify/assert" -) - -func TestWriteHeader(t *testing.T) { - t.Run("Captures status code properly", func(t *testing.T) { - recorder := httptest.NewRecorder() - wrapped := &middleware.WrappedWriter{ - ResponseWriter: recorder, - StatusCode: http.StatusOK, - } - - wrapped.WriteHeader(http.StatusNotFound) - - assert.Equal(t, http.StatusNotFound, wrapped.StatusCode, "It should return status not found") - }) - - t.Run("Forwards to responsewriter", func(t *testing.T) { - recorder := httptest.NewRecorder() - wrapped := &middleware.WrappedWriter{ - ResponseWriter: recorder, - StatusCode: http.StatusOK, - } - - wrapped.WriteHeader(http.StatusInternalServerError) - - assert.Equal(t, http.StatusInternalServerError, recorder.Code, "It should update the recorder") - }) - - t.Run("Starts at 200 status code", func(t *testing.T) { - recorder := httptest.NewRecorder() - wrapped := &middleware.WrappedWriter{ - ResponseWriter: recorder, - StatusCode: http.StatusOK, - } - - assert.Equal(t, http.StatusOK, wrapped.StatusCode, "It start as status ok") - }) -} - -func TestApiRequestLogging(t *testing.T) { - t.Run("Handler is called", func(t *testing.T) { - handlerCalled := false - mockHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - handlerCalled = true - w.WriteHeader(http.StatusOK) - }) - - logging := middleware.ApiRequestLogging(mockHandler) - req := httptest.NewRequest(http.MethodGet, "/test", nil) - recorder := httptest.NewRecorder() - - logging.ServeHTTP(recorder, req) - - assert.True(t, handlerCalled, "handler should be called") - }) - - t.Run("Request is passed through", func(t *testing.T) { - var receivedMethod string - var receivedPath string - - mockHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - receivedMethod = r.Method - receivedPath = r.URL.Path - w.WriteHeader(http.StatusOK) - }) - - logging := middleware.ApiRequestLogging(mockHandler) - req := httptest.NewRequest(http.MethodPost, "/api/products", nil) - recorder := httptest.NewRecorder() - - logging.ServeHTTP(recorder, req) - - assert.Equal(t, http.MethodPost, receivedMethod, "method should be passed through") - assert.Equal(t, "/api/products", receivedPath, "path should be passed through") - }) - - t.Run("logs status code, method, endpoint, and timer", func(t *testing.T) { - var logBuffer bytes.Buffer - originalOutput := log.Writer() - log.SetOutput(&logBuffer) - defer log.SetOutput(originalOutput) - - mockHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusNotFound) - }) - - logging := middleware.ApiRequestLogging(mockHandler) - req := httptest.NewRequest(http.MethodGet, "/test-path", nil) - recorder := httptest.NewRecorder() - - logging.ServeHTTP(recorder, req) - - logOutput := logBuffer.String() - - // Verify log contains all expected fields: status code, method, path, timing - assert.Contains(t, logOutput, "404", "Log should contain status code") - assert.Contains(t, logOutput, "GET", "Log should contain HTTP method") - assert.Contains(t, logOutput, "/test-path", "Log should contain request path") - assert.True(t, strings.Contains(logOutput, "ns") || strings.Contains(logOutput, "µs") || strings.Contains(logOutput, "ms") || strings.Contains(logOutput, "s"), "Log should contain timing information") - }) - -} - -func TestStructuredLogger(t *testing.T) { - - t.Run("prod mode set to false should enable debug level", func(t *testing.T) { - logger := middleware.StructuredLogger(false) - - assert.True(t, logger.Enabled(context.Background(), slog.LevelDebug)) - }) - - t.Run("prod mode set to true should disable debug level", func(t *testing.T) { - logger := middleware.StructuredLogger(true) - - assert.False(t, logger.Enabled(context.Background(), slog.LevelDebug)) - assert.True(t, logger.Enabled(context.Background(), slog.LevelInfo)) - }) -} diff --git a/backend/video-upload/cmd/helpers_test.go b/backend/video-upload/cmd/helpers_test.go deleted file mode 100644 index 578cae9..0000000 --- a/backend/video-upload/cmd/helpers_test.go +++ /dev/null @@ -1,47 +0,0 @@ -//go:build unit - -package main - -import ( - "context" - "net" - "net/http" - "net/http/httptest" - "strconv" - "testing" - "video-upload/internal/test" - - "github.com/nats-io/nats.go/jetstream" - "github.com/stretchr/testify/require" -) - -// freePort returns a port number that is not currently in use. -func freePort(t *testing.T) string { - t.Helper() - - l, err := net.Listen("tcp", ":0") - require.NoError(t, err) - port := strconv.Itoa(l.Addr().(*net.TCPAddr).Port) - - err = l.Close() - require.NoError(t, err) - - return port -} - -// startTestServer calls startHttpApi with a free port and a temp output dir, -// registers a Cleanup to shut the server down, and returns the server and cfg. -func startTestServer(t *testing.T, kv jetstream.KeyValue) (*http.Server, *Config) { - t.Helper() - - fakeSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})) - - t.Cleanup(fakeSrv.Close) - - cfg := &Config{HTTPPort: freePort(t), StorageURL: fakeSrv.URL} - server := startHttpApi(test.SilentLogger(), &test.MockJS{}, kv, cfg) - - t.Cleanup(func() { server.Shutdown(context.Background()) }) //nolint:errcheck - - return server, cfg -} diff --git a/backend/video-upload/cmd/main.go b/backend/video-upload/cmd/main.go index a672bdd..5cdfea0 100644 --- a/backend/video-upload/cmd/main.go +++ b/backend/video-upload/cmd/main.go @@ -2,17 +2,14 @@ package main import ( "context" - "fmt" "log" - "log/slog" - "net/http" "os" "os/signal" + "shared/middleware" + "shared/storage" "syscall" "time" "video-upload/internal/handler" - "video-upload/internal/middleware" - "video-upload/internal/storage" "github.com/joho/godotenv" "github.com/kelseyhightower/envconfig" @@ -33,7 +30,7 @@ func main() { log.Fatalf("failed to load config values: %v", err) } - logger := middleware.StructuredLogger(cfg.ProdMode) + logger := middleware.StructuredLogger(cfg.ProdMode, "video-upload") err = storage.CheckHealth(cfg.StorageURL, logger) if err != nil { @@ -60,7 +57,7 @@ func main() { logger.Debug("starting service...") - server := startHttpApi(logger, js, kv, cfg) + server := handler.StartHttpApi(logger, js, kv, cfg.HTTPPort, cfg.StorageURL) <-quit @@ -75,30 +72,6 @@ func main() { } } -func startHttpApi(logger *slog.Logger, js jetstream.JetStream, kv jetstream.KeyValue, cfg *Config) *http.Server { - router := http.NewServeMux() - - vh := &handler.VideoHandler{Logger: logger, JS: js, KV: kv, StorageURL: cfg.StorageURL} - - router.HandleFunc("POST /jobs/upload", vh.UploadVideo) - router.HandleFunc("POST /jobs/download", vh.DownloadVideo) - - server := &http.Server{ - Addr: ":" + cfg.HTTPPort, - Handler: middleware.Cors(middleware.ApiRequestLogging(router)), - } - - go func() { - fmt.Printf("server running on http://localhost:%s\n", cfg.HTTPPort) - err := server.ListenAndServe() - if err != nil && err != http.ErrServerClosed { - log.Fatalf("http server error: %v", err) - } - }() - - return server -} - func loadConfig() (*Config, error) { err := godotenv.Load("../.env") if err != nil { diff --git a/backend/video-upload/cmd/main_integration_test.go b/backend/video-upload/cmd/main_integration_test.go index a835073..6a3a8c3 100644 --- a/backend/video-upload/cmd/main_integration_test.go +++ b/backend/video-upload/cmd/main_integration_test.go @@ -11,7 +11,7 @@ import ( "strings" "testing" "time" - "video-upload/internal/service" + "video-upload/internal/handler" "video-upload/internal/test" nats "github.com/nats-io/nats.go" @@ -45,7 +45,7 @@ func setupServer(t *testing.T) *serverEnv { cfg := &Config{HTTPPort: test.FreePort(t), StorageURL: sharedStorageURL} url := "http://localhost:" + cfg.HTTPPort - server := startHttpApi(test.SilentLogger(), js, kv, cfg) + server := handler.StartHttpApi(test.SilentLogger(), js, kv, cfg.HTTPPort, cfg.StorageURL) t.Cleanup(func() { server.Shutdown(context.Background()) //nolint:errcheck }) @@ -151,7 +151,7 @@ func TestUploadPipeline(t *testing.T) { // Verify NATS scene-split message was published select { case data := <-received: - var msg service.SceneSplitMessage + var msg handler.SceneSplitMessage require.NoError(t, json.Unmarshal(data, &msg)) assert.Equal(t, uploadResp.JobID, msg.JobID) assert.Equal(t, "720p", msg.TargetResolution) diff --git a/backend/video-upload/cmd/main_unit_test.go b/backend/video-upload/cmd/main_unit_test.go index 3f407ce..6ded5cf 100644 --- a/backend/video-upload/cmd/main_unit_test.go +++ b/backend/video-upload/cmd/main_unit_test.go @@ -3,9 +3,6 @@ package main import ( - "errors" - "net/http" - "net/http/httptest" "os" "os/exec" "path/filepath" @@ -53,31 +50,6 @@ func TestLoadConfig(t *testing.T) { }) } -func TestStartHttp(t *testing.T) { - t.Run("returns non-nil server with address derived from config", func(t *testing.T) { - server, cfg := startTestServer(t, &test.MockKV{}) - - require.NotNil(t, server) - assert.Equal(t, ":"+cfg.HTTPPort, server.Addr) - }) - - t.Run("server handler is non-nil", func(t *testing.T) { - server, _ := startTestServer(t, &test.MockKV{}) - - assert.NotNil(t, server.Handler) - }) - - t.Run("unregistered path returns 404", func(t *testing.T) { - server, _ := startTestServer(t, &test.MockKV{}) - - req := httptest.NewRequest(http.MethodGet, "/nonexistent", nil) - w := httptest.NewRecorder() - server.Handler.ServeHTTP(w, req) - - assert.Equal(t, http.StatusNotFound, w.Code) - }) -} - func TestMainFunc(t *testing.T) { t.Run("exits on NATS connect error", func(t *testing.T) { if os.Getenv("RUN_MAIN") == "nats_error" { @@ -104,16 +76,4 @@ func TestMainFunc(t *testing.T) { require.ErrorAs(t, err, &exitErr) assert.Equal(t, 1, exitErr.ExitCode()) }) - - t.Run("returns 500 when KV.Put fails during upload", func(t *testing.T) { - kv := &test.MockKV{PutErr: errors.New("kv unavailable")} - server, _ := startTestServer(t, kv) - - req := test.NewUploadRequest(t, "/jobs/upload", "video.mp4", []byte("data"), "1080p") - w := httptest.NewRecorder() - server.Handler.ServeHTTP(w, req) - - assert.Equal(t, http.StatusInternalServerError, w.Code) - assert.Contains(t, w.Body.String(), "failed to record job status") - }) } diff --git a/backend/video-upload/cmd/makefile b/backend/video-upload/cmd/makefile index 701a935..822bfc9 100644 --- a/backend/video-upload/cmd/makefile +++ b/backend/video-upload/cmd/makefile @@ -1,6 +1,6 @@ test_all: integration unit -PKGS := . ../internal/handler/... ../internal/service/... ../internal/middleware/... ../internal/storage/... +PKGS := . ../internal/handler/... ../internal/storage/... format: go fmt ${PKGS} . diff --git a/backend/video-upload/internal/handler/video.go b/backend/video-upload/internal/handler/http.go similarity index 53% rename from backend/video-upload/internal/handler/video.go rename to backend/video-upload/internal/handler/http.go index 3eb6e66..6a33e5f 100644 --- a/backend/video-upload/internal/handler/video.go +++ b/backend/video-upload/internal/handler/http.go @@ -2,33 +2,66 @@ package handler import ( "encoding/json" + "fmt" "io" + "log" "log/slog" "net/http" "path/filepath" - "video-upload/internal/service" + "shared/handler" + "shared/middleware" "video-upload/internal/storage" "github.com/go-playground/validator/v10" "github.com/nats-io/nats.go/jetstream" ) -type VideoHandler struct { - Logger *slog.Logger - JS jetstream.JetStream - KV jetstream.KeyValue - StorageURL string - MaxUploadBytes int64 +func StartHttpApi(logger *slog.Logger, js jetstream.JetStream, kv jetstream.KeyValue, httpPort, storageURL string) *http.Server { + router := http.NewServeMux() + + vh := &videoHandler{logger: logger, js: js, kv: kv, storageURL: storageURL} + + router.HandleFunc("POST /jobs/upload", vh.uploadVideoRoute) + router.HandleFunc("POST /jobs/download", vh.downloadVideoRoute) + + server := &http.Server{ + Addr: ":" + httpPort, + Handler: middleware.Cors(middleware.ApiRequestLogging(router)), + } + + go func() { + fmt.Printf("server running on http://localhost:%s\n", httpPort) + err := server.ListenAndServe() + if err != nil && err != http.ErrServerClosed { + log.Fatalf("http server error: %v", err) + } + }() + + return server +} + +type videoHandler struct { + logger *slog.Logger + js jetstream.JetStream + kv jetstream.KeyValue + storageURL string + maxUploadBytes int64 } type uploadResponse struct { JobID string `json:"job_id"` } +type SceneSplitMessage struct { + JobID string `json:"job_id"` + TargetResolution string `json:"target_resolution"` + StorageURL string `json:"storage_url"` +} + // handler for video upload POST requests, Accepts a multipart video upload, saves it to disk, // and publishes a scene split message to NATS for downstream processing -func (v *VideoHandler) UploadVideo(w http.ResponseWriter, r *http.Request) { - limit := v.MaxUploadBytes +func (v *videoHandler) uploadVideoRoute(w http.ResponseWriter, r *http.Request) { + limit := v.maxUploadBytes if limit == 0 { limit = 10 << 30 // 10 GB } @@ -37,21 +70,21 @@ func (v *VideoHandler) UploadVideo(w http.ResponseWriter, r *http.Request) { err := r.ParseMultipartForm(32 << 20) if err != nil { http.Error(w, "invalid multipart form: "+err.Error(), http.StatusBadRequest) - v.Logger.Error("invalid request multipart form", "err", err) + v.logger.Error("invalid request multipart form", "err", err) return } file, header, err := r.FormFile("video") if err != nil { http.Error(w, "missing video field", http.StatusBadRequest) - v.Logger.Error("missing video file in request") + v.logger.Error("missing video file in request") return } defer func() { err := file.Close() if err != nil { http.Error(w, "error closing open file", http.StatusBadRequest) - v.Logger.Error("error closing open file", "err", err) + v.logger.Error("error closing open file", "err", err) return } }() @@ -59,48 +92,50 @@ func (v *VideoHandler) UploadVideo(w http.ResponseWriter, r *http.Request) { targetRes := r.FormValue("target_resolution") if targetRes == "" { http.Error(w, "missing target_resolution field", http.StatusBadRequest) - v.Logger.Error("missing target_resolution field") + v.logger.Error("missing target_resolution field") return } - result, err := storage.SaveUploadedVideo(file, v.StorageURL, header.Filename) + result, err := storage.SaveUploadedVideo(file, v.storageURL, header.Filename) if err != nil { http.Error(w, "failed to save uploaded video", http.StatusInternalServerError) - v.Logger.Error("failed to save uploaded video", "err", err) + v.logger.Error("failed to save uploaded video", "err", err) return } - err = PublishVideoMetadata( - v.JS, service.SceneSplitMessage{ + const pubSubject = "jobs.video.scene-split" + + err = handler.PublishJobComplete( + v.js, SceneSplitMessage{ JobID: result.JobID, TargetResolution: targetRes, StorageURL: result.StorageURL, - }, + }, pubSubject, ) if err != nil { http.Error(w, "unable to send process request msg to system", http.StatusInternalServerError) - v.Logger.Error("error publishing split video request to nats", "err", err) + v.logger.Error("error publishing split video request to nats", "err", err) return } - err = updateJobStatusKV(r.Context(), result.JobID, v.KV, v.Logger) + err = updateJobStatusKV(r.Context(), result.JobID, v.kv, v.logger) if err != nil { http.Error(w, "failed to record job status", http.StatusInternalServerError) return } - v.Logger.Info("video upload job submitted", "job_id", result.JobID, "file", header.Filename) + v.logger.Info("video upload job submitted", "job_id", result.JobID, "file", header.Filename) w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) err = json.NewEncoder(w).Encode(uploadResponse{JobID: result.JobID}) if err != nil { http.Error(w, "error encoding http response", http.StatusInternalServerError) - v.Logger.Error("error encoding success http response", "err", err) + v.logger.Error("error encoding success http response", "err", err) return } } // handler for streaming the completed out video for a given job ID -func (v *VideoHandler) DownloadVideo(w http.ResponseWriter, r *http.Request) { +func (v *videoHandler) downloadVideoRoute(w http.ResponseWriter, r *http.Request) { var payload struct { JobID string `json:"job_id" validate:"required,min=2"` FileName string `json:"file_name" validate:"required,min=2"` @@ -108,39 +143,39 @@ func (v *VideoHandler) DownloadVideo(w http.ResponseWriter, r *http.Request) { err := json.NewDecoder(r.Body).Decode(&payload) if err != nil { - v.Logger.Error("error decoding the request body", "err", err) + v.logger.Error("error decoding the request body", "err", err) http.Error(w, "invalid json payload", http.StatusBadRequest) return } err = validator.New().Struct(payload) if err != nil { - v.Logger.Error("error validating request body", "err", err) + v.logger.Error("error validating request body", "err", err) http.Error(w, err.Error(), http.StatusBadRequest) return } - body, err := storage.GetProcessedVideo(v.StorageURL, payload.JobID, payload.FileName) + body, err := storage.GetProcessedVideo(v.storageURL, payload.JobID, payload.FileName) if err != nil { - v.Logger.Error("failed to fetch processed video", "err", err) + v.logger.Error("failed to fetch processed video", "err", err) http.Error(w, "failed to fetch video", http.StatusInternalServerError) return } defer func() { err := body.Close() if err != nil { - v.Logger.Warn("failed to close response body for Get Processed Video", "err", err) + v.logger.Warn("failed to close response body for Get Processed Video", "err", err) } }() - v.Logger.Debug("fetching output video", "job_id", payload.JobID, "fileName", payload.FileName) + v.logger.Debug("fetching output video", "job_id", payload.JobID, "fileName", payload.FileName) w.Header().Set("Content-Disposition", "attachment; filename="+filepath.Base(payload.FileName)) w.Header().Set("Content-Type", "application/octet-stream") _, err = io.Copy(w, body) if err != nil { - v.Logger.Error("error streaming video to response", "err", err) + v.logger.Error("error streaming video to response", "err", err) return } } diff --git a/backend/video-upload/internal/handler/video_integration_test.go b/backend/video-upload/internal/handler/http_integration_test.go similarity index 91% rename from backend/video-upload/internal/handler/video_integration_test.go rename to backend/video-upload/internal/handler/http_integration_test.go index ea42670..939eaf3 100644 --- a/backend/video-upload/internal/handler/video_integration_test.go +++ b/backend/video-upload/internal/handler/http_integration_test.go @@ -1,6 +1,6 @@ //go:build integration -package handler_test +package handler import ( "bytes" @@ -13,8 +13,6 @@ import ( "os" "testing" "time" - "video-upload/internal/handler" - "video-upload/internal/service" "video-upload/internal/test" nats "github.com/nats-io/nats.go" @@ -35,23 +33,23 @@ func TestMain(m *testing.M) { os.Exit(code) } -func newUploadHandler(js jetstream.JetStream, kv jetstream.KeyValue, filerURL string) *handler.VideoHandler { - return &handler.VideoHandler{ - Logger: test.SilentLogger(), - JS: js, - KV: kv, - StorageURL: filerURL, +func newUploadHandler(js jetstream.JetStream, kv jetstream.KeyValue, filerURL string) *videoHandler { + return &videoHandler{ + logger: test.SilentLogger(), + js: js, + kv: kv, + storageURL: filerURL, } } func newDownloadVideoServer(t *testing.T, storageURL string) *httptest.Server { t.Helper() - h := &handler.VideoHandler{ - Logger: test.SilentLogger(), - StorageURL: storageURL, + h := &videoHandler{ + logger: test.SilentLogger(), + storageURL: storageURL, } mux := http.NewServeMux() - mux.HandleFunc("GET /jobs", h.DownloadVideo) + mux.HandleFunc("GET /jobs", h.downloadVideoRoute) return httptest.NewServer(mux) } @@ -62,13 +60,13 @@ func TestUploadVideoFlow(t *testing.T) { h := newUploadHandler(js, kv, sharedFilerUrl) t.Run("Rejects uploads exceeding MaxUploadBytes", func(t *testing.T) { - h.MaxUploadBytes = 100 - defer func() { h.MaxUploadBytes = 0 }() + h.maxUploadBytes = 100 + defer func() { h.maxUploadBytes = 0 }() req := test.NewUploadRequest(t, "/jobs", "big.mp4", bytes.Repeat([]byte("x"), 200), "1080p") rec := httptest.NewRecorder() - h.UploadVideo(rec, req) + h.uploadVideoRoute(rec, req) assert.Equal(t, http.StatusBadRequest, rec.Code) assert.Contains(t, rec.Body.String(), "invalid multipart form") @@ -78,7 +76,7 @@ func TestUploadVideoFlow(t *testing.T) { req := test.NewUploadRequest(t, "/jobs", "clip.mp4", []byte("fake video bytes"), "1080p") rec := httptest.NewRecorder() - h.UploadVideo(rec, req) + h.uploadVideoRoute(rec, req) require.Equal(t, http.StatusCreated, rec.Code) var resp struct { @@ -93,7 +91,7 @@ func TestUploadVideoFlow(t *testing.T) { req := test.NewUploadRequest(t, "/jobs", "video.mp4", content, "720p") rec := httptest.NewRecorder() - h.UploadVideo(rec, req) + h.uploadVideoRoute(rec, req) require.Equal(t, http.StatusCreated, rec.Code) var resp struct { @@ -120,7 +118,7 @@ func TestUploadVideoFlow(t *testing.T) { req := test.NewUploadRequest(t, "/jobs", "video.mp4", []byte("data"), "720p") rec := httptest.NewRecorder() - h.UploadVideo(rec, req) + h.uploadVideoRoute(rec, req) require.Equal(t, http.StatusCreated, rec.Code) var uploadResp struct { @@ -130,7 +128,7 @@ func TestUploadVideoFlow(t *testing.T) { select { case data := <-received: - var msg service.SceneSplitMessage + var msg SceneSplitMessage require.NoError(t, json.Unmarshal(data, &msg)) assert.Equal(t, uploadResp.JobID, msg.JobID) assert.Equal(t, "720p", msg.TargetResolution) @@ -146,7 +144,7 @@ func TestUploadVideoFlow(t *testing.T) { for range 3 { req := test.NewUploadRequest(t, "/jobs", "video.mp4", []byte("data"), "1080p") rec := httptest.NewRecorder() - h.UploadVideo(rec, req) + h.uploadVideoRoute(rec, req) require.Equal(t, http.StatusCreated, rec.Code) var resp struct { @@ -163,7 +161,7 @@ func TestUploadVideoFlow(t *testing.T) { req := test.NewUploadRequest(t, "/jobs", "big.mp4", content, "4k") rec := httptest.NewRecorder() - h.UploadVideo(rec, req) + h.uploadVideoRoute(rec, req) require.Equal(t, http.StatusCreated, rec.Code) var resp struct { @@ -185,7 +183,7 @@ func TestUploadVideoFlow(t *testing.T) { req := test.NewUploadRequest(t, "/jobs", "video.mp4", []byte("data"), "1080p") rec := httptest.NewRecorder() - h.UploadVideo(rec, req) + h.uploadVideoRoute(rec, req) assert.Equal(t, http.StatusInternalServerError, rec.Code) assert.Contains(t, rec.Body.String(), "unable to send process request") diff --git a/backend/video-upload/internal/handler/video_unit_test.go b/backend/video-upload/internal/handler/http_unit_test.go similarity index 55% rename from backend/video-upload/internal/handler/video_unit_test.go rename to backend/video-upload/internal/handler/http_unit_test.go index 18c3a2e..c83b061 100644 --- a/backend/video-upload/internal/handler/video_unit_test.go +++ b/backend/video-upload/internal/handler/http_unit_test.go @@ -1,25 +1,86 @@ //go:build unit -package handler_test +package handler import ( + "context" + "errors" + "net" "net/http" "net/http/httptest" + "strconv" "strings" "testing" - "video-upload/internal/handler" "video-upload/internal/test" + "github.com/nats-io/nats.go/jetstream" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) -func newVideoHandler(StorageURL string, js *test.MockJS) *handler.VideoHandler { - return &handler.VideoHandler{ - Logger: test.SilentLogger(), - JS: js, - KV: &test.MockKV{}, - StorageURL: StorageURL, - MaxUploadBytes: 0, +// freePort returns a port number that is not currently in use. +func freePort(t *testing.T) string { + t.Helper() + + l, err := net.Listen("tcp", ":0") + require.NoError(t, err) + port := strconv.Itoa(l.Addr().(*net.TCPAddr).Port) + + err = l.Close() + require.NoError(t, err) + + return port +} + +// startTestServer calls startHttpApi with a free port and a temp output dir, +// registers a Cleanup to shut the server down, and returns the server and cfg. +func startTestServer(t *testing.T, kv jetstream.KeyValue) (*http.Server, string) { + t.Helper() + + fakeSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})) + + t.Cleanup(fakeSrv.Close) + + HTTPPort := freePort(t) + server := StartHttpApi(test.SilentLogger(), &test.MockJS{}, kv, HTTPPort, fakeSrv.URL) + + t.Cleanup(func() { server.Shutdown(context.Background()) }) //nolint:errcheck + + return server, HTTPPort +} + +func TestStartHttp(t *testing.T) { + t.Run("returns non-nil server with address derived from config", func(t *testing.T) { + server, HTTPPort := startTestServer(t, &test.MockKV{}) + + require.NotNil(t, server) + assert.Equal(t, ":"+HTTPPort, server.Addr) + }) + + t.Run("server handler is non-nil", func(t *testing.T) { + server, _ := startTestServer(t, &test.MockKV{}) + + assert.NotNil(t, server.Handler) + }) + + t.Run("unregistered path returns 404", func(t *testing.T) { + server, _ := startTestServer(t, &test.MockKV{}) + + req := httptest.NewRequest(http.MethodGet, "/nonexistent", nil) + w := httptest.NewRecorder() + server.Handler.ServeHTTP(w, req) + + assert.Equal(t, http.StatusNotFound, w.Code) + }) +} + +func newVideoHandler(StorageURL string, js *test.MockJS) *videoHandler { + return &videoHandler{ + logger: test.SilentLogger(), + js: js, + kv: &test.MockKV{}, + storageURL: StorageURL, + maxUploadBytes: 0, } } @@ -30,7 +91,7 @@ func TestUploadVideo(t *testing.T) { req.Header.Set("Content-Type", "text/plain") rec := httptest.NewRecorder() - h.UploadVideo(rec, req) + h.uploadVideoRoute(rec, req) assert.Equal(t, http.StatusBadRequest, rec.Code) assert.Contains(t, rec.Body.String(), "invalid multipart form") @@ -53,7 +114,7 @@ func TestUploadVideo(t *testing.T) { req := test.NewUploadRequest(t, "/jobs", tc.fileName, tc.content, tc.targetRes) rec := httptest.NewRecorder() - h.UploadVideo(rec, req) + h.uploadVideoRoute(rec, req) assert.Equal(t, http.StatusBadRequest, rec.Code) assert.Contains(t, rec.Body.String(), tc.wantMsg) @@ -66,19 +127,31 @@ func TestUploadVideo(t *testing.T) { req := test.NewUploadRequest(t, "/jobs", "video.mp4", []byte("data"), "1080p") rec := httptest.NewRecorder() - h.UploadVideo(rec, req) + h.uploadVideoRoute(rec, req) assert.Equal(t, http.StatusInternalServerError, rec.Code) assert.Contains(t, rec.Body.String(), "failed to save uploaded video") }) + t.Run("returns 500 when KV.Put fails during upload", func(t *testing.T) { + kv := &test.MockKV{PutErr: errors.New("kv unavailable")} + server, _ := startTestServer(t, kv) + + req := test.NewUploadRequest(t, "/jobs/upload", "video.mp4", []byte("data"), "1080p") + w := httptest.NewRecorder() + server.Handler.ServeHTTP(w, req) + + assert.Equal(t, http.StatusInternalServerError, w.Code) + assert.Contains(t, w.Body.String(), "failed to record job status") + }) + t.Run("Does not publish to NATS when saving fails", func(t *testing.T) { js := &test.MockJS{} h := newVideoHandler("\x00", js) req := test.NewUploadRequest(t, "/jobs", "video.mp4", []byte("data"), "1080p") rec := httptest.NewRecorder() - h.UploadVideo(rec, req) + h.uploadVideoRoute(rec, req) assert.False(t, js.PublishCalled, "publish should not be called when save fails") }) @@ -103,7 +176,7 @@ func TestDownloadVideo(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/jobs", strings.NewReader(tc.body)) rec := httptest.NewRecorder() - h.DownloadVideo(rec, req) + h.downloadVideoRoute(rec, req) assert.Equal(t, http.StatusBadRequest, rec.Code) if tc.wantMsg != "" { @@ -117,7 +190,7 @@ func TestDownloadVideo(t *testing.T) { req := test.NewDownloadRequest(t, "abc-123", "video.mp4") rec := httptest.NewRecorder() - h.DownloadVideo(rec, req) + h.downloadVideoRoute(rec, req) assert.Equal(t, http.StatusInternalServerError, rec.Code) assert.Contains(t, rec.Body.String(), "failed to fetch video") diff --git a/backend/video-upload/internal/handler/publisher_integration_test.go b/backend/video-upload/internal/handler/publisher_integration_test.go deleted file mode 100644 index 17d266d..0000000 --- a/backend/video-upload/internal/handler/publisher_integration_test.go +++ /dev/null @@ -1,71 +0,0 @@ -//go:build integration - -package handler_test - -import ( - "context" - "encoding/json" - "testing" - "time" - "video-upload/internal/handler" - "video-upload/internal/service" - "video-upload/internal/test" - - "github.com/nats-io/nats.go" - "github.com/nats-io/nats.go/jetstream" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - natstc "github.com/testcontainers/testcontainers-go/modules/nats" -) - -func TestPublishVideoMetadata(t *testing.T) { - t.Run("returns error when no stream exists for subject", func(t *testing.T) { - ctx := context.Background() - - container, err := natstc.Run(ctx, "nats:2.10-alpine") - require.NoError(t, err) - t.Cleanup(func() { _ = container.Terminate(ctx) }) - - url, err := container.ConnectionString(ctx) - require.NoError(t, err) - - nc, err := nats.Connect(url, - nats.RetryOnFailedConnect(true), - nats.MaxReconnects(10), - nats.ReconnectWait(200*time.Millisecond), - ) - require.NoError(t, err) - t.Cleanup(nc.Close) - - js, err := jetstream.New(nc) - require.NoError(t, err) - - err = handler.PublishVideoMetadata(js, service.SceneSplitMessage{JobID: "job-1"}) - - assert.Error(t, err) - }) - - t.Run("publishes correct payload to NATS", func(t *testing.T) { - js, nc := test.SetupNats(t) - - received := make(chan []byte, 1) - sub, err := nc.Subscribe("jobs.video.scene-split", func(msg *nats.Msg) { - received <- msg.Data - }) - require.NoError(t, err) - t.Cleanup(func() { _ = sub.Unsubscribe() }) - - msg := service.SceneSplitMessage{JobID: "job-1"} - err = handler.PublishVideoMetadata(js, msg) - require.NoError(t, err) - - select { - case data := <-received: - var got service.SceneSplitMessage - require.NoError(t, json.Unmarshal(data, &got)) - assert.Equal(t, msg, got) - case <-time.After(3 * time.Second): - t.Fatal("timed out waiting for message") - } - }) -} diff --git a/backend/video-upload/internal/handler/publisher_unit_test.go b/backend/video-upload/internal/handler/publisher_unit_test.go deleted file mode 100644 index 70a86a0..0000000 --- a/backend/video-upload/internal/handler/publisher_unit_test.go +++ /dev/null @@ -1,26 +0,0 @@ -//go:build unit - -package handler_test - -import ( - "errors" - "testing" - "video-upload/internal/handler" - "video-upload/internal/service" - "video-upload/internal/test" - - "github.com/stretchr/testify/assert" -) - -func TestCatchesError(t *testing.T) { - t.Run("nats publish errors", func(t *testing.T) { - publishErr := errors.New("nats publish failed") - mock := &test.MockJS{PublishErr: publishErr} - - err := handler.PublishVideoMetadata(mock, service.SceneSplitMessage{ - JobID: "job-1", - }) - - assert.ErrorIs(t, err, publishErr) - }) -} diff --git a/backend/video-upload/internal/middleware/cors_unit_test.go b/backend/video-upload/internal/middleware/cors_unit_test.go deleted file mode 100644 index 5a3b7fa..0000000 --- a/backend/video-upload/internal/middleware/cors_unit_test.go +++ /dev/null @@ -1,74 +0,0 @@ -//go:build unit - -package middleware_test - -import ( - "net/http" - "net/http/httptest" - "testing" - "video-upload/internal/middleware" - - "github.com/stretchr/testify/assert" -) - -func TestCors(t *testing.T) { - tests := []struct { - name string - method string - origin string - expectStatus int - expectAllowOrigin string - expectNextCalled bool - }{ - { - name: "Allowed origin GET receives CORS headers", - method: http.MethodGet, - origin: "http://localhost:5173", - expectStatus: http.StatusOK, - expectAllowOrigin: "http://localhost:5173", - expectNextCalled: true, - }, - { - name: "Disallowed origin GET receives no CORS headers", - method: http.MethodGet, - origin: "http://evil.com", - expectStatus: http.StatusOK, - expectNextCalled: true, - }, - { - name: "Allowed origin OPTIONS returns 204, next not called", - method: http.MethodOptions, - origin: "http://localhost:5173", - expectStatus: http.StatusNoContent, - expectAllowOrigin: "http://localhost:5173", - expectNextCalled: false, - }, - { - name: "Disallowed origin OPTIONS returns 403, next not called", - method: http.MethodOptions, - origin: "http://evil.com", - expectStatus: http.StatusForbidden, - expectNextCalled: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - handlerCalled := false - mockHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - handlerCalled = true - w.WriteHeader(http.StatusOK) - }) - - req := httptest.NewRequest(tt.method, "/test", nil) - req.Header.Set("Origin", tt.origin) - recorder := httptest.NewRecorder() - - middleware.Cors(mockHandler).ServeHTTP(recorder, req) - - assert.Equal(t, tt.expectStatus, recorder.Code) - assert.Equal(t, tt.expectAllowOrigin, recorder.Header().Get("Access-Control-Allow-Origin")) - assert.Equal(t, tt.expectNextCalled, handlerCalled) - }) - } -} diff --git a/backend/video-upload/internal/middleware/logging.go b/backend/video-upload/internal/middleware/logging.go deleted file mode 100644 index d366d95..0000000 --- a/backend/video-upload/internal/middleware/logging.go +++ /dev/null @@ -1,47 +0,0 @@ -package middleware - -import ( - "log" - "log/slog" - "net/http" - "os" - "time" -) - -// wrapper to extend http response writer to expose -// the status codes -type WrappedWriter struct { - http.ResponseWriter - StatusCode int -} - -func (w *WrappedWriter) WriteHeader(statuscode int) { - w.ResponseWriter.WriteHeader(statuscode) - w.StatusCode = statuscode -} - -// General Structured logger for code -func StructuredLogger(prodMode bool) *slog.Logger { - level := slog.LevelDebug - if prodMode { - level = slog.LevelInfo - } - h := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: level}) - - return slog.New(h).With("service", "video-upload") -} - -// logging middleware to track status codes, the url path, and response latency -func ApiRequestLogging(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - start := time.Now() - - wrapped := &WrappedWriter{ - ResponseWriter: w, - StatusCode: http.StatusOK, - } - - next.ServeHTTP(wrapped, r) - log.Println(wrapped.StatusCode, r.Method, r.URL.Path, time.Since(start)) - }) -} diff --git a/backend/video-upload/internal/service/nats_messages.go b/backend/video-upload/internal/service/nats_messages.go deleted file mode 100644 index e77398d..0000000 --- a/backend/video-upload/internal/service/nats_messages.go +++ /dev/null @@ -1,12 +0,0 @@ -package service - -type SceneSplitMessage struct { - JobID string `json:"job_id"` - TargetResolution string `json:"target_resolution"` - StorageURL string `json:"storage_url"` -} - -// JobCompleteMessage is published by the video-recombiner to jobs.complete when a job finishes. -type JobCompleteMessage struct { - JobID string `json:"job_id"` -} diff --git a/backend/video-upload/internal/storage/health_check.go b/backend/video-upload/internal/storage/health_check.go deleted file mode 100644 index f93d7e7..0000000 --- a/backend/video-upload/internal/storage/health_check.go +++ /dev/null @@ -1,29 +0,0 @@ -package storage - -import ( - "fmt" - "log/slog" - "net/http" -) - -// send an http request to shared to storage to see if its reachable -func CheckHealth(storageURL string, logger *slog.Logger) error { - resp, err := http.Get(storageURL + "/dir/status") - - if err != nil { - return fmt.Errorf("error connecting to seedweedfs: %w", err) - } - - defer func() { - err := resp.Body.Close() - if err != nil { - logger.Warn("failed to close resp body for check health", "err", err) - } - }() - - if resp.StatusCode >= 500 { - return fmt.Errorf("seedweedfs returned status %d", resp.StatusCode) - } - - return nil -} diff --git a/backend/video-upload/internal/storage/health_check_integration_test.go b/backend/video-upload/internal/storage/health_check_integration_test.go deleted file mode 100644 index 978f481..0000000 --- a/backend/video-upload/internal/storage/health_check_integration_test.go +++ /dev/null @@ -1,25 +0,0 @@ -//go:build integration - -package storage_test - -import ( - "testing" - "video-upload/internal/storage" - "video-upload/internal/test" - - "github.com/stretchr/testify/assert" -) - -func TestCheckHealth(t *testing.T) { - t.Run("storage health check fails when seedweedfs is unreachable", func(t *testing.T) { - err := storage.CheckHealth("http://localhost:1", test.SilentLogger()) - - assert.Error(t, err) - }) - - t.Run("storage health check passes when seedweedfs is reachable", func(t *testing.T) { - err := storage.CheckHealth(sharedFilerUrl, test.SilentLogger()) - - assert.NoError(t, err) - }) -} diff --git a/backend/video-upload/internal/test/http_fixtures.go b/backend/video-upload/internal/test/http_fixtures.go index 4900428..3d06feb 100644 --- a/backend/video-upload/internal/test/http_fixtures.go +++ b/backend/video-upload/internal/test/http_fixtures.go @@ -3,6 +3,7 @@ package test import ( "errors" "fmt" + "github.com/stretchr/testify/require" "io" "log/slog" "net" @@ -10,8 +11,6 @@ import ( "strconv" "strings" "testing" - - "github.com/stretchr/testify/require" ) func SilentLogger() *slog.Logger {