From 5a7f29c116e9e12e45c72c9f842ba210f6202f9c Mon Sep 17 00:00:00 2001 From: lethdat Date: Thu, 2 Apr 2026 11:01:30 +0000 Subject: [PATCH] update cicd --- .claude/settings.local.json | 7 + .deploy/stream.api-production.yaml | 89 ++ Dockerfile | 10 +- Dockerfile.agent | 16 + cmd/agent/main.go | 54 ++ cmd/server/main.go | 5 +- cmd/worker/main.go | 1 - config.example.yaml | 19 +- config.yaml | 15 +- go.mod | 29 +- go.sum | 112 ++- internal/adapters/redis/adapter.go | 98 ++- internal/adapters/redis/dlq.go | 11 +- internal/api/proto/app/v1/admin.pb.go | 777 ++++++++++++++---- internal/api/proto/app/v1/admin_grpc.pb.go | 152 ++++ internal/api/proto/app/v1/common.pb.go | 181 ++-- .../api/proto/app/v1/video_metadata.pb.go | 221 +++++ .../proto/app/v1/video_metadata_grpc.pb.go | 121 +++ internal/config/config.go | 8 - internal/dto/job.go | 7 + internal/repository/job_repository.go | 79 ++ .../service_admin_jobs_agents_test.go | 220 ++++- .../__test__/service_video_metadata_test.go | 245 ++++++ .../service/__test__/testdb_setup_test.go | 18 +- internal/service/admin_helpers.go | 91 +- internal/service/catalog_mapper.go | 21 +- internal/service/interface.go | 10 + internal/service/notification_events_test.go | 6 +- internal/service/referral_helpers.go | 7 +- internal/service/register.go | 1 + internal/service/service_admin_jobs_agents.go | 82 ++ internal/service/service_core.go | 10 +- internal/service/service_job.go | 528 ++++++++++-- internal/service/service_popup_ads_test.go | 48 +- internal/service/service_videos.go | 142 ++++ internal/transport/grpc/agent_lifecycle.go | 3 +- internal/transport/grpc/auth.go | 2 +- internal/transport/grpc/server.go | 15 +- internal/transport/grpc/stream_handlers.go | 27 +- .../transport/mqtt/notification_publisher.go | 5 +- internal/workflow/agent/agent.go | 576 +++++++++++++ internal/workflow/agent/docker.go | 154 ++++ internal/workflow/agent/parser.go | 26 + internal/workflow/agent/parser_test.go | 43 + internal/workflow/render/workflow.go | 32 + proto/app/v1/admin.proto | 41 + proto/app/v1/common.proto | 7 + proto/app/v1/video_metadata.proto | 23 + .../MIGRATION_GUIDE.md | 0 script/create_database.sql | 376 +++++++++ .../full_player_configs_migration.sql | 0 .../install_player_configs.sql | 0 .../migrate_player_configs.sh | 0 run_migration.sh => script/run_migration.sh | 0 54 files changed, 4298 insertions(+), 473 deletions(-) create mode 100644 .claude/settings.local.json create mode 100644 .deploy/stream.api-production.yaml create mode 100644 Dockerfile.agent create mode 100644 cmd/agent/main.go delete mode 100644 cmd/worker/main.go create mode 100644 internal/api/proto/app/v1/video_metadata.pb.go create mode 100644 internal/api/proto/app/v1/video_metadata_grpc.pb.go create mode 100644 internal/service/__test__/service_video_metadata_test.go create mode 100644 internal/workflow/agent/agent.go create mode 100644 internal/workflow/agent/docker.go create mode 100644 internal/workflow/agent/parser.go create mode 100644 internal/workflow/agent/parser_test.go create mode 100644 proto/app/v1/video_metadata.proto rename MIGRATION_GUIDE.md => script/MIGRATION_GUIDE.md (100%) create mode 100644 script/create_database.sql rename full_player_configs_migration.sql => script/full_player_configs_migration.sql (100%) rename install_player_configs.sql => script/install_player_configs.sql (100%) rename migrate_player_configs.sh => script/migrate_player_configs.sh (100%) rename run_migration.sh => script/run_migration.sh (100%) diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..2df2bfb --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,7 @@ +{ + "permissions": { + "allow": [ + "Bash(go test:*)" + ] + } +} diff --git a/.deploy/stream.api-production.yaml b/.deploy/stream.api-production.yaml new file mode 100644 index 0000000..12af4c6 --- /dev/null +++ b/.deploy/stream.api-production.yaml @@ -0,0 +1,89 @@ +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: stream-api-config + namespace: stream-production + labels: + app: stream-api +data: + config.yaml: | + server: + grpc_port: "9000" + mode: "release" + + database: + dsn: "host=47.84.63.130 user=postgres password=D@tkhong9 dbname=video_db port=5432 sslmode=disable" + + redis: + addr: "47.84.62.226:6379" + password: "pass123" + db: 3 + + google: + client_id: "781933579930-avgrrvdj26ajqujs0snajk62jgch2jl5.apps.googleusercontent.com" + client_secret: "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" + redirect_url: "https://hlstiktok.com/auth/google/callback" + state_ttl_minutes: 10 + + internal: + marker: "your-secret-marker" + + email: + from: "no-reply@picpic.com" + + aws: + region: "us-east-1" + bucket: "" + access_key: "" + secret_key: "" + +--- +kind: Service +apiVersion: v1 +metadata: + name: stream-api-svc + namespace: stream-production + labels: + app: stream-api +spec: + selector: + app: stream-api + ports: + - protocol: TCP + port: 80 + targetPort: 9000 + type: NodePort +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: stream-api-dep + namespace: stream-production + labels: + app: stream-api +spec: + replicas: 1 + selector: + matchLabels: + app: stream-api + template: + metadata: + labels: + app: stream-api + spec: + # imagePullSecrets: + # - name: registry-production-secret + containers: + - name: stream-api + image: registry.awing.vn/stream-production/stream-api:$BUILD_NUMBER + ports: + - containerPort: 9000 + volumeMounts: + - name: stream-api-config + mountPath: /config.yaml + subPath: config.yaml + volumes: + - name: stream-api-config + configMap: + name: stream-api-config diff --git a/Dockerfile b/Dockerfile index 25c5fb4..bba627f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,14 +3,14 @@ FROM golang:1.25.6-alpine AS builder WORKDIR /app COPY go.mod go.sum ./ - RUN go mod download COPY . . -RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -installsuffix cgo -ldflags="-s -w" -o main ./cmd/grpc +RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -installsuffix cgo -ldflags="-s -w" -o server ./cmd/server + FROM scratch -COPY --from=builder /app/main /main +COPY --from=builder /app/server /server -EXPOSE 8080 -ENTRYPOINT ["/main"] \ No newline at end of file +EXPOSE 9000 +ENTRYPOINT ["/server"] diff --git a/Dockerfile.agent b/Dockerfile.agent new file mode 100644 index 0000000..7fd5e30 --- /dev/null +++ b/Dockerfile.agent @@ -0,0 +1,16 @@ +FROM golang:1.25.6-alpine AS builder + +WORKDIR /app +COPY go.mod go.sum ./ +RUN go mod download + +COPY . . +RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -installsuffix cgo -ldflags="-s -w" -o agent ./cmd/agent + +FROM alpine:latest + +RUN apk add --no-cache docker-cli + +COPY --from=builder /app/agent /bin/agent + +CMD ["/bin/agent"] diff --git a/cmd/agent/main.go b/cmd/agent/main.go new file mode 100644 index 0000000..22cf81d --- /dev/null +++ b/cmd/agent/main.go @@ -0,0 +1,54 @@ +package main + +import ( + "context" + "log" + "os" + "os/signal" + "runtime" + "syscall" + + "stream.api/internal/workflow/agent" +) + +func main() { + serverAddr := os.Getenv("AGENT_SERVER") + if serverAddr == "" { + serverAddr = "localhost:9000" + } + + secret := os.Getenv("APP_INTERNAL_MARKER") + if secret == "" { + secret = os.Getenv("INTERNAL_MARKER") + } + if secret == "" { + log.Fatal("APP_INTERNAL_MARKER environment variable is required") + } + + capacity := runtime.NumCPU() + + log.Printf("Starting stream.api agent") + log.Printf("Server: %s", serverAddr) + log.Printf("Capacity: %d", capacity) + + a, err := agent.New(serverAddr, secret, capacity) + if err != nil { + log.Fatalf("Failed to initialize agent: %v", err) + } + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + sigCh := make(chan os.Signal, 1) + signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) + + go func() { + <-sigCh + log.Println("Shutting down agent...") + cancel() + }() + + if err := a.Run(ctx); err != nil { + log.Fatalf("Agent error: %v", err) + } +} diff --git a/cmd/server/main.go b/cmd/server/main.go index 5f31b93..0e8f29a 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -45,7 +45,10 @@ func main() { // 4. Initialize Components appLogger := logger.NewLogger(cfg.Server.Mode) - module, err := grpc.NewGRPCModule(context.Background(), cfg, db, rdb, appLogger) + appCtx, appCancel := context.WithCancel(context.Background()) + defer appCancel() + + module, err := grpc.NewGRPCModule(appCtx, cfg, db, rdb, appLogger) if err != nil { log.Fatalf("Failed to setup gRPC runtime module: %v", err) } diff --git a/cmd/worker/main.go b/cmd/worker/main.go deleted file mode 100644 index 4df0094..0000000 --- a/cmd/worker/main.go +++ /dev/null @@ -1 +0,0 @@ -package worker diff --git a/config.example.yaml b/config.example.yaml index 58ebc1b..eebc976 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -1,5 +1,4 @@ server: - port: "8080" grpc_port: "9000" mode: "debug" # debug or release @@ -11,28 +10,15 @@ redis: password: "" db: 0 -jwt: - secret: "your_super_secret_jwt_key" - google: client_id: "your_google_client_id" client_secret: "your_google_client_secret" redirect_url: "http://localhost:8080/auth/google/callback" state_ttl_minutes: 10 -frontend: - base_url: "http://localhost:5173" - google_auth_finalize_path: "/auth/google/finalize" - internal: marker: "your_shared_internal_auth_marker" -cors: - allow_origins: - - "http://localhost:5173" - - "http://localhost:8080" - - "http://localhost:8081" - email: from: "no-reply@picpic.com" @@ -43,8 +29,11 @@ aws: secret_key: "your_secret_key" render: - agent_secret: "your_render_agent_secret" enable_metrics: true enable_tracing: false otlp_endpoint: "" service_name: "stream-api-render" + +# Agent runtime uses environment variables rather than this YAML file. +# Required: APP_INTERNAL_MARKER +# Optional: AGENT_SERVER (default localhost:9000), FORCE_NEW_ID, AGENT_IMAGE, HOST_DOCKER_SOCK diff --git a/config.yaml b/config.yaml index 59a40fd..e35d905 100644 --- a/config.yaml +++ b/config.yaml @@ -10,16 +10,19 @@ redis: password: "pass123" db: 3 -jwt: - secret: "your_super_secret_jwt_key" - google: - client_id: "your_google_client_id" - client_secret: "your_google_client_secret" - redirect_url: "http://localhost:8080/auth/google/callback" + client_id: "781933579930-avgrrvdj26ajqujs0snajk62jgch2jl5.apps.googleusercontent.com" + client_secret: "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" + redirect_url: "https://hlstiktok.com/auth/google/callback" email: from: "no-reply@picpic.com" internal: marker: "your-secret-marker" + +aws: + region: "us-east-1" + bucket: "your-bucket-name" + access_key: "your_access_key" + secret_key: "your_secret_key" \ No newline at end of file diff --git a/go.mod b/go.mod index 8c29700..ad189ce 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/aws/aws-sdk-go-v2/config v1.32.7 github.com/aws/aws-sdk-go-v2/credentials v1.19.7 github.com/aws/aws-sdk-go-v2/service/s3 v1.95.1 + github.com/docker/docker v26.1.5+incompatible github.com/eclipse/paho.mqtt.golang v1.5.1 github.com/google/uuid v1.6.0 github.com/lib/pq v1.11.2 @@ -26,10 +27,33 @@ require ( ) require ( + github.com/Microsoft/go-winio v0.4.21 // indirect + github.com/containerd/log v0.1.0 // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/docker/go-connections v0.6.0 // indirect + github.com/docker/go-units v0.5.0 // indirect github.com/dustin/go-humanize v1.0.1 // 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/gogo/protobuf v1.3.2 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/term v0.5.2 // indirect + github.com/morikuni/aec v1.1.0 // indirect github.com/ncruces/go-strftime v1.0.0 // 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/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 // indirect + go.opentelemetry.io/otel v1.42.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.42.0 // indirect + go.opentelemetry.io/otel/metric v1.42.0 // indirect + go.opentelemetry.io/otel/trace v1.42.0 // indirect + golang.org/x/time v0.15.0 // indirect + gotest.tools/v3 v3.5.2 // indirect modernc.org/libc v1.70.0 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.11.0 // indirect @@ -67,7 +91,6 @@ require ( github.com/jinzhu/now v1.1.5 // indirect github.com/mattn/go-sqlite3 v1.14.22 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect - github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/sagikazarmark/locafero v0.11.0 // indirect github.com/sony/gobreaker v1.0.0 github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect @@ -78,12 +101,12 @@ require ( go.uber.org/multierr v1.10.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/mod v0.33.0 // indirect - golang.org/x/net v0.50.0 // indirect + golang.org/x/net v0.51.0 // indirect golang.org/x/sync v0.19.0 // indirect golang.org/x/sys v0.42.0 // indirect golang.org/x/text v0.34.0 // indirect golang.org/x/tools v0.42.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect gorm.io/datatypes v1.2.4 gorm.io/driver/mysql v1.5.7 // indirect gorm.io/hints v1.1.0 // indirect diff --git a/go.sum b/go.sum index b12b890..f87dfd5 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,10 @@ cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdB cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +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.4.21 h1:+6mVbXh4wPzUrl1COX9A+ZCvEpYsOBZ6/+kwDnvLyro= +github.com/Microsoft/go-winio v0.4.21/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= github.com/aws/aws-sdk-go-v2 v1.41.1 h1:ABlyEARCDLN034NhxlRUSZr4l71mh+T5KAeGh6cerhU= github.com/aws/aws-sdk-go-v2 v1.41.1/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 h1:489krEF9xIGkOaaX3CE/Be2uWjiXrkCH6gUX+bZA/BU= @@ -44,21 +48,36 @@ github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= +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/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/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 v26.1.5+incompatible h1:NEAxTwEjxV6VbBMBoGG3zPqbiJosIApZjxlbrG9q3/g= +github.com/docker/docker v26.1.5+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-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/eclipse/paho.mqtt.golang v1.5.1 h1:/VSOv3oDLlpqR2Epjn1Q7b2bSTplJIeV2ISgCl2W7nE= github.com/eclipse/paho.mqtt.golang v1.5.1/go.mod h1:1/yJCneuyOoCOzKSsOTUc0AJfpsItBGWvYpBLimhArU= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +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= @@ -68,6 +87,8 @@ github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpv github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= @@ -82,6 +103,8 @@ 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/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +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/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= @@ -97,6 +120,8 @@ github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkr github.com/jinzhu/now v1.1.2/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 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= @@ -110,10 +135,22 @@ github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/microsoft/go-mssqldb v0.17.0 h1:Fto83dMZPnYv1Zwx5vHHxpNraeEaUlQ/hhHLgZiaenE= github.com/microsoft/go-mssqldb v0.17.0/go.mod h1:OkoNGhGEs8EZqchVTtochlXruEhEOaO4S0d2sB5aeGQ= +github.com/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/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.1.0 h1:vBBl0pUnvi/Je71dsRrhMBtreIqNMYErSAbEeb8jrXQ= +github.com/morikuni/aec v1.1.0/go.mod h1:xDRgiq/iw5l+zkao76YTKzKttOp2cwPEne25HDkJnBw= github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +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/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +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/redis/go-redis/v9 v9.17.2 h1:P2EGsA4qVIM3Pp+aPocCJ7DguDHhqrXNhVcEp4ViluI= @@ -124,6 +161,9 @@ github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0t github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sony/gobreaker v1.0.0 h1:feX5fGGXSl3dYd4aHZItw+FpHLvvoaqkawKjVNiFMNQ= github.com/sony/gobreaker v1.0.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= @@ -137,24 +177,35 @@ github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3A github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 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/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 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/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= -go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= -go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= -go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= -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/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/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= -go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg= +go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho= +go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 h1:THuZiwpQZuHPul65w4WcwEnkX2QIuMT+UFoOrygtoJw= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0/go.mod h1:J2pvYM5NGHofZ2/Ru6zw/TNWnEQp5crgyDeSrYpXkAw= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.42.0 h1:uLXP+3mghfMf7XmV4PkGfFhFKuNWoCvvx5wP/wOXo0o= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.42.0/go.mod h1:v0Tj04armyT59mnURNUJf7RCKcKzq+lgJs6QSjHjaTc= +go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4= +go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI= +go.opentelemetry.io/otel/sdk v1.42.0 h1:LyC8+jqk6UJwdrI/8VydAq/hvkFKNHZVIWuslJXYsDo= +go.opentelemetry.io/otel/sdk v1.42.0/go.mod h1:rGHCAxd9DAph0joO4W6OPwxjNTYWghRWmkHuGbayMts= +go.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9RKCAZ3YGuA= +go.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc= +go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY= +go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc= +go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= +go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= @@ -163,27 +214,58 @@ go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= -golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= -golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +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= golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.6.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/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= +golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +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.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU= google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= @@ -216,6 +298,8 @@ gorm.io/hints v1.1.0 h1:Lp4z3rxREufSdxn4qmkK3TLDltrM10FLTHiuqwDPvXw= gorm.io/hints v1.1.0/go.mod h1:lKQ0JjySsPBj3uslFzY3JhYDtqEwzm+G1hv8rWujB6Y= gorm.io/plugin/dbresolver v1.6.2 h1:F4b85TenghUeITqe3+epPSUtHH7RIk3fXr5l83DF8Pc= gorm.io/plugin/dbresolver v1.6.2/go.mod h1:tctw63jdrOezFR9HmrKnPkmig3m5Edem9fdxk9bQSzM= +gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= +gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis= modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= modernc.org/ccgo/v4 v4.32.0 h1:hjG66bI/kqIPX1b2yT6fr/jt+QedtP2fqojG2VrFuVw= diff --git a/internal/adapters/redis/adapter.go b/internal/adapters/redis/adapter.go index bc8b646..b9400d4 100644 --- a/internal/adapters/redis/adapter.go +++ b/internal/adapters/redis/adapter.go @@ -3,7 +3,9 @@ package redis import ( "context" "encoding/json" + "errors" "fmt" + "strings" "time" goredis "github.com/redis/go-redis/v9" @@ -12,14 +14,24 @@ import ( ) const ( - JobQueueKey = "render:jobs:queue" - LogChannel = "render:jobs:logs" - ResourceChannel = "render:agents:resources" - JobUpdateChannel = "render:jobs:updates" + JobQueueKey = "render:jobs:queue:v2" + JobInflightKey = "render:jobs:inflight" + JobInflightMetaKey = "render:jobs:inflight:meta" + JobSequenceKey = "render:jobs:queue:seq" + LogChannel = "render:jobs:logs" + ResourceChannel = "render:agents:resources" + JobUpdateChannel = "render:jobs:updates" + defaultQueuePoll = time.Second + defaultInflightTTL = 15 * time.Minute ) type RedisAdapter struct{ client *goredis.Client } +type inflightMeta struct { + ReadyScore float64 `json:"ready_score"` + ClaimedAt int64 `json:"claimed_at"` +} + func NewAdapter(addr, password string, db int) (*RedisAdapter, error) { client := goredis.NewClient(&goredis.Options{Addr: addr, Password: password, DB: db}) if err := client.Ping(context.Background()).Err(); err != nil { @@ -31,13 +43,26 @@ func NewAdapter(addr, password string, db int) (*RedisAdapter, error) { func (r *RedisAdapter) Client() *goredis.Client { return r.client } func (r *RedisAdapter) Enqueue(ctx context.Context, job *model.Job) error { - data, err := json.Marshal(job) + if job == nil || strings.TrimSpace(job.ID) == "" { + return errors.New("job id is required") + } + priority := int64(0) + if job.Priority != nil { + priority = *job.Priority + } + seq, err := r.client.Incr(ctx, JobSequenceKey).Result() if err != nil { return err } - timestamp := time.Now().UnixNano() - score := float64(-(int64(*job.Priority) * 1000000000) - timestamp) - return r.client.ZAdd(ctx, JobQueueKey, goredis.Z{Score: score, Member: data}).Err() + score := float64((-priority * 1_000_000_000_000) + seq) + jobID := strings.TrimSpace(job.ID) + if err := r.client.HDel(ctx, JobInflightMetaKey, jobID).Err(); err != nil { + return err + } + if err := r.client.ZRem(ctx, JobInflightKey, jobID).Err(); err != nil { + return err + } + return r.client.ZAdd(ctx, JobQueueKey, goredis.Z{Score: score, Member: jobID}).Err() } func (r *RedisAdapter) Dequeue(ctx context.Context) (*model.Job, error) { @@ -56,27 +81,57 @@ func (r *RedisAdapter) Dequeue(ctx context.Context) (*model.Job, error) { select { case <-ctx.Done(): return nil, ctx.Err() - case <-time.After(time.Second): + case <-time.After(defaultQueuePoll): continue } } - var raw []byte - switch member := res[0].Member.(type) { - case string: - raw = []byte(member) - case []byte: - raw = member - default: - return nil, fmt.Errorf("unexpected redis queue payload type %T", member) - } - var job model.Job - if err := json.Unmarshal(raw, &job); err != nil { + jobID := fmt.Sprintf("%v", res[0].Member) + meta, err := json.Marshal(inflightMeta{ReadyScore: res[0].Score, ClaimedAt: time.Now().Unix()}) + if err != nil { return nil, err } - return &job, nil + leaseScore := float64(time.Now().Add(defaultInflightTTL).Unix()) + pipe := r.client.TxPipeline() + pipe.ZAdd(ctx, JobInflightKey, goredis.Z{Score: leaseScore, Member: jobID}) + pipe.HSet(ctx, JobInflightMetaKey, jobID, meta) + if _, err := pipe.Exec(ctx); err != nil { + return nil, err + } + return &model.Job{ID: jobID}, nil } } +func (r *RedisAdapter) Ack(ctx context.Context, jobID string) error { + jobID = strings.TrimSpace(jobID) + if jobID == "" { + return nil + } + pipe := r.client.TxPipeline() + pipe.ZRem(ctx, JobQueueKey, jobID) + pipe.ZRem(ctx, JobInflightKey, jobID) + pipe.HDel(ctx, JobInflightMetaKey, jobID) + _, err := pipe.Exec(ctx) + return err +} + +func (r *RedisAdapter) ListExpiredInflight(ctx context.Context, now time.Time, limit int64) ([]string, error) { + if limit <= 0 { + limit = 100 + } + return r.client.ZRangeByScore(ctx, JobInflightKey, &goredis.ZRangeBy{Min: "-inf", Max: fmt.Sprintf("%d", now.Unix()), Offset: 0, Count: limit}).Result() +} + +func (r *RedisAdapter) TouchInflight(ctx context.Context, jobID string, ttl time.Duration) error { + jobID = strings.TrimSpace(jobID) + if jobID == "" { + return nil + } + if ttl <= 0 { + ttl = defaultInflightTTL + } + return r.client.ZAddXX(ctx, JobInflightKey, goredis.Z{Score: float64(time.Now().Add(ttl).Unix()), Member: jobID}).Err() +} + func (r *RedisAdapter) Publish(ctx context.Context, jobID string, logLine string, progress float64) error { payload, err := json.Marshal(dto.LogEntry{JobID: jobID, Line: logLine, Progress: progress}) if err != nil { @@ -173,6 +228,7 @@ func (r *RedisAdapter) SubscribeJobUpdates(ctx context.Context) (<-chan string, }() return ch, nil } + func (c *RedisAdapter) Set(ctx context.Context, key string, value interface{}, expiration time.Duration) error { return c.client.Set(ctx, key, value, expiration).Err() } diff --git a/internal/adapters/redis/dlq.go b/internal/adapters/redis/dlq.go index 6008b81..d028618 100644 --- a/internal/adapters/redis/dlq.go +++ b/internal/adapters/redis/dlq.go @@ -12,8 +12,8 @@ import ( ) const ( - dlqKey = "picpic:dlq" - dlqMetaPrefix = "picpic:dlq:meta:" + dlqKey = "render:jobs:dlq" + dlqMetaPrefix = "render:jobs:dlq:meta:" ) type DeadLetterQueue struct { @@ -33,11 +33,16 @@ func NewDeadLetterQueue(client *redis.Client) *DeadLetterQueue { // Add adds a failed job to the DLQ func (dlq *DeadLetterQueue) Add(ctx context.Context, job *model.Job, reason string) error { + retryCount := int64(0) + if job != nil && job.RetryCount != nil { + retryCount = *job.RetryCount + } + entry := DLQEntry{ Job: job, FailureTime: time.Now(), Reason: reason, - RetryCount: *job.RetryCount, + RetryCount: retryCount, } data, err := json.Marshal(entry) diff --git a/internal/api/proto/app/v1/admin.pb.go b/internal/api/proto/app/v1/admin.pb.go index 9dd0f57..6c8a1c0 100644 --- a/internal/api/proto/app/v1/admin.pb.go +++ b/internal/api/proto/app/v1/admin.pb.go @@ -4803,6 +4803,398 @@ func (x *RetryAdminJobResponse) GetJob() *AdminJob { return nil } +type ListAdminDlqJobsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Offset int32 `protobuf:"varint,1,opt,name=offset,proto3" json:"offset,omitempty"` + Limit int32 `protobuf:"varint,2,opt,name=limit,proto3" json:"limit,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListAdminDlqJobsRequest) Reset() { + *x = ListAdminDlqJobsRequest{} + mi := &file_app_v1_admin_proto_msgTypes[79] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListAdminDlqJobsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListAdminDlqJobsRequest) ProtoMessage() {} + +func (x *ListAdminDlqJobsRequest) ProtoReflect() protoreflect.Message { + mi := &file_app_v1_admin_proto_msgTypes[79] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListAdminDlqJobsRequest.ProtoReflect.Descriptor instead. +func (*ListAdminDlqJobsRequest) Descriptor() ([]byte, []int) { + return file_app_v1_admin_proto_rawDescGZIP(), []int{79} +} + +func (x *ListAdminDlqJobsRequest) GetOffset() int32 { + if x != nil { + return x.Offset + } + return 0 +} + +func (x *ListAdminDlqJobsRequest) GetLimit() int32 { + if x != nil { + return x.Limit + } + return 0 +} + +type ListAdminDlqJobsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Items []*AdminDlqEntry `protobuf:"bytes,1,rep,name=items,proto3" json:"items,omitempty"` + Total int64 `protobuf:"varint,2,opt,name=total,proto3" json:"total,omitempty"` + Offset int32 `protobuf:"varint,3,opt,name=offset,proto3" json:"offset,omitempty"` + Limit int32 `protobuf:"varint,4,opt,name=limit,proto3" json:"limit,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListAdminDlqJobsResponse) Reset() { + *x = ListAdminDlqJobsResponse{} + mi := &file_app_v1_admin_proto_msgTypes[80] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListAdminDlqJobsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListAdminDlqJobsResponse) ProtoMessage() {} + +func (x *ListAdminDlqJobsResponse) ProtoReflect() protoreflect.Message { + mi := &file_app_v1_admin_proto_msgTypes[80] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListAdminDlqJobsResponse.ProtoReflect.Descriptor instead. +func (*ListAdminDlqJobsResponse) Descriptor() ([]byte, []int) { + return file_app_v1_admin_proto_rawDescGZIP(), []int{80} +} + +func (x *ListAdminDlqJobsResponse) GetItems() []*AdminDlqEntry { + if x != nil { + return x.Items + } + return nil +} + +func (x *ListAdminDlqJobsResponse) GetTotal() int64 { + if x != nil { + return x.Total + } + return 0 +} + +func (x *ListAdminDlqJobsResponse) GetOffset() int32 { + if x != nil { + return x.Offset + } + return 0 +} + +func (x *ListAdminDlqJobsResponse) GetLimit() int32 { + if x != nil { + return x.Limit + } + return 0 +} + +type GetAdminDlqJobRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetAdminDlqJobRequest) Reset() { + *x = GetAdminDlqJobRequest{} + mi := &file_app_v1_admin_proto_msgTypes[81] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetAdminDlqJobRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetAdminDlqJobRequest) ProtoMessage() {} + +func (x *GetAdminDlqJobRequest) ProtoReflect() protoreflect.Message { + mi := &file_app_v1_admin_proto_msgTypes[81] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetAdminDlqJobRequest.ProtoReflect.Descriptor instead. +func (*GetAdminDlqJobRequest) Descriptor() ([]byte, []int) { + return file_app_v1_admin_proto_rawDescGZIP(), []int{81} +} + +func (x *GetAdminDlqJobRequest) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +type GetAdminDlqJobResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Item *AdminDlqEntry `protobuf:"bytes,1,opt,name=item,proto3" json:"item,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetAdminDlqJobResponse) Reset() { + *x = GetAdminDlqJobResponse{} + mi := &file_app_v1_admin_proto_msgTypes[82] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetAdminDlqJobResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetAdminDlqJobResponse) ProtoMessage() {} + +func (x *GetAdminDlqJobResponse) ProtoReflect() protoreflect.Message { + mi := &file_app_v1_admin_proto_msgTypes[82] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetAdminDlqJobResponse.ProtoReflect.Descriptor instead. +func (*GetAdminDlqJobResponse) Descriptor() ([]byte, []int) { + return file_app_v1_admin_proto_rawDescGZIP(), []int{82} +} + +func (x *GetAdminDlqJobResponse) GetItem() *AdminDlqEntry { + if x != nil { + return x.Item + } + return nil +} + +type RetryAdminDlqJobRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RetryAdminDlqJobRequest) Reset() { + *x = RetryAdminDlqJobRequest{} + mi := &file_app_v1_admin_proto_msgTypes[83] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RetryAdminDlqJobRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RetryAdminDlqJobRequest) ProtoMessage() {} + +func (x *RetryAdminDlqJobRequest) ProtoReflect() protoreflect.Message { + mi := &file_app_v1_admin_proto_msgTypes[83] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RetryAdminDlqJobRequest.ProtoReflect.Descriptor instead. +func (*RetryAdminDlqJobRequest) Descriptor() ([]byte, []int) { + return file_app_v1_admin_proto_rawDescGZIP(), []int{83} +} + +func (x *RetryAdminDlqJobRequest) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +type RetryAdminDlqJobResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Job *AdminJob `protobuf:"bytes,1,opt,name=job,proto3" json:"job,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RetryAdminDlqJobResponse) Reset() { + *x = RetryAdminDlqJobResponse{} + mi := &file_app_v1_admin_proto_msgTypes[84] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RetryAdminDlqJobResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RetryAdminDlqJobResponse) ProtoMessage() {} + +func (x *RetryAdminDlqJobResponse) ProtoReflect() protoreflect.Message { + mi := &file_app_v1_admin_proto_msgTypes[84] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RetryAdminDlqJobResponse.ProtoReflect.Descriptor instead. +func (*RetryAdminDlqJobResponse) Descriptor() ([]byte, []int) { + return file_app_v1_admin_proto_rawDescGZIP(), []int{84} +} + +func (x *RetryAdminDlqJobResponse) GetJob() *AdminJob { + if x != nil { + return x.Job + } + return nil +} + +type RemoveAdminDlqJobRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RemoveAdminDlqJobRequest) Reset() { + *x = RemoveAdminDlqJobRequest{} + mi := &file_app_v1_admin_proto_msgTypes[85] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RemoveAdminDlqJobRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RemoveAdminDlqJobRequest) ProtoMessage() {} + +func (x *RemoveAdminDlqJobRequest) ProtoReflect() protoreflect.Message { + mi := &file_app_v1_admin_proto_msgTypes[85] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RemoveAdminDlqJobRequest.ProtoReflect.Descriptor instead. +func (*RemoveAdminDlqJobRequest) Descriptor() ([]byte, []int) { + return file_app_v1_admin_proto_rawDescGZIP(), []int{85} +} + +func (x *RemoveAdminDlqJobRequest) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +type RemoveAdminDlqJobResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Status string `protobuf:"bytes,1,opt,name=status,proto3" json:"status,omitempty"` + JobId string `protobuf:"bytes,2,opt,name=job_id,json=jobId,proto3" json:"job_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RemoveAdminDlqJobResponse) Reset() { + *x = RemoveAdminDlqJobResponse{} + mi := &file_app_v1_admin_proto_msgTypes[86] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RemoveAdminDlqJobResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RemoveAdminDlqJobResponse) ProtoMessage() {} + +func (x *RemoveAdminDlqJobResponse) ProtoReflect() protoreflect.Message { + mi := &file_app_v1_admin_proto_msgTypes[86] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RemoveAdminDlqJobResponse.ProtoReflect.Descriptor instead. +func (*RemoveAdminDlqJobResponse) Descriptor() ([]byte, []int) { + return file_app_v1_admin_proto_rawDescGZIP(), []int{86} +} + +func (x *RemoveAdminDlqJobResponse) GetStatus() string { + if x != nil { + return x.Status + } + return "" +} + +func (x *RemoveAdminDlqJobResponse) GetJobId() string { + if x != nil { + return x.JobId + } + return "" +} + type ListAdminAgentsRequest struct { state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields @@ -4811,7 +5203,7 @@ type ListAdminAgentsRequest struct { func (x *ListAdminAgentsRequest) Reset() { *x = ListAdminAgentsRequest{} - mi := &file_app_v1_admin_proto_msgTypes[79] + mi := &file_app_v1_admin_proto_msgTypes[87] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4823,7 +5215,7 @@ func (x *ListAdminAgentsRequest) String() string { func (*ListAdminAgentsRequest) ProtoMessage() {} func (x *ListAdminAgentsRequest) ProtoReflect() protoreflect.Message { - mi := &file_app_v1_admin_proto_msgTypes[79] + mi := &file_app_v1_admin_proto_msgTypes[87] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4836,7 +5228,7 @@ func (x *ListAdminAgentsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ListAdminAgentsRequest.ProtoReflect.Descriptor instead. func (*ListAdminAgentsRequest) Descriptor() ([]byte, []int) { - return file_app_v1_admin_proto_rawDescGZIP(), []int{79} + return file_app_v1_admin_proto_rawDescGZIP(), []int{87} } type ListAdminAgentsResponse struct { @@ -4848,7 +5240,7 @@ type ListAdminAgentsResponse struct { func (x *ListAdminAgentsResponse) Reset() { *x = ListAdminAgentsResponse{} - mi := &file_app_v1_admin_proto_msgTypes[80] + mi := &file_app_v1_admin_proto_msgTypes[88] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4860,7 +5252,7 @@ func (x *ListAdminAgentsResponse) String() string { func (*ListAdminAgentsResponse) ProtoMessage() {} func (x *ListAdminAgentsResponse) ProtoReflect() protoreflect.Message { - mi := &file_app_v1_admin_proto_msgTypes[80] + mi := &file_app_v1_admin_proto_msgTypes[88] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4873,7 +5265,7 @@ func (x *ListAdminAgentsResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use ListAdminAgentsResponse.ProtoReflect.Descriptor instead. func (*ListAdminAgentsResponse) Descriptor() ([]byte, []int) { - return file_app_v1_admin_proto_rawDescGZIP(), []int{80} + return file_app_v1_admin_proto_rawDescGZIP(), []int{88} } func (x *ListAdminAgentsResponse) GetAgents() []*AdminAgent { @@ -4892,7 +5284,7 @@ type RestartAdminAgentRequest struct { func (x *RestartAdminAgentRequest) Reset() { *x = RestartAdminAgentRequest{} - mi := &file_app_v1_admin_proto_msgTypes[81] + mi := &file_app_v1_admin_proto_msgTypes[89] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4904,7 +5296,7 @@ func (x *RestartAdminAgentRequest) String() string { func (*RestartAdminAgentRequest) ProtoMessage() {} func (x *RestartAdminAgentRequest) ProtoReflect() protoreflect.Message { - mi := &file_app_v1_admin_proto_msgTypes[81] + mi := &file_app_v1_admin_proto_msgTypes[89] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4917,7 +5309,7 @@ func (x *RestartAdminAgentRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use RestartAdminAgentRequest.ProtoReflect.Descriptor instead. func (*RestartAdminAgentRequest) Descriptor() ([]byte, []int) { - return file_app_v1_admin_proto_rawDescGZIP(), []int{81} + return file_app_v1_admin_proto_rawDescGZIP(), []int{89} } func (x *RestartAdminAgentRequest) GetId() string { @@ -4936,7 +5328,7 @@ type UpdateAdminAgentRequest struct { func (x *UpdateAdminAgentRequest) Reset() { *x = UpdateAdminAgentRequest{} - mi := &file_app_v1_admin_proto_msgTypes[82] + mi := &file_app_v1_admin_proto_msgTypes[90] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4948,7 +5340,7 @@ func (x *UpdateAdminAgentRequest) String() string { func (*UpdateAdminAgentRequest) ProtoMessage() {} func (x *UpdateAdminAgentRequest) ProtoReflect() protoreflect.Message { - mi := &file_app_v1_admin_proto_msgTypes[82] + mi := &file_app_v1_admin_proto_msgTypes[90] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -4961,7 +5353,7 @@ func (x *UpdateAdminAgentRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use UpdateAdminAgentRequest.ProtoReflect.Descriptor instead. func (*UpdateAdminAgentRequest) Descriptor() ([]byte, []int) { - return file_app_v1_admin_proto_rawDescGZIP(), []int{82} + return file_app_v1_admin_proto_rawDescGZIP(), []int{90} } func (x *UpdateAdminAgentRequest) GetId() string { @@ -4980,7 +5372,7 @@ type AdminAgentCommandResponse struct { func (x *AdminAgentCommandResponse) Reset() { *x = AdminAgentCommandResponse{} - mi := &file_app_v1_admin_proto_msgTypes[83] + mi := &file_app_v1_admin_proto_msgTypes[91] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -4992,7 +5384,7 @@ func (x *AdminAgentCommandResponse) String() string { func (*AdminAgentCommandResponse) ProtoMessage() {} func (x *AdminAgentCommandResponse) ProtoReflect() protoreflect.Message { - mi := &file_app_v1_admin_proto_msgTypes[83] + mi := &file_app_v1_admin_proto_msgTypes[91] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -5005,7 +5397,7 @@ func (x *AdminAgentCommandResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use AdminAgentCommandResponse.ProtoReflect.Descriptor instead. func (*AdminAgentCommandResponse) Descriptor() ([]byte, []int) { - return file_app_v1_admin_proto_rawDescGZIP(), []int{83} + return file_app_v1_admin_proto_rawDescGZIP(), []int{91} } func (x *AdminAgentCommandResponse) GetStatus() string { @@ -5448,7 +5840,28 @@ const file_app_v1_admin_proto_rawDesc = "" + "\x14RetryAdminJobRequest\x12\x0e\n" + "\x02id\x18\x01 \x01(\tR\x02id\"B\n" + "\x15RetryAdminJobResponse\x12)\n" + - "\x03job\x18\x01 \x01(\v2\x17.stream.app.v1.AdminJobR\x03job\"\x18\n" + + "\x03job\x18\x01 \x01(\v2\x17.stream.app.v1.AdminJobR\x03job\"G\n" + + "\x17ListAdminDlqJobsRequest\x12\x16\n" + + "\x06offset\x18\x01 \x01(\x05R\x06offset\x12\x14\n" + + "\x05limit\x18\x02 \x01(\x05R\x05limit\"\x92\x01\n" + + "\x18ListAdminDlqJobsResponse\x122\n" + + "\x05items\x18\x01 \x03(\v2\x1c.stream.app.v1.AdminDlqEntryR\x05items\x12\x14\n" + + "\x05total\x18\x02 \x01(\x03R\x05total\x12\x16\n" + + "\x06offset\x18\x03 \x01(\x05R\x06offset\x12\x14\n" + + "\x05limit\x18\x04 \x01(\x05R\x05limit\"'\n" + + "\x15GetAdminDlqJobRequest\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\"J\n" + + "\x16GetAdminDlqJobResponse\x120\n" + + "\x04item\x18\x01 \x01(\v2\x1c.stream.app.v1.AdminDlqEntryR\x04item\")\n" + + "\x17RetryAdminDlqJobRequest\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\"E\n" + + "\x18RetryAdminDlqJobResponse\x12)\n" + + "\x03job\x18\x01 \x01(\v2\x17.stream.app.v1.AdminJobR\x03job\"*\n" + + "\x18RemoveAdminDlqJobRequest\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\"J\n" + + "\x19RemoveAdminDlqJobResponse\x12\x16\n" + + "\x06status\x18\x01 \x01(\tR\x06status\x12\x15\n" + + "\x06job_id\x18\x02 \x01(\tR\x05jobId\"\x18\n" + "\x16ListAdminAgentsRequest\"L\n" + "\x17ListAdminAgentsResponse\x121\n" + "\x06agents\x18\x01 \x03(\v2\x19.stream.app.v1.AdminAgentR\x06agents\"*\n" + @@ -5457,7 +5870,7 @@ const file_app_v1_admin_proto_rawDesc = "" + "\x17UpdateAdminAgentRequest\x12\x0e\n" + "\x02id\x18\x01 \x01(\tR\x02id\"3\n" + "\x19AdminAgentCommandResponse\x12\x16\n" + - "\x06status\x18\x01 \x01(\tR\x06status2\x9d$\n" + + "\x06status\x18\x01 \x01(\tR\x06status2\xae'\n" + "\x05Admin\x12f\n" + "\x11GetAdminDashboard\x12'.stream.app.v1.GetAdminDashboardRequest\x1a(.stream.app.v1.GetAdminDashboardResponse\x12]\n" + "\x0eListAdminUsers\x12$.stream.app.v1.ListAdminUsersRequest\x1a%.stream.app.v1.ListAdminUsersResponse\x12W\n" + @@ -5500,7 +5913,11 @@ const file_app_v1_admin_proto_rawDesc = "" + "\x0fGetAdminJobLogs\x12%.stream.app.v1.GetAdminJobLogsRequest\x1a&.stream.app.v1.GetAdminJobLogsResponse\x12]\n" + "\x0eCreateAdminJob\x12$.stream.app.v1.CreateAdminJobRequest\x1a%.stream.app.v1.CreateAdminJobResponse\x12]\n" + "\x0eCancelAdminJob\x12$.stream.app.v1.CancelAdminJobRequest\x1a%.stream.app.v1.CancelAdminJobResponse\x12Z\n" + - "\rRetryAdminJob\x12#.stream.app.v1.RetryAdminJobRequest\x1a$.stream.app.v1.RetryAdminJobResponse\x12`\n" + + "\rRetryAdminJob\x12#.stream.app.v1.RetryAdminJobRequest\x1a$.stream.app.v1.RetryAdminJobResponse\x12c\n" + + "\x10ListAdminDlqJobs\x12&.stream.app.v1.ListAdminDlqJobsRequest\x1a'.stream.app.v1.ListAdminDlqJobsResponse\x12]\n" + + "\x0eGetAdminDlqJob\x12$.stream.app.v1.GetAdminDlqJobRequest\x1a%.stream.app.v1.GetAdminDlqJobResponse\x12c\n" + + "\x10RetryAdminDlqJob\x12&.stream.app.v1.RetryAdminDlqJobRequest\x1a'.stream.app.v1.RetryAdminDlqJobResponse\x12f\n" + + "\x11RemoveAdminDlqJob\x12'.stream.app.v1.RemoveAdminDlqJobRequest\x1a(.stream.app.v1.RemoveAdminDlqJobResponse\x12`\n" + "\x0fListAdminAgents\x12%.stream.app.v1.ListAdminAgentsRequest\x1a&.stream.app.v1.ListAdminAgentsResponse\x12f\n" + "\x11RestartAdminAgent\x12'.stream.app.v1.RestartAdminAgentRequest\x1a(.stream.app.v1.AdminAgentCommandResponse\x12d\n" + "\x10UpdateAdminAgent\x12&.stream.app.v1.UpdateAdminAgentRequest\x1a(.stream.app.v1.AdminAgentCommandResponseB,Z*stream.api/internal/gen/proto/app/v1;appv1b\x06proto3" @@ -5517,7 +5934,7 @@ func file_app_v1_admin_proto_rawDescGZIP() []byte { return file_app_v1_admin_proto_rawDescData } -var file_app_v1_admin_proto_msgTypes = make([]protoimpl.MessageInfo, 85) +var file_app_v1_admin_proto_msgTypes = make([]protoimpl.MessageInfo, 93) var file_app_v1_admin_proto_goTypes = []any{ (*GetAdminDashboardRequest)(nil), // 0: stream.app.v1.GetAdminDashboardRequest (*GetAdminDashboardResponse)(nil), // 1: stream.app.v1.GetAdminDashboardResponse @@ -5598,158 +6015,178 @@ var file_app_v1_admin_proto_goTypes = []any{ (*CancelAdminJobResponse)(nil), // 76: stream.app.v1.CancelAdminJobResponse (*RetryAdminJobRequest)(nil), // 77: stream.app.v1.RetryAdminJobRequest (*RetryAdminJobResponse)(nil), // 78: stream.app.v1.RetryAdminJobResponse - (*ListAdminAgentsRequest)(nil), // 79: stream.app.v1.ListAdminAgentsRequest - (*ListAdminAgentsResponse)(nil), // 80: stream.app.v1.ListAdminAgentsResponse - (*RestartAdminAgentRequest)(nil), // 81: stream.app.v1.RestartAdminAgentRequest - (*UpdateAdminAgentRequest)(nil), // 82: stream.app.v1.UpdateAdminAgentRequest - (*AdminAgentCommandResponse)(nil), // 83: stream.app.v1.AdminAgentCommandResponse - nil, // 84: stream.app.v1.CreateAdminJobRequest.EnvEntry - (*AdminDashboard)(nil), // 85: stream.app.v1.AdminDashboard - (*AdminUser)(nil), // 86: stream.app.v1.AdminUser - (*AdminUserDetail)(nil), // 87: stream.app.v1.AdminUserDetail - (*AdminVideo)(nil), // 88: stream.app.v1.AdminVideo - (*AdminPayment)(nil), // 89: stream.app.v1.AdminPayment - (*PlanSubscription)(nil), // 90: stream.app.v1.PlanSubscription - (*AdminPlan)(nil), // 91: stream.app.v1.AdminPlan - (*AdminAdTemplate)(nil), // 92: stream.app.v1.AdminAdTemplate - (*AdminPopupAd)(nil), // 93: stream.app.v1.AdminPopupAd - (*AdminPlayerConfig)(nil), // 94: stream.app.v1.AdminPlayerConfig - (*AdminJob)(nil), // 95: stream.app.v1.AdminJob - (*AdminAgent)(nil), // 96: stream.app.v1.AdminAgent - (*MessageResponse)(nil), // 97: stream.app.v1.MessageResponse + (*ListAdminDlqJobsRequest)(nil), // 79: stream.app.v1.ListAdminDlqJobsRequest + (*ListAdminDlqJobsResponse)(nil), // 80: stream.app.v1.ListAdminDlqJobsResponse + (*GetAdminDlqJobRequest)(nil), // 81: stream.app.v1.GetAdminDlqJobRequest + (*GetAdminDlqJobResponse)(nil), // 82: stream.app.v1.GetAdminDlqJobResponse + (*RetryAdminDlqJobRequest)(nil), // 83: stream.app.v1.RetryAdminDlqJobRequest + (*RetryAdminDlqJobResponse)(nil), // 84: stream.app.v1.RetryAdminDlqJobResponse + (*RemoveAdminDlqJobRequest)(nil), // 85: stream.app.v1.RemoveAdminDlqJobRequest + (*RemoveAdminDlqJobResponse)(nil), // 86: stream.app.v1.RemoveAdminDlqJobResponse + (*ListAdminAgentsRequest)(nil), // 87: stream.app.v1.ListAdminAgentsRequest + (*ListAdminAgentsResponse)(nil), // 88: stream.app.v1.ListAdminAgentsResponse + (*RestartAdminAgentRequest)(nil), // 89: stream.app.v1.RestartAdminAgentRequest + (*UpdateAdminAgentRequest)(nil), // 90: stream.app.v1.UpdateAdminAgentRequest + (*AdminAgentCommandResponse)(nil), // 91: stream.app.v1.AdminAgentCommandResponse + nil, // 92: stream.app.v1.CreateAdminJobRequest.EnvEntry + (*AdminDashboard)(nil), // 93: stream.app.v1.AdminDashboard + (*AdminUser)(nil), // 94: stream.app.v1.AdminUser + (*AdminUserDetail)(nil), // 95: stream.app.v1.AdminUserDetail + (*AdminVideo)(nil), // 96: stream.app.v1.AdminVideo + (*AdminPayment)(nil), // 97: stream.app.v1.AdminPayment + (*PlanSubscription)(nil), // 98: stream.app.v1.PlanSubscription + (*AdminPlan)(nil), // 99: stream.app.v1.AdminPlan + (*AdminAdTemplate)(nil), // 100: stream.app.v1.AdminAdTemplate + (*AdminPopupAd)(nil), // 101: stream.app.v1.AdminPopupAd + (*AdminPlayerConfig)(nil), // 102: stream.app.v1.AdminPlayerConfig + (*AdminJob)(nil), // 103: stream.app.v1.AdminJob + (*AdminDlqEntry)(nil), // 104: stream.app.v1.AdminDlqEntry + (*AdminAgent)(nil), // 105: stream.app.v1.AdminAgent + (*MessageResponse)(nil), // 106: stream.app.v1.MessageResponse } var file_app_v1_admin_proto_depIdxs = []int32{ - 85, // 0: stream.app.v1.GetAdminDashboardResponse.dashboard:type_name -> stream.app.v1.AdminDashboard - 86, // 1: stream.app.v1.ListAdminUsersResponse.users:type_name -> stream.app.v1.AdminUser - 87, // 2: stream.app.v1.GetAdminUserResponse.user:type_name -> stream.app.v1.AdminUserDetail - 86, // 3: stream.app.v1.CreateAdminUserResponse.user:type_name -> stream.app.v1.AdminUser - 86, // 4: stream.app.v1.UpdateAdminUserResponse.user:type_name -> stream.app.v1.AdminUser - 87, // 5: stream.app.v1.UpdateAdminUserReferralSettingsResponse.user:type_name -> stream.app.v1.AdminUserDetail - 88, // 6: stream.app.v1.ListAdminVideosResponse.videos:type_name -> stream.app.v1.AdminVideo - 88, // 7: stream.app.v1.GetAdminVideoResponse.video:type_name -> stream.app.v1.AdminVideo - 88, // 8: stream.app.v1.CreateAdminVideoResponse.video:type_name -> stream.app.v1.AdminVideo - 88, // 9: stream.app.v1.UpdateAdminVideoResponse.video:type_name -> stream.app.v1.AdminVideo - 89, // 10: stream.app.v1.ListAdminPaymentsResponse.payments:type_name -> stream.app.v1.AdminPayment - 89, // 11: stream.app.v1.GetAdminPaymentResponse.payment:type_name -> stream.app.v1.AdminPayment - 89, // 12: stream.app.v1.CreateAdminPaymentResponse.payment:type_name -> stream.app.v1.AdminPayment - 90, // 13: stream.app.v1.CreateAdminPaymentResponse.subscription:type_name -> stream.app.v1.PlanSubscription - 89, // 14: stream.app.v1.UpdateAdminPaymentResponse.payment:type_name -> stream.app.v1.AdminPayment - 91, // 15: stream.app.v1.ListAdminPlansResponse.plans:type_name -> stream.app.v1.AdminPlan - 91, // 16: stream.app.v1.CreateAdminPlanResponse.plan:type_name -> stream.app.v1.AdminPlan - 91, // 17: stream.app.v1.UpdateAdminPlanResponse.plan:type_name -> stream.app.v1.AdminPlan - 92, // 18: stream.app.v1.ListAdminAdTemplatesResponse.templates:type_name -> stream.app.v1.AdminAdTemplate - 92, // 19: stream.app.v1.GetAdminAdTemplateResponse.template:type_name -> stream.app.v1.AdminAdTemplate - 92, // 20: stream.app.v1.CreateAdminAdTemplateResponse.template:type_name -> stream.app.v1.AdminAdTemplate - 92, // 21: stream.app.v1.UpdateAdminAdTemplateResponse.template:type_name -> stream.app.v1.AdminAdTemplate - 93, // 22: stream.app.v1.ListAdminPopupAdsResponse.items:type_name -> stream.app.v1.AdminPopupAd - 93, // 23: stream.app.v1.GetAdminPopupAdResponse.item:type_name -> stream.app.v1.AdminPopupAd - 93, // 24: stream.app.v1.CreateAdminPopupAdResponse.item:type_name -> stream.app.v1.AdminPopupAd - 93, // 25: stream.app.v1.UpdateAdminPopupAdResponse.item:type_name -> stream.app.v1.AdminPopupAd - 94, // 26: stream.app.v1.ListAdminPlayerConfigsResponse.configs:type_name -> stream.app.v1.AdminPlayerConfig - 94, // 27: stream.app.v1.GetAdminPlayerConfigResponse.config:type_name -> stream.app.v1.AdminPlayerConfig - 94, // 28: stream.app.v1.CreateAdminPlayerConfigResponse.config:type_name -> stream.app.v1.AdminPlayerConfig - 94, // 29: stream.app.v1.UpdateAdminPlayerConfigResponse.config:type_name -> stream.app.v1.AdminPlayerConfig - 95, // 30: stream.app.v1.ListAdminJobsResponse.jobs:type_name -> stream.app.v1.AdminJob - 95, // 31: stream.app.v1.GetAdminJobResponse.job:type_name -> stream.app.v1.AdminJob - 84, // 32: stream.app.v1.CreateAdminJobRequest.env:type_name -> stream.app.v1.CreateAdminJobRequest.EnvEntry - 95, // 33: stream.app.v1.CreateAdminJobResponse.job:type_name -> stream.app.v1.AdminJob - 95, // 34: stream.app.v1.RetryAdminJobResponse.job:type_name -> stream.app.v1.AdminJob - 96, // 35: stream.app.v1.ListAdminAgentsResponse.agents:type_name -> stream.app.v1.AdminAgent - 0, // 36: stream.app.v1.Admin.GetAdminDashboard:input_type -> stream.app.v1.GetAdminDashboardRequest - 2, // 37: stream.app.v1.Admin.ListAdminUsers:input_type -> stream.app.v1.ListAdminUsersRequest - 4, // 38: stream.app.v1.Admin.GetAdminUser:input_type -> stream.app.v1.GetAdminUserRequest - 6, // 39: stream.app.v1.Admin.CreateAdminUser:input_type -> stream.app.v1.CreateAdminUserRequest - 8, // 40: stream.app.v1.Admin.UpdateAdminUser:input_type -> stream.app.v1.UpdateAdminUserRequest - 10, // 41: stream.app.v1.Admin.UpdateAdminUserReferralSettings:input_type -> stream.app.v1.UpdateAdminUserReferralSettingsRequest - 12, // 42: stream.app.v1.Admin.UpdateAdminUserRole:input_type -> stream.app.v1.UpdateAdminUserRoleRequest - 14, // 43: stream.app.v1.Admin.DeleteAdminUser:input_type -> stream.app.v1.DeleteAdminUserRequest - 15, // 44: stream.app.v1.Admin.ListAdminVideos:input_type -> stream.app.v1.ListAdminVideosRequest - 17, // 45: stream.app.v1.Admin.GetAdminVideo:input_type -> stream.app.v1.GetAdminVideoRequest - 19, // 46: stream.app.v1.Admin.CreateAdminVideo:input_type -> stream.app.v1.CreateAdminVideoRequest - 21, // 47: stream.app.v1.Admin.UpdateAdminVideo:input_type -> stream.app.v1.UpdateAdminVideoRequest - 23, // 48: stream.app.v1.Admin.DeleteAdminVideo:input_type -> stream.app.v1.DeleteAdminVideoRequest - 24, // 49: stream.app.v1.Admin.ListAdminPayments:input_type -> stream.app.v1.ListAdminPaymentsRequest - 26, // 50: stream.app.v1.Admin.GetAdminPayment:input_type -> stream.app.v1.GetAdminPaymentRequest - 28, // 51: stream.app.v1.Admin.CreateAdminPayment:input_type -> stream.app.v1.CreateAdminPaymentRequest - 30, // 52: stream.app.v1.Admin.UpdateAdminPayment:input_type -> stream.app.v1.UpdateAdminPaymentRequest - 32, // 53: stream.app.v1.Admin.ListAdminPlans:input_type -> stream.app.v1.ListAdminPlansRequest - 34, // 54: stream.app.v1.Admin.CreateAdminPlan:input_type -> stream.app.v1.CreateAdminPlanRequest - 36, // 55: stream.app.v1.Admin.UpdateAdminPlan:input_type -> stream.app.v1.UpdateAdminPlanRequest - 38, // 56: stream.app.v1.Admin.DeleteAdminPlan:input_type -> stream.app.v1.DeleteAdminPlanRequest - 40, // 57: stream.app.v1.Admin.ListAdminAdTemplates:input_type -> stream.app.v1.ListAdminAdTemplatesRequest - 42, // 58: stream.app.v1.Admin.GetAdminAdTemplate:input_type -> stream.app.v1.GetAdminAdTemplateRequest - 44, // 59: stream.app.v1.Admin.CreateAdminAdTemplate:input_type -> stream.app.v1.CreateAdminAdTemplateRequest - 46, // 60: stream.app.v1.Admin.UpdateAdminAdTemplate:input_type -> stream.app.v1.UpdateAdminAdTemplateRequest - 48, // 61: stream.app.v1.Admin.DeleteAdminAdTemplate:input_type -> stream.app.v1.DeleteAdminAdTemplateRequest - 49, // 62: stream.app.v1.Admin.ListAdminPopupAds:input_type -> stream.app.v1.ListAdminPopupAdsRequest - 51, // 63: stream.app.v1.Admin.GetAdminPopupAd:input_type -> stream.app.v1.GetAdminPopupAdRequest - 53, // 64: stream.app.v1.Admin.CreateAdminPopupAd:input_type -> stream.app.v1.CreateAdminPopupAdRequest - 55, // 65: stream.app.v1.Admin.UpdateAdminPopupAd:input_type -> stream.app.v1.UpdateAdminPopupAdRequest - 57, // 66: stream.app.v1.Admin.DeleteAdminPopupAd:input_type -> stream.app.v1.DeleteAdminPopupAdRequest - 58, // 67: stream.app.v1.Admin.ListAdminPlayerConfigs:input_type -> stream.app.v1.ListAdminPlayerConfigsRequest - 60, // 68: stream.app.v1.Admin.GetAdminPlayerConfig:input_type -> stream.app.v1.GetAdminPlayerConfigRequest - 62, // 69: stream.app.v1.Admin.CreateAdminPlayerConfig:input_type -> stream.app.v1.CreateAdminPlayerConfigRequest - 64, // 70: stream.app.v1.Admin.UpdateAdminPlayerConfig:input_type -> stream.app.v1.UpdateAdminPlayerConfigRequest - 66, // 71: stream.app.v1.Admin.DeleteAdminPlayerConfig:input_type -> stream.app.v1.DeleteAdminPlayerConfigRequest - 67, // 72: stream.app.v1.Admin.ListAdminJobs:input_type -> stream.app.v1.ListAdminJobsRequest - 69, // 73: stream.app.v1.Admin.GetAdminJob:input_type -> stream.app.v1.GetAdminJobRequest - 71, // 74: stream.app.v1.Admin.GetAdminJobLogs:input_type -> stream.app.v1.GetAdminJobLogsRequest - 73, // 75: stream.app.v1.Admin.CreateAdminJob:input_type -> stream.app.v1.CreateAdminJobRequest - 75, // 76: stream.app.v1.Admin.CancelAdminJob:input_type -> stream.app.v1.CancelAdminJobRequest - 77, // 77: stream.app.v1.Admin.RetryAdminJob:input_type -> stream.app.v1.RetryAdminJobRequest - 79, // 78: stream.app.v1.Admin.ListAdminAgents:input_type -> stream.app.v1.ListAdminAgentsRequest - 81, // 79: stream.app.v1.Admin.RestartAdminAgent:input_type -> stream.app.v1.RestartAdminAgentRequest - 82, // 80: stream.app.v1.Admin.UpdateAdminAgent:input_type -> stream.app.v1.UpdateAdminAgentRequest - 1, // 81: stream.app.v1.Admin.GetAdminDashboard:output_type -> stream.app.v1.GetAdminDashboardResponse - 3, // 82: stream.app.v1.Admin.ListAdminUsers:output_type -> stream.app.v1.ListAdminUsersResponse - 5, // 83: stream.app.v1.Admin.GetAdminUser:output_type -> stream.app.v1.GetAdminUserResponse - 7, // 84: stream.app.v1.Admin.CreateAdminUser:output_type -> stream.app.v1.CreateAdminUserResponse - 9, // 85: stream.app.v1.Admin.UpdateAdminUser:output_type -> stream.app.v1.UpdateAdminUserResponse - 11, // 86: stream.app.v1.Admin.UpdateAdminUserReferralSettings:output_type -> stream.app.v1.UpdateAdminUserReferralSettingsResponse - 13, // 87: stream.app.v1.Admin.UpdateAdminUserRole:output_type -> stream.app.v1.UpdateAdminUserRoleResponse - 97, // 88: stream.app.v1.Admin.DeleteAdminUser:output_type -> stream.app.v1.MessageResponse - 16, // 89: stream.app.v1.Admin.ListAdminVideos:output_type -> stream.app.v1.ListAdminVideosResponse - 18, // 90: stream.app.v1.Admin.GetAdminVideo:output_type -> stream.app.v1.GetAdminVideoResponse - 20, // 91: stream.app.v1.Admin.CreateAdminVideo:output_type -> stream.app.v1.CreateAdminVideoResponse - 22, // 92: stream.app.v1.Admin.UpdateAdminVideo:output_type -> stream.app.v1.UpdateAdminVideoResponse - 97, // 93: stream.app.v1.Admin.DeleteAdminVideo:output_type -> stream.app.v1.MessageResponse - 25, // 94: stream.app.v1.Admin.ListAdminPayments:output_type -> stream.app.v1.ListAdminPaymentsResponse - 27, // 95: stream.app.v1.Admin.GetAdminPayment:output_type -> stream.app.v1.GetAdminPaymentResponse - 29, // 96: stream.app.v1.Admin.CreateAdminPayment:output_type -> stream.app.v1.CreateAdminPaymentResponse - 31, // 97: stream.app.v1.Admin.UpdateAdminPayment:output_type -> stream.app.v1.UpdateAdminPaymentResponse - 33, // 98: stream.app.v1.Admin.ListAdminPlans:output_type -> stream.app.v1.ListAdminPlansResponse - 35, // 99: stream.app.v1.Admin.CreateAdminPlan:output_type -> stream.app.v1.CreateAdminPlanResponse - 37, // 100: stream.app.v1.Admin.UpdateAdminPlan:output_type -> stream.app.v1.UpdateAdminPlanResponse - 39, // 101: stream.app.v1.Admin.DeleteAdminPlan:output_type -> stream.app.v1.DeleteAdminPlanResponse - 41, // 102: stream.app.v1.Admin.ListAdminAdTemplates:output_type -> stream.app.v1.ListAdminAdTemplatesResponse - 43, // 103: stream.app.v1.Admin.GetAdminAdTemplate:output_type -> stream.app.v1.GetAdminAdTemplateResponse - 45, // 104: stream.app.v1.Admin.CreateAdminAdTemplate:output_type -> stream.app.v1.CreateAdminAdTemplateResponse - 47, // 105: stream.app.v1.Admin.UpdateAdminAdTemplate:output_type -> stream.app.v1.UpdateAdminAdTemplateResponse - 97, // 106: stream.app.v1.Admin.DeleteAdminAdTemplate:output_type -> stream.app.v1.MessageResponse - 50, // 107: stream.app.v1.Admin.ListAdminPopupAds:output_type -> stream.app.v1.ListAdminPopupAdsResponse - 52, // 108: stream.app.v1.Admin.GetAdminPopupAd:output_type -> stream.app.v1.GetAdminPopupAdResponse - 54, // 109: stream.app.v1.Admin.CreateAdminPopupAd:output_type -> stream.app.v1.CreateAdminPopupAdResponse - 56, // 110: stream.app.v1.Admin.UpdateAdminPopupAd:output_type -> stream.app.v1.UpdateAdminPopupAdResponse - 97, // 111: stream.app.v1.Admin.DeleteAdminPopupAd:output_type -> stream.app.v1.MessageResponse - 59, // 112: stream.app.v1.Admin.ListAdminPlayerConfigs:output_type -> stream.app.v1.ListAdminPlayerConfigsResponse - 61, // 113: stream.app.v1.Admin.GetAdminPlayerConfig:output_type -> stream.app.v1.GetAdminPlayerConfigResponse - 63, // 114: stream.app.v1.Admin.CreateAdminPlayerConfig:output_type -> stream.app.v1.CreateAdminPlayerConfigResponse - 65, // 115: stream.app.v1.Admin.UpdateAdminPlayerConfig:output_type -> stream.app.v1.UpdateAdminPlayerConfigResponse - 97, // 116: stream.app.v1.Admin.DeleteAdminPlayerConfig:output_type -> stream.app.v1.MessageResponse - 68, // 117: stream.app.v1.Admin.ListAdminJobs:output_type -> stream.app.v1.ListAdminJobsResponse - 70, // 118: stream.app.v1.Admin.GetAdminJob:output_type -> stream.app.v1.GetAdminJobResponse - 72, // 119: stream.app.v1.Admin.GetAdminJobLogs:output_type -> stream.app.v1.GetAdminJobLogsResponse - 74, // 120: stream.app.v1.Admin.CreateAdminJob:output_type -> stream.app.v1.CreateAdminJobResponse - 76, // 121: stream.app.v1.Admin.CancelAdminJob:output_type -> stream.app.v1.CancelAdminJobResponse - 78, // 122: stream.app.v1.Admin.RetryAdminJob:output_type -> stream.app.v1.RetryAdminJobResponse - 80, // 123: stream.app.v1.Admin.ListAdminAgents:output_type -> stream.app.v1.ListAdminAgentsResponse - 83, // 124: stream.app.v1.Admin.RestartAdminAgent:output_type -> stream.app.v1.AdminAgentCommandResponse - 83, // 125: stream.app.v1.Admin.UpdateAdminAgent:output_type -> stream.app.v1.AdminAgentCommandResponse - 81, // [81:126] is the sub-list for method output_type - 36, // [36:81] is the sub-list for method input_type - 36, // [36:36] is the sub-list for extension type_name - 36, // [36:36] is the sub-list for extension extendee - 0, // [0:36] is the sub-list for field type_name + 93, // 0: stream.app.v1.GetAdminDashboardResponse.dashboard:type_name -> stream.app.v1.AdminDashboard + 94, // 1: stream.app.v1.ListAdminUsersResponse.users:type_name -> stream.app.v1.AdminUser + 95, // 2: stream.app.v1.GetAdminUserResponse.user:type_name -> stream.app.v1.AdminUserDetail + 94, // 3: stream.app.v1.CreateAdminUserResponse.user:type_name -> stream.app.v1.AdminUser + 94, // 4: stream.app.v1.UpdateAdminUserResponse.user:type_name -> stream.app.v1.AdminUser + 95, // 5: stream.app.v1.UpdateAdminUserReferralSettingsResponse.user:type_name -> stream.app.v1.AdminUserDetail + 96, // 6: stream.app.v1.ListAdminVideosResponse.videos:type_name -> stream.app.v1.AdminVideo + 96, // 7: stream.app.v1.GetAdminVideoResponse.video:type_name -> stream.app.v1.AdminVideo + 96, // 8: stream.app.v1.CreateAdminVideoResponse.video:type_name -> stream.app.v1.AdminVideo + 96, // 9: stream.app.v1.UpdateAdminVideoResponse.video:type_name -> stream.app.v1.AdminVideo + 97, // 10: stream.app.v1.ListAdminPaymentsResponse.payments:type_name -> stream.app.v1.AdminPayment + 97, // 11: stream.app.v1.GetAdminPaymentResponse.payment:type_name -> stream.app.v1.AdminPayment + 97, // 12: stream.app.v1.CreateAdminPaymentResponse.payment:type_name -> stream.app.v1.AdminPayment + 98, // 13: stream.app.v1.CreateAdminPaymentResponse.subscription:type_name -> stream.app.v1.PlanSubscription + 97, // 14: stream.app.v1.UpdateAdminPaymentResponse.payment:type_name -> stream.app.v1.AdminPayment + 99, // 15: stream.app.v1.ListAdminPlansResponse.plans:type_name -> stream.app.v1.AdminPlan + 99, // 16: stream.app.v1.CreateAdminPlanResponse.plan:type_name -> stream.app.v1.AdminPlan + 99, // 17: stream.app.v1.UpdateAdminPlanResponse.plan:type_name -> stream.app.v1.AdminPlan + 100, // 18: stream.app.v1.ListAdminAdTemplatesResponse.templates:type_name -> stream.app.v1.AdminAdTemplate + 100, // 19: stream.app.v1.GetAdminAdTemplateResponse.template:type_name -> stream.app.v1.AdminAdTemplate + 100, // 20: stream.app.v1.CreateAdminAdTemplateResponse.template:type_name -> stream.app.v1.AdminAdTemplate + 100, // 21: stream.app.v1.UpdateAdminAdTemplateResponse.template:type_name -> stream.app.v1.AdminAdTemplate + 101, // 22: stream.app.v1.ListAdminPopupAdsResponse.items:type_name -> stream.app.v1.AdminPopupAd + 101, // 23: stream.app.v1.GetAdminPopupAdResponse.item:type_name -> stream.app.v1.AdminPopupAd + 101, // 24: stream.app.v1.CreateAdminPopupAdResponse.item:type_name -> stream.app.v1.AdminPopupAd + 101, // 25: stream.app.v1.UpdateAdminPopupAdResponse.item:type_name -> stream.app.v1.AdminPopupAd + 102, // 26: stream.app.v1.ListAdminPlayerConfigsResponse.configs:type_name -> stream.app.v1.AdminPlayerConfig + 102, // 27: stream.app.v1.GetAdminPlayerConfigResponse.config:type_name -> stream.app.v1.AdminPlayerConfig + 102, // 28: stream.app.v1.CreateAdminPlayerConfigResponse.config:type_name -> stream.app.v1.AdminPlayerConfig + 102, // 29: stream.app.v1.UpdateAdminPlayerConfigResponse.config:type_name -> stream.app.v1.AdminPlayerConfig + 103, // 30: stream.app.v1.ListAdminJobsResponse.jobs:type_name -> stream.app.v1.AdminJob + 103, // 31: stream.app.v1.GetAdminJobResponse.job:type_name -> stream.app.v1.AdminJob + 92, // 32: stream.app.v1.CreateAdminJobRequest.env:type_name -> stream.app.v1.CreateAdminJobRequest.EnvEntry + 103, // 33: stream.app.v1.CreateAdminJobResponse.job:type_name -> stream.app.v1.AdminJob + 103, // 34: stream.app.v1.RetryAdminJobResponse.job:type_name -> stream.app.v1.AdminJob + 104, // 35: stream.app.v1.ListAdminDlqJobsResponse.items:type_name -> stream.app.v1.AdminDlqEntry + 104, // 36: stream.app.v1.GetAdminDlqJobResponse.item:type_name -> stream.app.v1.AdminDlqEntry + 103, // 37: stream.app.v1.RetryAdminDlqJobResponse.job:type_name -> stream.app.v1.AdminJob + 105, // 38: stream.app.v1.ListAdminAgentsResponse.agents:type_name -> stream.app.v1.AdminAgent + 0, // 39: stream.app.v1.Admin.GetAdminDashboard:input_type -> stream.app.v1.GetAdminDashboardRequest + 2, // 40: stream.app.v1.Admin.ListAdminUsers:input_type -> stream.app.v1.ListAdminUsersRequest + 4, // 41: stream.app.v1.Admin.GetAdminUser:input_type -> stream.app.v1.GetAdminUserRequest + 6, // 42: stream.app.v1.Admin.CreateAdminUser:input_type -> stream.app.v1.CreateAdminUserRequest + 8, // 43: stream.app.v1.Admin.UpdateAdminUser:input_type -> stream.app.v1.UpdateAdminUserRequest + 10, // 44: stream.app.v1.Admin.UpdateAdminUserReferralSettings:input_type -> stream.app.v1.UpdateAdminUserReferralSettingsRequest + 12, // 45: stream.app.v1.Admin.UpdateAdminUserRole:input_type -> stream.app.v1.UpdateAdminUserRoleRequest + 14, // 46: stream.app.v1.Admin.DeleteAdminUser:input_type -> stream.app.v1.DeleteAdminUserRequest + 15, // 47: stream.app.v1.Admin.ListAdminVideos:input_type -> stream.app.v1.ListAdminVideosRequest + 17, // 48: stream.app.v1.Admin.GetAdminVideo:input_type -> stream.app.v1.GetAdminVideoRequest + 19, // 49: stream.app.v1.Admin.CreateAdminVideo:input_type -> stream.app.v1.CreateAdminVideoRequest + 21, // 50: stream.app.v1.Admin.UpdateAdminVideo:input_type -> stream.app.v1.UpdateAdminVideoRequest + 23, // 51: stream.app.v1.Admin.DeleteAdminVideo:input_type -> stream.app.v1.DeleteAdminVideoRequest + 24, // 52: stream.app.v1.Admin.ListAdminPayments:input_type -> stream.app.v1.ListAdminPaymentsRequest + 26, // 53: stream.app.v1.Admin.GetAdminPayment:input_type -> stream.app.v1.GetAdminPaymentRequest + 28, // 54: stream.app.v1.Admin.CreateAdminPayment:input_type -> stream.app.v1.CreateAdminPaymentRequest + 30, // 55: stream.app.v1.Admin.UpdateAdminPayment:input_type -> stream.app.v1.UpdateAdminPaymentRequest + 32, // 56: stream.app.v1.Admin.ListAdminPlans:input_type -> stream.app.v1.ListAdminPlansRequest + 34, // 57: stream.app.v1.Admin.CreateAdminPlan:input_type -> stream.app.v1.CreateAdminPlanRequest + 36, // 58: stream.app.v1.Admin.UpdateAdminPlan:input_type -> stream.app.v1.UpdateAdminPlanRequest + 38, // 59: stream.app.v1.Admin.DeleteAdminPlan:input_type -> stream.app.v1.DeleteAdminPlanRequest + 40, // 60: stream.app.v1.Admin.ListAdminAdTemplates:input_type -> stream.app.v1.ListAdminAdTemplatesRequest + 42, // 61: stream.app.v1.Admin.GetAdminAdTemplate:input_type -> stream.app.v1.GetAdminAdTemplateRequest + 44, // 62: stream.app.v1.Admin.CreateAdminAdTemplate:input_type -> stream.app.v1.CreateAdminAdTemplateRequest + 46, // 63: stream.app.v1.Admin.UpdateAdminAdTemplate:input_type -> stream.app.v1.UpdateAdminAdTemplateRequest + 48, // 64: stream.app.v1.Admin.DeleteAdminAdTemplate:input_type -> stream.app.v1.DeleteAdminAdTemplateRequest + 49, // 65: stream.app.v1.Admin.ListAdminPopupAds:input_type -> stream.app.v1.ListAdminPopupAdsRequest + 51, // 66: stream.app.v1.Admin.GetAdminPopupAd:input_type -> stream.app.v1.GetAdminPopupAdRequest + 53, // 67: stream.app.v1.Admin.CreateAdminPopupAd:input_type -> stream.app.v1.CreateAdminPopupAdRequest + 55, // 68: stream.app.v1.Admin.UpdateAdminPopupAd:input_type -> stream.app.v1.UpdateAdminPopupAdRequest + 57, // 69: stream.app.v1.Admin.DeleteAdminPopupAd:input_type -> stream.app.v1.DeleteAdminPopupAdRequest + 58, // 70: stream.app.v1.Admin.ListAdminPlayerConfigs:input_type -> stream.app.v1.ListAdminPlayerConfigsRequest + 60, // 71: stream.app.v1.Admin.GetAdminPlayerConfig:input_type -> stream.app.v1.GetAdminPlayerConfigRequest + 62, // 72: stream.app.v1.Admin.CreateAdminPlayerConfig:input_type -> stream.app.v1.CreateAdminPlayerConfigRequest + 64, // 73: stream.app.v1.Admin.UpdateAdminPlayerConfig:input_type -> stream.app.v1.UpdateAdminPlayerConfigRequest + 66, // 74: stream.app.v1.Admin.DeleteAdminPlayerConfig:input_type -> stream.app.v1.DeleteAdminPlayerConfigRequest + 67, // 75: stream.app.v1.Admin.ListAdminJobs:input_type -> stream.app.v1.ListAdminJobsRequest + 69, // 76: stream.app.v1.Admin.GetAdminJob:input_type -> stream.app.v1.GetAdminJobRequest + 71, // 77: stream.app.v1.Admin.GetAdminJobLogs:input_type -> stream.app.v1.GetAdminJobLogsRequest + 73, // 78: stream.app.v1.Admin.CreateAdminJob:input_type -> stream.app.v1.CreateAdminJobRequest + 75, // 79: stream.app.v1.Admin.CancelAdminJob:input_type -> stream.app.v1.CancelAdminJobRequest + 77, // 80: stream.app.v1.Admin.RetryAdminJob:input_type -> stream.app.v1.RetryAdminJobRequest + 79, // 81: stream.app.v1.Admin.ListAdminDlqJobs:input_type -> stream.app.v1.ListAdminDlqJobsRequest + 81, // 82: stream.app.v1.Admin.GetAdminDlqJob:input_type -> stream.app.v1.GetAdminDlqJobRequest + 83, // 83: stream.app.v1.Admin.RetryAdminDlqJob:input_type -> stream.app.v1.RetryAdminDlqJobRequest + 85, // 84: stream.app.v1.Admin.RemoveAdminDlqJob:input_type -> stream.app.v1.RemoveAdminDlqJobRequest + 87, // 85: stream.app.v1.Admin.ListAdminAgents:input_type -> stream.app.v1.ListAdminAgentsRequest + 89, // 86: stream.app.v1.Admin.RestartAdminAgent:input_type -> stream.app.v1.RestartAdminAgentRequest + 90, // 87: stream.app.v1.Admin.UpdateAdminAgent:input_type -> stream.app.v1.UpdateAdminAgentRequest + 1, // 88: stream.app.v1.Admin.GetAdminDashboard:output_type -> stream.app.v1.GetAdminDashboardResponse + 3, // 89: stream.app.v1.Admin.ListAdminUsers:output_type -> stream.app.v1.ListAdminUsersResponse + 5, // 90: stream.app.v1.Admin.GetAdminUser:output_type -> stream.app.v1.GetAdminUserResponse + 7, // 91: stream.app.v1.Admin.CreateAdminUser:output_type -> stream.app.v1.CreateAdminUserResponse + 9, // 92: stream.app.v1.Admin.UpdateAdminUser:output_type -> stream.app.v1.UpdateAdminUserResponse + 11, // 93: stream.app.v1.Admin.UpdateAdminUserReferralSettings:output_type -> stream.app.v1.UpdateAdminUserReferralSettingsResponse + 13, // 94: stream.app.v1.Admin.UpdateAdminUserRole:output_type -> stream.app.v1.UpdateAdminUserRoleResponse + 106, // 95: stream.app.v1.Admin.DeleteAdminUser:output_type -> stream.app.v1.MessageResponse + 16, // 96: stream.app.v1.Admin.ListAdminVideos:output_type -> stream.app.v1.ListAdminVideosResponse + 18, // 97: stream.app.v1.Admin.GetAdminVideo:output_type -> stream.app.v1.GetAdminVideoResponse + 20, // 98: stream.app.v1.Admin.CreateAdminVideo:output_type -> stream.app.v1.CreateAdminVideoResponse + 22, // 99: stream.app.v1.Admin.UpdateAdminVideo:output_type -> stream.app.v1.UpdateAdminVideoResponse + 106, // 100: stream.app.v1.Admin.DeleteAdminVideo:output_type -> stream.app.v1.MessageResponse + 25, // 101: stream.app.v1.Admin.ListAdminPayments:output_type -> stream.app.v1.ListAdminPaymentsResponse + 27, // 102: stream.app.v1.Admin.GetAdminPayment:output_type -> stream.app.v1.GetAdminPaymentResponse + 29, // 103: stream.app.v1.Admin.CreateAdminPayment:output_type -> stream.app.v1.CreateAdminPaymentResponse + 31, // 104: stream.app.v1.Admin.UpdateAdminPayment:output_type -> stream.app.v1.UpdateAdminPaymentResponse + 33, // 105: stream.app.v1.Admin.ListAdminPlans:output_type -> stream.app.v1.ListAdminPlansResponse + 35, // 106: stream.app.v1.Admin.CreateAdminPlan:output_type -> stream.app.v1.CreateAdminPlanResponse + 37, // 107: stream.app.v1.Admin.UpdateAdminPlan:output_type -> stream.app.v1.UpdateAdminPlanResponse + 39, // 108: stream.app.v1.Admin.DeleteAdminPlan:output_type -> stream.app.v1.DeleteAdminPlanResponse + 41, // 109: stream.app.v1.Admin.ListAdminAdTemplates:output_type -> stream.app.v1.ListAdminAdTemplatesResponse + 43, // 110: stream.app.v1.Admin.GetAdminAdTemplate:output_type -> stream.app.v1.GetAdminAdTemplateResponse + 45, // 111: stream.app.v1.Admin.CreateAdminAdTemplate:output_type -> stream.app.v1.CreateAdminAdTemplateResponse + 47, // 112: stream.app.v1.Admin.UpdateAdminAdTemplate:output_type -> stream.app.v1.UpdateAdminAdTemplateResponse + 106, // 113: stream.app.v1.Admin.DeleteAdminAdTemplate:output_type -> stream.app.v1.MessageResponse + 50, // 114: stream.app.v1.Admin.ListAdminPopupAds:output_type -> stream.app.v1.ListAdminPopupAdsResponse + 52, // 115: stream.app.v1.Admin.GetAdminPopupAd:output_type -> stream.app.v1.GetAdminPopupAdResponse + 54, // 116: stream.app.v1.Admin.CreateAdminPopupAd:output_type -> stream.app.v1.CreateAdminPopupAdResponse + 56, // 117: stream.app.v1.Admin.UpdateAdminPopupAd:output_type -> stream.app.v1.UpdateAdminPopupAdResponse + 106, // 118: stream.app.v1.Admin.DeleteAdminPopupAd:output_type -> stream.app.v1.MessageResponse + 59, // 119: stream.app.v1.Admin.ListAdminPlayerConfigs:output_type -> stream.app.v1.ListAdminPlayerConfigsResponse + 61, // 120: stream.app.v1.Admin.GetAdminPlayerConfig:output_type -> stream.app.v1.GetAdminPlayerConfigResponse + 63, // 121: stream.app.v1.Admin.CreateAdminPlayerConfig:output_type -> stream.app.v1.CreateAdminPlayerConfigResponse + 65, // 122: stream.app.v1.Admin.UpdateAdminPlayerConfig:output_type -> stream.app.v1.UpdateAdminPlayerConfigResponse + 106, // 123: stream.app.v1.Admin.DeleteAdminPlayerConfig:output_type -> stream.app.v1.MessageResponse + 68, // 124: stream.app.v1.Admin.ListAdminJobs:output_type -> stream.app.v1.ListAdminJobsResponse + 70, // 125: stream.app.v1.Admin.GetAdminJob:output_type -> stream.app.v1.GetAdminJobResponse + 72, // 126: stream.app.v1.Admin.GetAdminJobLogs:output_type -> stream.app.v1.GetAdminJobLogsResponse + 74, // 127: stream.app.v1.Admin.CreateAdminJob:output_type -> stream.app.v1.CreateAdminJobResponse + 76, // 128: stream.app.v1.Admin.CancelAdminJob:output_type -> stream.app.v1.CancelAdminJobResponse + 78, // 129: stream.app.v1.Admin.RetryAdminJob:output_type -> stream.app.v1.RetryAdminJobResponse + 80, // 130: stream.app.v1.Admin.ListAdminDlqJobs:output_type -> stream.app.v1.ListAdminDlqJobsResponse + 82, // 131: stream.app.v1.Admin.GetAdminDlqJob:output_type -> stream.app.v1.GetAdminDlqJobResponse + 84, // 132: stream.app.v1.Admin.RetryAdminDlqJob:output_type -> stream.app.v1.RetryAdminDlqJobResponse + 86, // 133: stream.app.v1.Admin.RemoveAdminDlqJob:output_type -> stream.app.v1.RemoveAdminDlqJobResponse + 88, // 134: stream.app.v1.Admin.ListAdminAgents:output_type -> stream.app.v1.ListAdminAgentsResponse + 91, // 135: stream.app.v1.Admin.RestartAdminAgent:output_type -> stream.app.v1.AdminAgentCommandResponse + 91, // 136: stream.app.v1.Admin.UpdateAdminAgent:output_type -> stream.app.v1.AdminAgentCommandResponse + 88, // [88:137] is the sub-list for method output_type + 39, // [39:88] is the sub-list for method input_type + 39, // [39:39] is the sub-list for extension type_name + 39, // [39:39] is the sub-list for extension extendee + 0, // [0:39] is the sub-list for field type_name } func init() { file_app_v1_admin_proto_init() } @@ -5787,7 +6224,7 @@ func file_app_v1_admin_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_app_v1_admin_proto_rawDesc), len(file_app_v1_admin_proto_rawDesc)), NumEnums: 0, - NumMessages: 85, + NumMessages: 93, NumExtensions: 0, NumServices: 1, }, diff --git a/internal/api/proto/app/v1/admin_grpc.pb.go b/internal/api/proto/app/v1/admin_grpc.pb.go index 0772c17..67f7c89 100644 --- a/internal/api/proto/app/v1/admin_grpc.pb.go +++ b/internal/api/proto/app/v1/admin_grpc.pb.go @@ -61,6 +61,10 @@ const ( Admin_CreateAdminJob_FullMethodName = "/stream.app.v1.Admin/CreateAdminJob" Admin_CancelAdminJob_FullMethodName = "/stream.app.v1.Admin/CancelAdminJob" Admin_RetryAdminJob_FullMethodName = "/stream.app.v1.Admin/RetryAdminJob" + Admin_ListAdminDlqJobs_FullMethodName = "/stream.app.v1.Admin/ListAdminDlqJobs" + Admin_GetAdminDlqJob_FullMethodName = "/stream.app.v1.Admin/GetAdminDlqJob" + Admin_RetryAdminDlqJob_FullMethodName = "/stream.app.v1.Admin/RetryAdminDlqJob" + Admin_RemoveAdminDlqJob_FullMethodName = "/stream.app.v1.Admin/RemoveAdminDlqJob" Admin_ListAdminAgents_FullMethodName = "/stream.app.v1.Admin/ListAdminAgents" Admin_RestartAdminAgent_FullMethodName = "/stream.app.v1.Admin/RestartAdminAgent" Admin_UpdateAdminAgent_FullMethodName = "/stream.app.v1.Admin/UpdateAdminAgent" @@ -112,6 +116,10 @@ type AdminClient interface { CreateAdminJob(ctx context.Context, in *CreateAdminJobRequest, opts ...grpc.CallOption) (*CreateAdminJobResponse, error) CancelAdminJob(ctx context.Context, in *CancelAdminJobRequest, opts ...grpc.CallOption) (*CancelAdminJobResponse, error) RetryAdminJob(ctx context.Context, in *RetryAdminJobRequest, opts ...grpc.CallOption) (*RetryAdminJobResponse, error) + ListAdminDlqJobs(ctx context.Context, in *ListAdminDlqJobsRequest, opts ...grpc.CallOption) (*ListAdminDlqJobsResponse, error) + GetAdminDlqJob(ctx context.Context, in *GetAdminDlqJobRequest, opts ...grpc.CallOption) (*GetAdminDlqJobResponse, error) + RetryAdminDlqJob(ctx context.Context, in *RetryAdminDlqJobRequest, opts ...grpc.CallOption) (*RetryAdminDlqJobResponse, error) + RemoveAdminDlqJob(ctx context.Context, in *RemoveAdminDlqJobRequest, opts ...grpc.CallOption) (*RemoveAdminDlqJobResponse, error) ListAdminAgents(ctx context.Context, in *ListAdminAgentsRequest, opts ...grpc.CallOption) (*ListAdminAgentsResponse, error) RestartAdminAgent(ctx context.Context, in *RestartAdminAgentRequest, opts ...grpc.CallOption) (*AdminAgentCommandResponse, error) UpdateAdminAgent(ctx context.Context, in *UpdateAdminAgentRequest, opts ...grpc.CallOption) (*AdminAgentCommandResponse, error) @@ -545,6 +553,46 @@ func (c *adminClient) RetryAdminJob(ctx context.Context, in *RetryAdminJobReques return out, nil } +func (c *adminClient) ListAdminDlqJobs(ctx context.Context, in *ListAdminDlqJobsRequest, opts ...grpc.CallOption) (*ListAdminDlqJobsResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListAdminDlqJobsResponse) + err := c.cc.Invoke(ctx, Admin_ListAdminDlqJobs_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *adminClient) GetAdminDlqJob(ctx context.Context, in *GetAdminDlqJobRequest, opts ...grpc.CallOption) (*GetAdminDlqJobResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetAdminDlqJobResponse) + err := c.cc.Invoke(ctx, Admin_GetAdminDlqJob_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *adminClient) RetryAdminDlqJob(ctx context.Context, in *RetryAdminDlqJobRequest, opts ...grpc.CallOption) (*RetryAdminDlqJobResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(RetryAdminDlqJobResponse) + err := c.cc.Invoke(ctx, Admin_RetryAdminDlqJob_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *adminClient) RemoveAdminDlqJob(ctx context.Context, in *RemoveAdminDlqJobRequest, opts ...grpc.CallOption) (*RemoveAdminDlqJobResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(RemoveAdminDlqJobResponse) + err := c.cc.Invoke(ctx, Admin_RemoveAdminDlqJob_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *adminClient) ListAdminAgents(ctx context.Context, in *ListAdminAgentsRequest, opts ...grpc.CallOption) (*ListAdminAgentsResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(ListAdminAgentsResponse) @@ -621,6 +669,10 @@ type AdminServer interface { CreateAdminJob(context.Context, *CreateAdminJobRequest) (*CreateAdminJobResponse, error) CancelAdminJob(context.Context, *CancelAdminJobRequest) (*CancelAdminJobResponse, error) RetryAdminJob(context.Context, *RetryAdminJobRequest) (*RetryAdminJobResponse, error) + ListAdminDlqJobs(context.Context, *ListAdminDlqJobsRequest) (*ListAdminDlqJobsResponse, error) + GetAdminDlqJob(context.Context, *GetAdminDlqJobRequest) (*GetAdminDlqJobResponse, error) + RetryAdminDlqJob(context.Context, *RetryAdminDlqJobRequest) (*RetryAdminDlqJobResponse, error) + RemoveAdminDlqJob(context.Context, *RemoveAdminDlqJobRequest) (*RemoveAdminDlqJobResponse, error) ListAdminAgents(context.Context, *ListAdminAgentsRequest) (*ListAdminAgentsResponse, error) RestartAdminAgent(context.Context, *RestartAdminAgentRequest) (*AdminAgentCommandResponse, error) UpdateAdminAgent(context.Context, *UpdateAdminAgentRequest) (*AdminAgentCommandResponse, error) @@ -760,6 +812,18 @@ func (UnimplementedAdminServer) CancelAdminJob(context.Context, *CancelAdminJobR func (UnimplementedAdminServer) RetryAdminJob(context.Context, *RetryAdminJobRequest) (*RetryAdminJobResponse, error) { return nil, status.Error(codes.Unimplemented, "method RetryAdminJob not implemented") } +func (UnimplementedAdminServer) ListAdminDlqJobs(context.Context, *ListAdminDlqJobsRequest) (*ListAdminDlqJobsResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ListAdminDlqJobs not implemented") +} +func (UnimplementedAdminServer) GetAdminDlqJob(context.Context, *GetAdminDlqJobRequest) (*GetAdminDlqJobResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetAdminDlqJob not implemented") +} +func (UnimplementedAdminServer) RetryAdminDlqJob(context.Context, *RetryAdminDlqJobRequest) (*RetryAdminDlqJobResponse, error) { + return nil, status.Error(codes.Unimplemented, "method RetryAdminDlqJob not implemented") +} +func (UnimplementedAdminServer) RemoveAdminDlqJob(context.Context, *RemoveAdminDlqJobRequest) (*RemoveAdminDlqJobResponse, error) { + return nil, status.Error(codes.Unimplemented, "method RemoveAdminDlqJob not implemented") +} func (UnimplementedAdminServer) ListAdminAgents(context.Context, *ListAdminAgentsRequest) (*ListAdminAgentsResponse, error) { return nil, status.Error(codes.Unimplemented, "method ListAdminAgents not implemented") } @@ -1546,6 +1610,78 @@ func _Admin_RetryAdminJob_Handler(srv interface{}, ctx context.Context, dec func return interceptor(ctx, in, info, handler) } +func _Admin_ListAdminDlqJobs_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListAdminDlqJobsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AdminServer).ListAdminDlqJobs(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Admin_ListAdminDlqJobs_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AdminServer).ListAdminDlqJobs(ctx, req.(*ListAdminDlqJobsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _Admin_GetAdminDlqJob_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetAdminDlqJobRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AdminServer).GetAdminDlqJob(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Admin_GetAdminDlqJob_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AdminServer).GetAdminDlqJob(ctx, req.(*GetAdminDlqJobRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _Admin_RetryAdminDlqJob_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(RetryAdminDlqJobRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AdminServer).RetryAdminDlqJob(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Admin_RetryAdminDlqJob_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AdminServer).RetryAdminDlqJob(ctx, req.(*RetryAdminDlqJobRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _Admin_RemoveAdminDlqJob_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(RemoveAdminDlqJobRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AdminServer).RemoveAdminDlqJob(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Admin_RemoveAdminDlqJob_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AdminServer).RemoveAdminDlqJob(ctx, req.(*RemoveAdminDlqJobRequest)) + } + return interceptor(ctx, in, info, handler) +} + func _Admin_ListAdminAgents_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(ListAdminAgentsRequest) if err := dec(in); err != nil { @@ -1775,6 +1911,22 @@ var Admin_ServiceDesc = grpc.ServiceDesc{ MethodName: "RetryAdminJob", Handler: _Admin_RetryAdminJob_Handler, }, + { + MethodName: "ListAdminDlqJobs", + Handler: _Admin_ListAdminDlqJobs_Handler, + }, + { + MethodName: "GetAdminDlqJob", + Handler: _Admin_GetAdminDlqJob_Handler, + }, + { + MethodName: "RetryAdminDlqJob", + Handler: _Admin_RetryAdminDlqJob_Handler, + }, + { + MethodName: "RemoveAdminDlqJob", + Handler: _Admin_RemoveAdminDlqJob_Handler, + }, { MethodName: "ListAdminAgents", Handler: _Admin_ListAdminAgents_Handler, diff --git a/internal/api/proto/app/v1/common.pb.go b/internal/api/proto/app/v1/common.pb.go index b14c454..5f665db 100644 --- a/internal/api/proto/app/v1/common.pb.go +++ b/internal/api/proto/app/v1/common.pb.go @@ -3442,6 +3442,74 @@ func (x *AdminAgent) GetUpdatedAt() *timestamppb.Timestamp { return nil } +type AdminDlqEntry struct { + state protoimpl.MessageState `protogen:"open.v1"` + Job *AdminJob `protobuf:"bytes,1,opt,name=job,proto3" json:"job,omitempty"` + FailureTime *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=failure_time,json=failureTime,proto3" json:"failure_time,omitempty"` + Reason string `protobuf:"bytes,3,opt,name=reason,proto3" json:"reason,omitempty"` + RetryCount int32 `protobuf:"varint,4,opt,name=retry_count,json=retryCount,proto3" json:"retry_count,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AdminDlqEntry) Reset() { + *x = AdminDlqEntry{} + mi := &file_app_v1_common_proto_msgTypes[27] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AdminDlqEntry) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AdminDlqEntry) ProtoMessage() {} + +func (x *AdminDlqEntry) ProtoReflect() protoreflect.Message { + mi := &file_app_v1_common_proto_msgTypes[27] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AdminDlqEntry.ProtoReflect.Descriptor instead. +func (*AdminDlqEntry) Descriptor() ([]byte, []int) { + return file_app_v1_common_proto_rawDescGZIP(), []int{27} +} + +func (x *AdminDlqEntry) GetJob() *AdminJob { + if x != nil { + return x.Job + } + return nil +} + +func (x *AdminDlqEntry) GetFailureTime() *timestamppb.Timestamp { + if x != nil { + return x.FailureTime + } + return nil +} + +func (x *AdminDlqEntry) GetReason() string { + if x != nil { + return x.Reason + } + return "" +} + +func (x *AdminDlqEntry) GetRetryCount() int32 { + if x != nil { + return x.RetryCount + } + return 0 +} + var File_app_v1_common_proto protoreflect.FileDescriptor const file_app_v1_common_proto_rawDesc = "" + @@ -3954,7 +4022,13 @@ const file_app_v1_common_proto_rawDesc = "" + "\n" + "created_at\x18\v \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\x129\n" + "\n" + - "updated_at\x18\f \x01(\v2\x1a.google.protobuf.TimestampR\tupdatedAtB,Z*stream.api/internal/gen/proto/app/v1;appv1b\x06proto3" + "updated_at\x18\f \x01(\v2\x1a.google.protobuf.TimestampR\tupdatedAt\"\xb2\x01\n" + + "\rAdminDlqEntry\x12)\n" + + "\x03job\x18\x01 \x01(\v2\x17.stream.app.v1.AdminJobR\x03job\x12=\n" + + "\ffailure_time\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\vfailureTime\x12\x16\n" + + "\x06reason\x18\x03 \x01(\tR\x06reason\x12\x1f\n" + + "\vretry_count\x18\x04 \x01(\x05R\n" + + "retryCountB,Z*stream.api/internal/gen/proto/app/v1;appv1b\x06proto3" var ( file_app_v1_common_proto_rawDescOnce sync.Once @@ -3968,7 +4042,7 @@ func file_app_v1_common_proto_rawDescGZIP() []byte { return file_app_v1_common_proto_rawDescData } -var file_app_v1_common_proto_msgTypes = make([]protoimpl.MessageInfo, 27) +var file_app_v1_common_proto_msgTypes = make([]protoimpl.MessageInfo, 28) var file_app_v1_common_proto_goTypes = []any{ (*MessageResponse)(nil), // 0: stream.app.v1.MessageResponse (*User)(nil), // 1: stream.app.v1.User @@ -3997,61 +4071,64 @@ var file_app_v1_common_proto_goTypes = []any{ (*AdminPopupAd)(nil), // 24: stream.app.v1.AdminPopupAd (*AdminJob)(nil), // 25: stream.app.v1.AdminJob (*AdminAgent)(nil), // 26: stream.app.v1.AdminAgent - (*timestamppb.Timestamp)(nil), // 27: google.protobuf.Timestamp + (*AdminDlqEntry)(nil), // 27: stream.app.v1.AdminDlqEntry + (*timestamppb.Timestamp)(nil), // 28: google.protobuf.Timestamp } var file_app_v1_common_proto_depIdxs = []int32{ - 27, // 0: stream.app.v1.User.plan_started_at:type_name -> google.protobuf.Timestamp - 27, // 1: stream.app.v1.User.plan_expires_at:type_name -> google.protobuf.Timestamp - 27, // 2: stream.app.v1.User.created_at:type_name -> google.protobuf.Timestamp - 27, // 3: stream.app.v1.User.updated_at:type_name -> google.protobuf.Timestamp - 27, // 4: stream.app.v1.Notification.created_at:type_name -> google.protobuf.Timestamp - 27, // 5: stream.app.v1.Domain.created_at:type_name -> google.protobuf.Timestamp - 27, // 6: stream.app.v1.Domain.updated_at:type_name -> google.protobuf.Timestamp - 27, // 7: stream.app.v1.AdTemplate.created_at:type_name -> google.protobuf.Timestamp - 27, // 8: stream.app.v1.AdTemplate.updated_at:type_name -> google.protobuf.Timestamp - 27, // 9: stream.app.v1.PopupAd.created_at:type_name -> google.protobuf.Timestamp - 27, // 10: stream.app.v1.PopupAd.updated_at:type_name -> google.protobuf.Timestamp - 27, // 11: stream.app.v1.PlayerConfig.created_at:type_name -> google.protobuf.Timestamp - 27, // 12: stream.app.v1.PlayerConfig.updated_at:type_name -> google.protobuf.Timestamp - 27, // 13: stream.app.v1.AdminPlayerConfig.created_at:type_name -> google.protobuf.Timestamp - 27, // 14: stream.app.v1.AdminPlayerConfig.updated_at:type_name -> google.protobuf.Timestamp - 27, // 15: stream.app.v1.Payment.created_at:type_name -> google.protobuf.Timestamp - 27, // 16: stream.app.v1.Payment.updated_at:type_name -> google.protobuf.Timestamp - 27, // 17: stream.app.v1.PlanSubscription.started_at:type_name -> google.protobuf.Timestamp - 27, // 18: stream.app.v1.PlanSubscription.expires_at:type_name -> google.protobuf.Timestamp - 27, // 19: stream.app.v1.PlanSubscription.created_at:type_name -> google.protobuf.Timestamp - 27, // 20: stream.app.v1.PlanSubscription.updated_at:type_name -> google.protobuf.Timestamp - 27, // 21: stream.app.v1.WalletTransaction.created_at:type_name -> google.protobuf.Timestamp - 27, // 22: stream.app.v1.WalletTransaction.updated_at:type_name -> google.protobuf.Timestamp - 27, // 23: stream.app.v1.PaymentHistoryItem.expires_at:type_name -> google.protobuf.Timestamp - 27, // 24: stream.app.v1.PaymentHistoryItem.created_at:type_name -> google.protobuf.Timestamp - 27, // 25: stream.app.v1.Video.created_at:type_name -> google.protobuf.Timestamp - 27, // 26: stream.app.v1.Video.updated_at:type_name -> google.protobuf.Timestamp - 27, // 27: stream.app.v1.AdminUser.created_at:type_name -> google.protobuf.Timestamp - 27, // 28: stream.app.v1.AdminUser.updated_at:type_name -> google.protobuf.Timestamp + 28, // 0: stream.app.v1.User.plan_started_at:type_name -> google.protobuf.Timestamp + 28, // 1: stream.app.v1.User.plan_expires_at:type_name -> google.protobuf.Timestamp + 28, // 2: stream.app.v1.User.created_at:type_name -> google.protobuf.Timestamp + 28, // 3: stream.app.v1.User.updated_at:type_name -> google.protobuf.Timestamp + 28, // 4: stream.app.v1.Notification.created_at:type_name -> google.protobuf.Timestamp + 28, // 5: stream.app.v1.Domain.created_at:type_name -> google.protobuf.Timestamp + 28, // 6: stream.app.v1.Domain.updated_at:type_name -> google.protobuf.Timestamp + 28, // 7: stream.app.v1.AdTemplate.created_at:type_name -> google.protobuf.Timestamp + 28, // 8: stream.app.v1.AdTemplate.updated_at:type_name -> google.protobuf.Timestamp + 28, // 9: stream.app.v1.PopupAd.created_at:type_name -> google.protobuf.Timestamp + 28, // 10: stream.app.v1.PopupAd.updated_at:type_name -> google.protobuf.Timestamp + 28, // 11: stream.app.v1.PlayerConfig.created_at:type_name -> google.protobuf.Timestamp + 28, // 12: stream.app.v1.PlayerConfig.updated_at:type_name -> google.protobuf.Timestamp + 28, // 13: stream.app.v1.AdminPlayerConfig.created_at:type_name -> google.protobuf.Timestamp + 28, // 14: stream.app.v1.AdminPlayerConfig.updated_at:type_name -> google.protobuf.Timestamp + 28, // 15: stream.app.v1.Payment.created_at:type_name -> google.protobuf.Timestamp + 28, // 16: stream.app.v1.Payment.updated_at:type_name -> google.protobuf.Timestamp + 28, // 17: stream.app.v1.PlanSubscription.started_at:type_name -> google.protobuf.Timestamp + 28, // 18: stream.app.v1.PlanSubscription.expires_at:type_name -> google.protobuf.Timestamp + 28, // 19: stream.app.v1.PlanSubscription.created_at:type_name -> google.protobuf.Timestamp + 28, // 20: stream.app.v1.PlanSubscription.updated_at:type_name -> google.protobuf.Timestamp + 28, // 21: stream.app.v1.WalletTransaction.created_at:type_name -> google.protobuf.Timestamp + 28, // 22: stream.app.v1.WalletTransaction.updated_at:type_name -> google.protobuf.Timestamp + 28, // 23: stream.app.v1.PaymentHistoryItem.expires_at:type_name -> google.protobuf.Timestamp + 28, // 24: stream.app.v1.PaymentHistoryItem.created_at:type_name -> google.protobuf.Timestamp + 28, // 25: stream.app.v1.Video.created_at:type_name -> google.protobuf.Timestamp + 28, // 26: stream.app.v1.Video.updated_at:type_name -> google.protobuf.Timestamp + 28, // 27: stream.app.v1.AdminUser.created_at:type_name -> google.protobuf.Timestamp + 28, // 28: stream.app.v1.AdminUser.updated_at:type_name -> google.protobuf.Timestamp 17, // 29: stream.app.v1.AdminUserReferralInfo.referrer:type_name -> stream.app.v1.ReferralUserSummary - 27, // 30: stream.app.v1.AdminUserReferralInfo.reward_granted_at:type_name -> google.protobuf.Timestamp + 28, // 30: stream.app.v1.AdminUserReferralInfo.reward_granted_at:type_name -> google.protobuf.Timestamp 16, // 31: stream.app.v1.AdminUserDetail.user:type_name -> stream.app.v1.AdminUser 11, // 32: stream.app.v1.AdminUserDetail.subscription:type_name -> stream.app.v1.PlanSubscription 18, // 33: stream.app.v1.AdminUserDetail.referral:type_name -> stream.app.v1.AdminUserReferralInfo - 27, // 34: stream.app.v1.AdminVideo.created_at:type_name -> google.protobuf.Timestamp - 27, // 35: stream.app.v1.AdminVideo.updated_at:type_name -> google.protobuf.Timestamp - 27, // 36: stream.app.v1.AdminPayment.created_at:type_name -> google.protobuf.Timestamp - 27, // 37: stream.app.v1.AdminPayment.updated_at:type_name -> google.protobuf.Timestamp - 27, // 38: stream.app.v1.AdminAdTemplate.created_at:type_name -> google.protobuf.Timestamp - 27, // 39: stream.app.v1.AdminAdTemplate.updated_at:type_name -> google.protobuf.Timestamp - 27, // 40: stream.app.v1.AdminPopupAd.created_at:type_name -> google.protobuf.Timestamp - 27, // 41: stream.app.v1.AdminPopupAd.updated_at:type_name -> google.protobuf.Timestamp - 27, // 42: stream.app.v1.AdminJob.created_at:type_name -> google.protobuf.Timestamp - 27, // 43: stream.app.v1.AdminJob.updated_at:type_name -> google.protobuf.Timestamp - 27, // 44: stream.app.v1.AdminAgent.last_heartbeat:type_name -> google.protobuf.Timestamp - 27, // 45: stream.app.v1.AdminAgent.created_at:type_name -> google.protobuf.Timestamp - 27, // 46: stream.app.v1.AdminAgent.updated_at:type_name -> google.protobuf.Timestamp - 47, // [47:47] is the sub-list for method output_type - 47, // [47:47] is the sub-list for method input_type - 47, // [47:47] is the sub-list for extension type_name - 47, // [47:47] is the sub-list for extension extendee - 0, // [0:47] is the sub-list for field type_name + 28, // 34: stream.app.v1.AdminVideo.created_at:type_name -> google.protobuf.Timestamp + 28, // 35: stream.app.v1.AdminVideo.updated_at:type_name -> google.protobuf.Timestamp + 28, // 36: stream.app.v1.AdminPayment.created_at:type_name -> google.protobuf.Timestamp + 28, // 37: stream.app.v1.AdminPayment.updated_at:type_name -> google.protobuf.Timestamp + 28, // 38: stream.app.v1.AdminAdTemplate.created_at:type_name -> google.protobuf.Timestamp + 28, // 39: stream.app.v1.AdminAdTemplate.updated_at:type_name -> google.protobuf.Timestamp + 28, // 40: stream.app.v1.AdminPopupAd.created_at:type_name -> google.protobuf.Timestamp + 28, // 41: stream.app.v1.AdminPopupAd.updated_at:type_name -> google.protobuf.Timestamp + 28, // 42: stream.app.v1.AdminJob.created_at:type_name -> google.protobuf.Timestamp + 28, // 43: stream.app.v1.AdminJob.updated_at:type_name -> google.protobuf.Timestamp + 28, // 44: stream.app.v1.AdminAgent.last_heartbeat:type_name -> google.protobuf.Timestamp + 28, // 45: stream.app.v1.AdminAgent.created_at:type_name -> google.protobuf.Timestamp + 28, // 46: stream.app.v1.AdminAgent.updated_at:type_name -> google.protobuf.Timestamp + 25, // 47: stream.app.v1.AdminDlqEntry.job:type_name -> stream.app.v1.AdminJob + 28, // 48: stream.app.v1.AdminDlqEntry.failure_time:type_name -> google.protobuf.Timestamp + 49, // [49:49] is the sub-list for method output_type + 49, // [49:49] is the sub-list for method input_type + 49, // [49:49] is the sub-list for extension type_name + 49, // [49:49] is the sub-list for extension extendee + 0, // [0:49] is the sub-list for field type_name } func init() { file_app_v1_common_proto_init() } @@ -4085,7 +4162,7 @@ func file_app_v1_common_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_app_v1_common_proto_rawDesc), len(file_app_v1_common_proto_rawDesc)), NumEnums: 0, - NumMessages: 27, + NumMessages: 28, NumExtensions: 0, NumServices: 0, }, diff --git a/internal/api/proto/app/v1/video_metadata.pb.go b/internal/api/proto/app/v1/video_metadata.pb.go new file mode 100644 index 0000000..b18e164 --- /dev/null +++ b/internal/api/proto/app/v1/video_metadata.pb.go @@ -0,0 +1,221 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc (unknown) +// source: app/v1/video_metadata.proto + +package appv1 + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type GetVideoMetadataRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + VideoId string `protobuf:"bytes,1,opt,name=video_id,json=videoId,proto3" json:"video_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetVideoMetadataRequest) Reset() { + *x = GetVideoMetadataRequest{} + mi := &file_app_v1_video_metadata_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetVideoMetadataRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetVideoMetadataRequest) ProtoMessage() {} + +func (x *GetVideoMetadataRequest) ProtoReflect() protoreflect.Message { + mi := &file_app_v1_video_metadata_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetVideoMetadataRequest.ProtoReflect.Descriptor instead. +func (*GetVideoMetadataRequest) Descriptor() ([]byte, []int) { + return file_app_v1_video_metadata_proto_rawDescGZIP(), []int{0} +} + +func (x *GetVideoMetadataRequest) GetVideoId() string { + if x != nil { + return x.VideoId + } + return "" +} + +type GetVideoMetadataResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Video *Video `protobuf:"bytes,1,opt,name=video,proto3" json:"video,omitempty"` + DefaultPlayerConfig *PlayerConfig `protobuf:"bytes,2,opt,name=default_player_config,json=defaultPlayerConfig,proto3" json:"default_player_config,omitempty"` + AdTemplate *AdTemplate `protobuf:"bytes,3,opt,name=ad_template,json=adTemplate,proto3" json:"ad_template,omitempty"` + ActivePopupAd *PopupAd `protobuf:"bytes,4,opt,name=active_popup_ad,json=activePopupAd,proto3" json:"active_popup_ad,omitempty"` + Domains []*Domain `protobuf:"bytes,5,rep,name=domains,proto3" json:"domains,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetVideoMetadataResponse) Reset() { + *x = GetVideoMetadataResponse{} + mi := &file_app_v1_video_metadata_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetVideoMetadataResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetVideoMetadataResponse) ProtoMessage() {} + +func (x *GetVideoMetadataResponse) ProtoReflect() protoreflect.Message { + mi := &file_app_v1_video_metadata_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetVideoMetadataResponse.ProtoReflect.Descriptor instead. +func (*GetVideoMetadataResponse) Descriptor() ([]byte, []int) { + return file_app_v1_video_metadata_proto_rawDescGZIP(), []int{1} +} + +func (x *GetVideoMetadataResponse) GetVideo() *Video { + if x != nil { + return x.Video + } + return nil +} + +func (x *GetVideoMetadataResponse) GetDefaultPlayerConfig() *PlayerConfig { + if x != nil { + return x.DefaultPlayerConfig + } + return nil +} + +func (x *GetVideoMetadataResponse) GetAdTemplate() *AdTemplate { + if x != nil { + return x.AdTemplate + } + return nil +} + +func (x *GetVideoMetadataResponse) GetActivePopupAd() *PopupAd { + if x != nil { + return x.ActivePopupAd + } + return nil +} + +func (x *GetVideoMetadataResponse) GetDomains() []*Domain { + if x != nil { + return x.Domains + } + return nil +} + +var File_app_v1_video_metadata_proto protoreflect.FileDescriptor + +const file_app_v1_video_metadata_proto_rawDesc = "" + + "\n" + + "\x1bapp/v1/video_metadata.proto\x12\rstream.app.v1\x1a\x13app/v1/common.proto\"4\n" + + "\x17GetVideoMetadataRequest\x12\x19\n" + + "\bvideo_id\x18\x01 \x01(\tR\avideoId\"\xc4\x02\n" + + "\x18GetVideoMetadataResponse\x12*\n" + + "\x05video\x18\x01 \x01(\v2\x14.stream.app.v1.VideoR\x05video\x12O\n" + + "\x15default_player_config\x18\x02 \x01(\v2\x1b.stream.app.v1.PlayerConfigR\x13defaultPlayerConfig\x12:\n" + + "\vad_template\x18\x03 \x01(\v2\x19.stream.app.v1.AdTemplateR\n" + + "adTemplate\x12>\n" + + "\x0factive_popup_ad\x18\x04 \x01(\v2\x16.stream.app.v1.PopupAdR\ractivePopupAd\x12/\n" + + "\adomains\x18\x05 \x03(\v2\x15.stream.app.v1.DomainR\adomains2t\n" + + "\rVideoMetadata\x12c\n" + + "\x10GetVideoMetadata\x12&.stream.app.v1.GetVideoMetadataRequest\x1a'.stream.app.v1.GetVideoMetadataResponseB,Z*stream.api/internal/gen/proto/app/v1;appv1b\x06proto3" + +var ( + file_app_v1_video_metadata_proto_rawDescOnce sync.Once + file_app_v1_video_metadata_proto_rawDescData []byte +) + +func file_app_v1_video_metadata_proto_rawDescGZIP() []byte { + file_app_v1_video_metadata_proto_rawDescOnce.Do(func() { + file_app_v1_video_metadata_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_app_v1_video_metadata_proto_rawDesc), len(file_app_v1_video_metadata_proto_rawDesc))) + }) + return file_app_v1_video_metadata_proto_rawDescData +} + +var file_app_v1_video_metadata_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_app_v1_video_metadata_proto_goTypes = []any{ + (*GetVideoMetadataRequest)(nil), // 0: stream.app.v1.GetVideoMetadataRequest + (*GetVideoMetadataResponse)(nil), // 1: stream.app.v1.GetVideoMetadataResponse + (*Video)(nil), // 2: stream.app.v1.Video + (*PlayerConfig)(nil), // 3: stream.app.v1.PlayerConfig + (*AdTemplate)(nil), // 4: stream.app.v1.AdTemplate + (*PopupAd)(nil), // 5: stream.app.v1.PopupAd + (*Domain)(nil), // 6: stream.app.v1.Domain +} +var file_app_v1_video_metadata_proto_depIdxs = []int32{ + 2, // 0: stream.app.v1.GetVideoMetadataResponse.video:type_name -> stream.app.v1.Video + 3, // 1: stream.app.v1.GetVideoMetadataResponse.default_player_config:type_name -> stream.app.v1.PlayerConfig + 4, // 2: stream.app.v1.GetVideoMetadataResponse.ad_template:type_name -> stream.app.v1.AdTemplate + 5, // 3: stream.app.v1.GetVideoMetadataResponse.active_popup_ad:type_name -> stream.app.v1.PopupAd + 6, // 4: stream.app.v1.GetVideoMetadataResponse.domains:type_name -> stream.app.v1.Domain + 0, // 5: stream.app.v1.VideoMetadata.GetVideoMetadata:input_type -> stream.app.v1.GetVideoMetadataRequest + 1, // 6: stream.app.v1.VideoMetadata.GetVideoMetadata:output_type -> stream.app.v1.GetVideoMetadataResponse + 6, // [6:7] is the sub-list for method output_type + 5, // [5:6] is the sub-list for method input_type + 5, // [5:5] is the sub-list for extension type_name + 5, // [5:5] is the sub-list for extension extendee + 0, // [0:5] is the sub-list for field type_name +} + +func init() { file_app_v1_video_metadata_proto_init() } +func file_app_v1_video_metadata_proto_init() { + if File_app_v1_video_metadata_proto != nil { + return + } + file_app_v1_common_proto_init() + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_app_v1_video_metadata_proto_rawDesc), len(file_app_v1_video_metadata_proto_rawDesc)), + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_app_v1_video_metadata_proto_goTypes, + DependencyIndexes: file_app_v1_video_metadata_proto_depIdxs, + MessageInfos: file_app_v1_video_metadata_proto_msgTypes, + }.Build() + File_app_v1_video_metadata_proto = out.File + file_app_v1_video_metadata_proto_goTypes = nil + file_app_v1_video_metadata_proto_depIdxs = nil +} diff --git a/internal/api/proto/app/v1/video_metadata_grpc.pb.go b/internal/api/proto/app/v1/video_metadata_grpc.pb.go new file mode 100644 index 0000000..4e59f08 --- /dev/null +++ b/internal/api/proto/app/v1/video_metadata_grpc.pb.go @@ -0,0 +1,121 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.6.1 +// - protoc (unknown) +// source: app/v1/video_metadata.proto + +package appv1 + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + VideoMetadata_GetVideoMetadata_FullMethodName = "/stream.app.v1.VideoMetadata/GetVideoMetadata" +) + +// VideoMetadataClient is the client API for VideoMetadata service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type VideoMetadataClient interface { + GetVideoMetadata(ctx context.Context, in *GetVideoMetadataRequest, opts ...grpc.CallOption) (*GetVideoMetadataResponse, error) +} + +type videoMetadataClient struct { + cc grpc.ClientConnInterface +} + +func NewVideoMetadataClient(cc grpc.ClientConnInterface) VideoMetadataClient { + return &videoMetadataClient{cc} +} + +func (c *videoMetadataClient) GetVideoMetadata(ctx context.Context, in *GetVideoMetadataRequest, opts ...grpc.CallOption) (*GetVideoMetadataResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetVideoMetadataResponse) + err := c.cc.Invoke(ctx, VideoMetadata_GetVideoMetadata_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// VideoMetadataServer is the server API for VideoMetadata service. +// All implementations must embed UnimplementedVideoMetadataServer +// for forward compatibility. +type VideoMetadataServer interface { + GetVideoMetadata(context.Context, *GetVideoMetadataRequest) (*GetVideoMetadataResponse, error) + mustEmbedUnimplementedVideoMetadataServer() +} + +// UnimplementedVideoMetadataServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedVideoMetadataServer struct{} + +func (UnimplementedVideoMetadataServer) GetVideoMetadata(context.Context, *GetVideoMetadataRequest) (*GetVideoMetadataResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetVideoMetadata not implemented") +} +func (UnimplementedVideoMetadataServer) mustEmbedUnimplementedVideoMetadataServer() {} +func (UnimplementedVideoMetadataServer) testEmbeddedByValue() {} + +// UnsafeVideoMetadataServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to VideoMetadataServer will +// result in compilation errors. +type UnsafeVideoMetadataServer interface { + mustEmbedUnimplementedVideoMetadataServer() +} + +func RegisterVideoMetadataServer(s grpc.ServiceRegistrar, srv VideoMetadataServer) { + // If the following call panics, it indicates UnimplementedVideoMetadataServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&VideoMetadata_ServiceDesc, srv) +} + +func _VideoMetadata_GetVideoMetadata_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetVideoMetadataRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(VideoMetadataServer).GetVideoMetadata(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: VideoMetadata_GetVideoMetadata_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(VideoMetadataServer).GetVideoMetadata(ctx, req.(*GetVideoMetadataRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// VideoMetadata_ServiceDesc is the grpc.ServiceDesc for VideoMetadata service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var VideoMetadata_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "stream.app.v1.VideoMetadata", + HandlerType: (*VideoMetadataServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "GetVideoMetadata", + Handler: _VideoMetadata_GetVideoMetadata_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "app/v1/video_metadata.proto", +} diff --git a/internal/config/config.go b/internal/config/config.go index 7f784bf..18b2112 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -11,7 +11,6 @@ type Config struct { Database DatabaseConfig Redis RedisConfig Google GoogleConfig - Frontend FrontendConfig Email EmailConfig AWS AWSConfig Render RenderConfig @@ -25,7 +24,6 @@ type ServerConfig struct { } type RenderConfig struct { - AgentSecret string `mapstructure:"agent_secret"` EnableMetrics bool `mapstructure:"enable_metrics"` EnableTracing bool `mapstructure:"enable_tracing"` OTLPEndpoint string `mapstructure:"otlp_endpoint"` @@ -53,11 +51,6 @@ type GoogleConfig struct { StateTTLMinute int `mapstructure:"state_ttl_minutes"` } -type FrontendConfig struct { - BaseURL string `mapstructure:"base_url"` - GoogleAuthFinalizePath string `mapstructure:"google_auth_finalize_path"` -} - type EmailConfig struct { From string // Add SMTP settings here later @@ -83,7 +76,6 @@ func LoadConfig() (*Config, error) { v.SetDefault("render.enable_tracing", false) v.SetDefault("render.service_name", "stream-api-render") v.SetDefault("google.state_ttl_minutes", 10) - v.SetDefault("frontend.google_auth_finalize_path", "/auth/google/finalize") v.SetDefault("internal.marker", "") // Environment variable settings diff --git a/internal/dto/job.go b/internal/dto/job.go index 535668b..03949d5 100644 --- a/internal/dto/job.go +++ b/internal/dto/job.go @@ -44,3 +44,10 @@ type JobConfigEnvelope struct { VideoID string `json:"video_id,omitempty"` TimeLimit int64 `json:"time_limit,omitempty"` } + +type DLQEntry struct { + Job *model.Job `json:"job,omitempty"` + FailureTime int64 `json:"failure_time_unix,omitempty"` + Reason string `json:"reason,omitempty"` + RetryCount int64 `json:"retry_count,omitempty"` +} diff --git a/internal/repository/job_repository.go b/internal/repository/job_repository.go index 88208db..a641e27 100644 --- a/internal/repository/job_repository.go +++ b/internal/repository/job_repository.go @@ -100,3 +100,82 @@ func (r *jobRepository) GetLatestByVideoID(ctx context.Context, videoID string) } return &job, nil } + +func (r *jobRepository) AssignPendingJob(ctx context.Context, jobID string, agentID int64, now time.Time) (bool, error) { + result, err := query.Job.WithContext(ctx). + Where(query.Job.ID.Eq(strings.TrimSpace(jobID)), query.Job.Status.Eq("pending"), query.Job.Cancelled.Is(false)). + Updates(map[string]any{"status": "running", "agent_id": agentID, "updated_at": now}) + if err != nil { + return false, err + } + return result.RowsAffected > 0, nil +} + +func (r *jobRepository) MarkJobStatusIfCurrent(ctx context.Context, jobID string, fromStatuses []string, toStatus string, now time.Time, clearAgent bool) (bool, error) { + jobID = strings.TrimSpace(jobID) + updates := map[string]any{"status": toStatus, "updated_at": now} + if clearAgent { + updates["agent_id"] = nil + } + q := query.Job.WithContext(ctx).Where(query.Job.ID.Eq(jobID), query.Job.Status.In(fromStatuses...)) + result, err := q.Updates(updates) + if err != nil { + return false, err + } + return result.RowsAffected > 0, nil +} + +func (r *jobRepository) CancelJobIfActive(ctx context.Context, jobID string, now time.Time) (bool, error) { + result, err := query.Job.WithContext(ctx). + Where(query.Job.ID.Eq(strings.TrimSpace(jobID)), query.Job.Status.In("pending", "running")). + Updates(map[string]any{"status": "cancelled", "cancelled": true, "updated_at": now}) + if err != nil { + return false, err + } + return result.RowsAffected > 0, nil +} + +func (r *jobRepository) RequeueJob(ctx context.Context, jobID string, retryCount int64, logs *string, now time.Time) (bool, error) { + updates := map[string]any{"status": "pending", "cancelled": false, "progress": 0, "agent_id": nil, "retry_count": retryCount, "updated_at": now} + if logs != nil { + updates["logs"] = *logs + } + result, err := query.Job.WithContext(ctx). + Where(query.Job.ID.Eq(strings.TrimSpace(jobID)), query.Job.Status.In("running", "pending", "failure", "cancelled")). + Updates(updates) + if err != nil { + return false, err + } + return result.RowsAffected > 0, nil +} + +func (r *jobRepository) MoveJobToFailure(ctx context.Context, jobID string, logs *string, now time.Time) (bool, error) { + updates := map[string]any{"status": "failure", "agent_id": nil, "updated_at": now} + if logs != nil { + updates["logs"] = *logs + } + result, err := query.Job.WithContext(ctx). + Where(query.Job.ID.Eq(strings.TrimSpace(jobID)), query.Job.Status.In("running", "pending")). + Updates(updates) + if err != nil { + return false, err + } + return result.RowsAffected > 0, nil +} + +func (r *jobRepository) UpdateProgressAndLogsIfRunning(ctx context.Context, jobID string, progress *float64, logs *string, now time.Time) (bool, error) { + updates := map[string]any{"updated_at": now} + if progress != nil { + updates["progress"] = *progress + } + if logs != nil { + updates["logs"] = *logs + } + result, err := query.Job.WithContext(ctx). + Where(query.Job.ID.Eq(strings.TrimSpace(jobID)), query.Job.Status.Eq("running")). + Updates(updates) + if err != nil { + return false, err + } + return result.RowsAffected > 0, nil +} diff --git a/internal/service/__test__/service_admin_jobs_agents_test.go b/internal/service/__test__/service_admin_jobs_agents_test.go index 5cef158..3222a81 100644 --- a/internal/service/__test__/service_admin_jobs_agents_test.go +++ b/internal/service/__test__/service_admin_jobs_agents_test.go @@ -1,15 +1,17 @@ package service import ( + "context" + "fmt" "testing" "time" "github.com/google/uuid" "google.golang.org/grpc/codes" "gorm.io/gorm" + redisadapter "stream.api/internal/adapters/redis" appv1 "stream.api/internal/api/proto/app/v1" "stream.api/internal/database/model" - runtimeservices "stream.api/internal/service/runtime/services" renderworkflow "stream.api/internal/workflow/render" ) @@ -18,7 +20,7 @@ func TestListAdminJobsCursorPagination(t *testing.T) { ensureTestJobsTable(t, db) services := newTestAppServices(t, db) - services.videoWorkflowService = renderworkflow.New(db, runtimeservices.NewJobService(db, nil, nil)) + services.videoWorkflowService = renderworkflow.New(db, NewJobService(db, nil, nil, nil)) admin := seedTestUser(t, db, model.User{ID: uuid.NewString(), Email: "admin@example.com", Role: ptrString("ADMIN")}) baseTime := time.Date(2026, 3, 22, 10, 0, 0, 0, time.UTC) @@ -67,7 +69,7 @@ func TestListAdminJobsInvalidCursor(t *testing.T) { ensureTestJobsTable(t, db) services := newTestAppServices(t, db) - services.videoWorkflowService = renderworkflow.New(db, runtimeservices.NewJobService(db, nil, nil)) + services.videoWorkflowService = renderworkflow.New(db, NewJobService(db, nil, nil, nil)) admin := seedTestUser(t, db, model.User{ID: uuid.NewString(), Email: "admin@example.com", Role: ptrString("ADMIN")}) conn, cleanup := newTestGRPCServer(t, services) @@ -86,7 +88,7 @@ func TestListAdminJobsCursorRejectsAgentMismatch(t *testing.T) { ensureTestJobsTable(t, db) services := newTestAppServices(t, db) - services.videoWorkflowService = renderworkflow.New(db, runtimeservices.NewJobService(db, nil, nil)) + services.videoWorkflowService = renderworkflow.New(db, NewJobService(db, nil, nil, nil)) admin := seedTestUser(t, db, model.User{ID: uuid.NewString(), Email: "admin@example.com", Role: ptrString("ADMIN")}) baseTime := time.Date(2026, 3, 22, 11, 0, 0, 0, time.UTC) @@ -192,4 +194,212 @@ func assertAdminJobIDs(t *testing.T, jobs []*appv1.AdminJob, want []string) { } } -func ptrTime(v time.Time) *time.Time { return &v } +type fakeDLQ struct { + entries map[string]*redisadapter.DLQEntry + order []string + listErr error + getErr error + removeErr error + retryErr error +} + +func newFakeDLQ(entries ...*redisadapter.DLQEntry) *fakeDLQ { + f := &fakeDLQ{entries: map[string]*redisadapter.DLQEntry{}, order: []string{}} + for _, entry := range entries { + if entry == nil || entry.Job == nil { + continue + } + f.entries[entry.Job.ID] = entry + f.order = append(f.order, entry.Job.ID) + } + return f +} + +func (f *fakeDLQ) Add(_ context.Context, job *model.Job, reason string) error { + if f.entries == nil { + f.entries = map[string]*redisadapter.DLQEntry{} + } + entry := &redisadapter.DLQEntry{Job: job, FailureTime: time.Now().UTC(), Reason: reason} + f.entries[job.ID] = entry + f.order = append(f.order, job.ID) + return nil +} + +func (f *fakeDLQ) Get(_ context.Context, jobID string) (*redisadapter.DLQEntry, error) { + if f.getErr != nil { + return nil, f.getErr + } + entry, ok := f.entries[jobID] + if !ok { + return nil, fmt.Errorf("job not found in DLQ") + } + return entry, nil +} + +func (f *fakeDLQ) List(_ context.Context, offset, limit int64) ([]*redisadapter.DLQEntry, error) { + if f.listErr != nil { + return nil, f.listErr + } + if offset < 0 { + offset = 0 + } + if limit <= 0 { + limit = int64(len(f.order)) + } + items := make([]*redisadapter.DLQEntry, 0) + for i := offset; i < int64(len(f.order)) && int64(len(items)) < limit; i++ { + id := f.order[i] + if entry, ok := f.entries[id]; ok { + items = append(items, entry) + } + } + return items, nil +} + +func (f *fakeDLQ) Count(_ context.Context) (int64, error) { + return int64(len(f.entries)), nil +} + +func (f *fakeDLQ) Remove(_ context.Context, jobID string) error { + if f.removeErr != nil { + return f.removeErr + } + if _, ok := f.entries[jobID]; !ok { + return fmt.Errorf("job not found in DLQ") + } + delete(f.entries, jobID) + filtered := make([]string, 0, len(f.order)) + for _, id := range f.order { + if id != jobID { + filtered = append(filtered, id) + } + } + f.order = filtered + return nil +} + +func (f *fakeDLQ) Retry(ctx context.Context, jobID string) (*model.Job, error) { + if f.retryErr != nil { + return nil, f.retryErr + } + entry, err := f.Get(ctx, jobID) + if err != nil { + return nil, err + } + if err := f.Remove(ctx, jobID); err != nil { + return nil, err + } + return entry.Job, nil +} + +type fakeQueue struct { + enqueueErr error +} + +func (f *fakeQueue) Enqueue(_ context.Context, _ *model.Job) error { return f.enqueueErr } +func (f *fakeQueue) Dequeue(_ context.Context) (*model.Job, error) { return nil, nil } + +func TestAdminDlqJobs(t *testing.T) { + t.Run("list happy path", func(t *testing.T) { + db := newTestDB(t) + ensureTestJobsTable(t, db) + admin := seedTestUser(t, db, model.User{ID: uuid.NewString(), Email: "admin@example.com", Role: ptrString("ADMIN")}) + job1 := seedTestJob(t, db, model.Job{ID: "job-dlq-1", CreatedAt: ptrTime(time.Now().Add(-2 * time.Hour).UTC()), UpdatedAt: ptrTime(time.Now().Add(-2 * time.Hour).UTC())}) + job2 := seedTestJob(t, db, model.Job{ID: "job-dlq-2", CreatedAt: ptrTime(time.Now().Add(-time.Hour).UTC()), UpdatedAt: ptrTime(time.Now().Add(-time.Hour).UTC())}) + services := newTestAppServices(t, db) + services.videoWorkflowService = renderworkflow.New(db, NewJobService(db, &fakeQueue{}, nil, newFakeDLQ( + &redisadapter.DLQEntry{Job: &job1, FailureTime: time.Now().Add(-30 * time.Minute).UTC(), Reason: "lease_expired", RetryCount: 2}, + &redisadapter.DLQEntry{Job: &job2, FailureTime: time.Now().Add(-10 * time.Minute).UTC(), Reason: "invalid_config", RetryCount: 3}, + ))) + conn, cleanup := newTestGRPCServer(t, services) + defer cleanup() + client := newAdminClient(conn) + resp, err := client.ListAdminDlqJobs(testActorOutgoingContext(admin.ID, "ADMIN"), &appv1.ListAdminDlqJobsRequest{Offset: 0, Limit: 10}) + if err != nil { + t.Fatalf("ListAdminDlqJobs error = %v", err) + } + if resp.GetTotal() != 2 { + t.Fatalf("total = %d, want 2", resp.GetTotal()) + } + if len(resp.GetItems()) != 2 { + t.Fatalf("items len = %d, want 2", len(resp.GetItems())) + } + if resp.GetItems()[0].GetJob().GetId() != "job-dlq-1" { + t.Fatalf("first job id = %q", resp.GetItems()[0].GetJob().GetId()) + } + if resp.GetItems()[0].GetReason() != "lease_expired" { + t.Fatalf("first reason = %q", resp.GetItems()[0].GetReason()) + } + }) + + t.Run("get not found", func(t *testing.T) { + db := newTestDB(t) + ensureTestJobsTable(t, db) + admin := seedTestUser(t, db, model.User{ID: uuid.NewString(), Email: "admin@example.com", Role: ptrString("ADMIN")}) + services := newTestAppServices(t, db) + services.videoWorkflowService = renderworkflow.New(db, NewJobService(db, &fakeQueue{}, nil, newFakeDLQ())) + conn, cleanup := newTestGRPCServer(t, services) + defer cleanup() + client := newAdminClient(conn) + _, err := client.GetAdminDlqJob(testActorOutgoingContext(admin.ID, "ADMIN"), &appv1.GetAdminDlqJobRequest{Id: "missing"}) + assertGRPCCode(t, err, codes.NotFound) + }) + + t.Run("retry happy path", func(t *testing.T) { + db := newTestDB(t) + ensureTestJobsTable(t, db) + admin := seedTestUser(t, db, model.User{ID: uuid.NewString(), Email: "admin@example.com", Role: ptrString("ADMIN")}) + job := seedTestJob(t, db, model.Job{ID: "job-dlq-retry", Status: ptrString("failure"), CreatedAt: ptrTime(time.Now().Add(-time.Hour).UTC()), UpdatedAt: ptrTime(time.Now().Add(-time.Hour).UTC())}) + services := newTestAppServices(t, db) + services.videoWorkflowService = renderworkflow.New(db, NewJobService(db, &fakeQueue{}, nil, newFakeDLQ( + &redisadapter.DLQEntry{Job: &job, FailureTime: time.Now().UTC(), Reason: "lease_expired", RetryCount: 1}, + ))) + conn, cleanup := newTestGRPCServer(t, services) + defer cleanup() + client := newAdminClient(conn) + resp, err := client.RetryAdminDlqJob(testActorOutgoingContext(admin.ID, "ADMIN"), &appv1.RetryAdminDlqJobRequest{Id: job.ID}) + if err != nil { + t.Fatalf("RetryAdminDlqJob error = %v", err) + } + if resp.GetJob().GetId() != job.ID { + t.Fatalf("job id = %q, want %q", resp.GetJob().GetId(), job.ID) + } + if resp.GetJob().GetStatus() != "pending" { + t.Fatalf("job status = %q, want pending", resp.GetJob().GetStatus()) + } + }) + + t.Run("remove happy path", func(t *testing.T) { + db := newTestDB(t) + ensureTestJobsTable(t, db) + admin := seedTestUser(t, db, model.User{ID: uuid.NewString(), Email: "admin@example.com", Role: ptrString("ADMIN")}) + job := seedTestJob(t, db, model.Job{ID: "job-dlq-remove", CreatedAt: ptrTime(time.Now().Add(-time.Hour).UTC()), UpdatedAt: ptrTime(time.Now().Add(-time.Hour).UTC())}) + services := newTestAppServices(t, db) + services.videoWorkflowService = renderworkflow.New(db, NewJobService(db, &fakeQueue{}, nil, newFakeDLQ( + &redisadapter.DLQEntry{Job: &job, FailureTime: time.Now().UTC(), Reason: "invalid_config", RetryCount: 3}, + ))) + conn, cleanup := newTestGRPCServer(t, services) + defer cleanup() + client := newAdminClient(conn) + resp, err := client.RemoveAdminDlqJob(testActorOutgoingContext(admin.ID, "ADMIN"), &appv1.RemoveAdminDlqJobRequest{Id: job.ID}) + if err != nil { + t.Fatalf("RemoveAdminDlqJob error = %v", err) + } + if resp.GetStatus() != "removed" { + t.Fatalf("status = %q, want removed", resp.GetStatus()) + } + }) + + t.Run("list permission denied", func(t *testing.T) { + db := newTestDB(t) + ensureTestJobsTable(t, db) + user := seedTestUser(t, db, model.User{ID: uuid.NewString(), Email: "user@example.com", Role: ptrString("USER")}) + services := newTestAppServices(t, db) + services.videoWorkflowService = renderworkflow.New(db, NewJobService(db, &fakeQueue{}, nil, newFakeDLQ())) + conn, cleanup := newTestGRPCServer(t, services) + defer cleanup() + client := newAdminClient(conn) + _, err := client.ListAdminDlqJobs(testActorOutgoingContext(user.ID, "USER"), &appv1.ListAdminDlqJobsRequest{}) + assertGRPCCode(t, err, codes.PermissionDenied) + }) +} diff --git a/internal/service/__test__/service_video_metadata_test.go b/internal/service/__test__/service_video_metadata_test.go new file mode 100644 index 0000000..ffbf35e --- /dev/null +++ b/internal/service/__test__/service_video_metadata_test.go @@ -0,0 +1,245 @@ +package service + +import ( + "context" + "testing" + "time" + + "github.com/google/uuid" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/metadata" + appv1 "stream.api/internal/api/proto/app/v1" + "stream.api/internal/database/model" + "stream.api/internal/middleware" +) + +func testInternalMetadataContext() context.Context { + return metadata.NewOutgoingContext(context.Background(), metadata.Pairs( + middleware.ActorMarkerMetadataKey, testTrustedMarker, + )) +} + +func testInvalidInternalMetadataContext() context.Context { + return metadata.NewOutgoingContext(context.Background(), metadata.Pairs( + middleware.ActorMarkerMetadataKey, "wrong-marker", + )) +} + +func TestGetVideoMetadata(t *testing.T) { + t.Run("happy path with owner defaults", func(t *testing.T) { + db := newTestDB(t) + admin := seedTestUser(t, db, model.User{ID: uuid.NewString(), Email: "admin@example.com", Role: ptrString("ADMIN")}) + user := seedTestUser(t, db, model.User{ID: uuid.NewString(), Email: "paid@example.com", Role: ptrString("USER"), PlanID: ptrString("plan-paid")}) + now := time.Now().UTC() + video := model.Video{ID: uuid.NewString(), UserID: user.ID, Title: "video", URL: "https://cdn.example.com/video.mp4", Status: ptrString("ready"), CreatedAt: &now, UpdatedAt: now, AdID: nil} + if err := db.Create(&video).Error; err != nil { + t.Fatalf("create video: %v", err) + } + if err := db.Create(&model.Domain{ID: uuid.NewString(), UserID: user.ID, Name: "video.example.com"}).Error; err != nil { + t.Fatalf("create domain: %v", err) + } + if err := db.Create(&model.AdTemplate{ID: uuid.NewString(), UserID: user.ID, Name: "default-ad", VastTagURL: "https://ads.example.com/vast", IsDefault: true, IsActive: ptrBool(true)}).Error; err != nil { + t.Fatalf("create ad template: %v", err) + } + if err := db.Create(&model.PopupAd{ID: uuid.NewString(), UserID: user.ID, Type: "banner", Label: "promo", Value: "https://ads.example.com/banner", IsActive: ptrBool(true), CreatedAt: &now, UpdatedAt: now}).Error; err != nil { + t.Fatalf("create popup ad: %v", err) + } + if err := db.Create(&model.PlayerConfig{ID: uuid.NewString(), UserID: user.ID, Name: "default-player", IsDefault: true, IsActive: ptrBool(true), CreatedAt: &now, UpdatedAt: now}).Error; err != nil { + t.Fatalf("create player config: %v", err) + } + services := newTestAppServices(t, db) + _ = admin + conn, cleanup := newTestGRPCServer(t, services) + defer cleanup() + client := newVideoMetadataClient(conn) + resp, err := client.GetVideoMetadata(testInternalMetadataContext(), &appv1.GetVideoMetadataRequest{VideoId: video.ID}) + if err != nil { + t.Fatalf("GetVideoMetadata error = %v", err) + } + if resp.GetVideo().GetId() != video.ID { + t.Fatalf("video id = %q, want %q", resp.GetVideo().GetId(), video.ID) + } + if resp.GetAdTemplate().GetName() != "default-ad" { + t.Fatalf("ad template name = %q", resp.GetAdTemplate().GetName()) + } + if resp.GetActivePopupAd().GetLabel() != "promo" { + t.Fatalf("popup label = %q", resp.GetActivePopupAd().GetLabel()) + } + if resp.GetDefaultPlayerConfig().GetName() != "default-player" { + t.Fatalf("player config name = %q", resp.GetDefaultPlayerConfig().GetName()) + } + if len(resp.GetDomains()) != 1 { + t.Fatalf("domains len = %d, want 1", len(resp.GetDomains())) + } + }) + + t.Run("missing ad template returns failed precondition", func(t *testing.T) { + db := newTestDB(t) + user := seedTestUser(t, db, model.User{ID: uuid.NewString(), Email: "paid@example.com", Role: ptrString("USER"), PlanID: ptrString("plan-paid")}) + now := time.Now().UTC() + video := model.Video{ID: uuid.NewString(), UserID: user.ID, Title: "video", URL: "https://cdn.example.com/video.mp4", Status: ptrString("ready"), CreatedAt: &now, UpdatedAt: now} + _ = db.Create(&video).Error + _ = db.Create(&model.Domain{ID: uuid.NewString(), UserID: user.ID, Name: "video.example.com"}).Error + _ = db.Create(&model.PopupAd{ID: uuid.NewString(), UserID: user.ID, Type: "banner", Label: "promo", Value: "https://ads.example.com/banner", IsActive: ptrBool(true), CreatedAt: &now, UpdatedAt: now}).Error + _ = db.Create(&model.PlayerConfig{ID: uuid.NewString(), UserID: user.ID, Name: "default-player", IsDefault: true, IsActive: ptrBool(true), CreatedAt: &now, UpdatedAt: now}).Error + services := newTestAppServices(t, db) + conn, cleanup := newTestGRPCServer(t, services) + defer cleanup() + client := newVideoMetadataClient(conn) + _, err := client.GetVideoMetadata(testInternalMetadataContext(), &appv1.GetVideoMetadataRequest{VideoId: video.ID}) + assertGRPCCode(t, err, codes.FailedPrecondition) + }) + + t.Run("missing popup ad returns failed precondition", func(t *testing.T) { + db := newTestDB(t) + user := seedTestUser(t, db, model.User{ID: uuid.NewString(), Email: "paid@example.com", Role: ptrString("USER"), PlanID: ptrString("plan-paid")}) + now := time.Now().UTC() + video := model.Video{ID: uuid.NewString(), UserID: user.ID, Title: "video", URL: "https://cdn.example.com/video.mp4", Status: ptrString("ready"), CreatedAt: &now, UpdatedAt: now} + _ = db.Create(&video).Error + _ = db.Create(&model.Domain{ID: uuid.NewString(), UserID: user.ID, Name: "video.example.com"}).Error + _ = db.Create(&model.AdTemplate{ID: uuid.NewString(), UserID: user.ID, Name: "default-ad", VastTagURL: "https://ads.example.com/vast", IsDefault: true, IsActive: ptrBool(true)}).Error + _ = db.Create(&model.PlayerConfig{ID: uuid.NewString(), UserID: user.ID, Name: "default-player", IsDefault: true, IsActive: ptrBool(true), CreatedAt: &now, UpdatedAt: now}).Error + services := newTestAppServices(t, db) + conn, cleanup := newTestGRPCServer(t, services) + defer cleanup() + client := newVideoMetadataClient(conn) + _, err := client.GetVideoMetadata(testInternalMetadataContext(), &appv1.GetVideoMetadataRequest{VideoId: video.ID}) + assertGRPCCode(t, err, codes.FailedPrecondition) + }) + + t.Run("missing player config returns failed precondition", func(t *testing.T) { + db := newTestDB(t) + user := seedTestUser(t, db, model.User{ID: uuid.NewString(), Email: "paid@example.com", Role: ptrString("USER"), PlanID: ptrString("plan-paid")}) + now := time.Now().UTC() + video := model.Video{ID: uuid.NewString(), UserID: user.ID, Title: "video", URL: "https://cdn.example.com/video.mp4", Status: ptrString("ready"), CreatedAt: &now, UpdatedAt: now} + _ = db.Create(&video).Error + _ = db.Create(&model.Domain{ID: uuid.NewString(), UserID: user.ID, Name: "video.example.com"}).Error + _ = db.Create(&model.AdTemplate{ID: uuid.NewString(), UserID: user.ID, Name: "default-ad", VastTagURL: "https://ads.example.com/vast", IsDefault: true, IsActive: ptrBool(true)}).Error + _ = db.Create(&model.PopupAd{ID: uuid.NewString(), UserID: user.ID, Type: "banner", Label: "promo", Value: "https://ads.example.com/banner", IsActive: ptrBool(true), CreatedAt: &now, UpdatedAt: now}).Error + services := newTestAppServices(t, db) + conn, cleanup := newTestGRPCServer(t, services) + defer cleanup() + client := newVideoMetadataClient(conn) + _, err := client.GetVideoMetadata(testInternalMetadataContext(), &appv1.GetVideoMetadataRequest{VideoId: video.ID}) + assertGRPCCode(t, err, codes.FailedPrecondition) + }) + + t.Run("missing domains returns failed precondition", func(t *testing.T) { + db := newTestDB(t) + user := seedTestUser(t, db, model.User{ID: uuid.NewString(), Email: "paid@example.com", Role: ptrString("USER"), PlanID: ptrString("plan-paid")}) + now := time.Now().UTC() + video := model.Video{ID: uuid.NewString(), UserID: user.ID, Title: "video", URL: "https://cdn.example.com/video.mp4", Status: ptrString("ready"), CreatedAt: &now, UpdatedAt: now} + _ = db.Create(&video).Error + _ = db.Create(&model.AdTemplate{ID: uuid.NewString(), UserID: user.ID, Name: "default-ad", VastTagURL: "https://ads.example.com/vast", IsDefault: true, IsActive: ptrBool(true)}).Error + _ = db.Create(&model.PopupAd{ID: uuid.NewString(), UserID: user.ID, Type: "banner", Label: "promo", Value: "https://ads.example.com/banner", IsActive: ptrBool(true), CreatedAt: &now, UpdatedAt: now}).Error + _ = db.Create(&model.PlayerConfig{ID: uuid.NewString(), UserID: user.ID, Name: "default-player", IsDefault: true, IsActive: ptrBool(true), CreatedAt: &now, UpdatedAt: now}).Error + services := newTestAppServices(t, db) + conn, cleanup := newTestGRPCServer(t, services) + defer cleanup() + client := newVideoMetadataClient(conn) + _, err := client.GetVideoMetadata(testInternalMetadataContext(), &appv1.GetVideoMetadataRequest{VideoId: video.ID}) + assertGRPCCode(t, err, codes.FailedPrecondition) + }) + + t.Run("free user falls back to system admin config", func(t *testing.T) { + db := newTestDB(t) + admin := seedTestUser(t, db, model.User{ID: uuid.NewString(), Email: "admin@example.com", Role: ptrString("ADMIN")}) + user := seedTestUser(t, db, model.User{ID: uuid.NewString(), Email: "free@example.com", Role: ptrString("USER")}) + now := time.Now().UTC() + video := model.Video{ID: uuid.NewString(), UserID: user.ID, Title: "video", URL: "https://cdn.example.com/video.mp4", Status: ptrString("ready"), CreatedAt: &now, UpdatedAt: now} + if err := db.Create(&video).Error; err != nil { + t.Fatalf("create video: %v", err) + } + if err := db.Create(&model.Domain{ID: uuid.NewString(), UserID: user.ID, Name: "free.example.com"}).Error; err != nil { + t.Fatalf("create domain: %v", err) + } + if err := db.Create(&model.AdTemplate{ID: uuid.NewString(), UserID: admin.ID, Name: "system-ad", VastTagURL: "https://ads.example.com/system", IsDefault: true, IsActive: ptrBool(true)}).Error; err != nil { + t.Fatalf("create system ad template: %v", err) + } + if err := db.Create(&model.PopupAd{ID: uuid.NewString(), UserID: admin.ID, Type: "banner", Label: "system-popup", Value: "https://ads.example.com/system-popup", IsActive: ptrBool(true), CreatedAt: &now, UpdatedAt: now}).Error; err != nil { + t.Fatalf("create system popup ad: %v", err) + } + if err := db.Create(&model.PlayerConfig{ID: uuid.NewString(), UserID: admin.ID, Name: "system-player", IsDefault: true, IsActive: ptrBool(true), CreatedAt: &now, UpdatedAt: now}).Error; err != nil { + t.Fatalf("create system player config: %v", err) + } + services := newTestAppServices(t, db) + conn, cleanup := newTestGRPCServer(t, services) + defer cleanup() + client := newVideoMetadataClient(conn) + resp, err := client.GetVideoMetadata(testInternalMetadataContext(), &appv1.GetVideoMetadataRequest{VideoId: video.ID}) + if err != nil { + t.Fatalf("GetVideoMetadata error = %v", err) + } + if resp.GetAdTemplate().GetName() != "system-ad" { + t.Fatalf("ad template = %q, want system-ad", resp.GetAdTemplate().GetName()) + } + if resp.GetActivePopupAd().GetLabel() != "system-popup" { + t.Fatalf("popup label = %q, want system-popup", resp.GetActivePopupAd().GetLabel()) + } + if resp.GetDefaultPlayerConfig().GetName() != "system-player" { + t.Fatalf("player config = %q, want system-player", resp.GetDefaultPlayerConfig().GetName()) + } + if len(resp.GetDomains()) != 1 || resp.GetDomains()[0].GetName() != "free.example.com" { + t.Fatalf("domains = %#v, want owner domains", resp.GetDomains()) + } + }) + + t.Run("video ad id takes precedence over fallback ad template", func(t *testing.T) { + db := newTestDB(t) + admin := seedTestUser(t, db, model.User{ID: uuid.NewString(), Email: "admin@example.com", Role: ptrString("ADMIN")}) + user := seedTestUser(t, db, model.User{ID: uuid.NewString(), Email: "free@example.com", Role: ptrString("USER")}) + now := time.Now().UTC() + videoAd := model.AdTemplate{ID: uuid.NewString(), UserID: user.ID, Name: "video-ad", VastTagURL: "https://ads.example.com/video", IsDefault: false, IsActive: ptrBool(true)} + if err := db.Create(&videoAd).Error; err != nil { + t.Fatalf("create video ad template: %v", err) + } + video := model.Video{ID: uuid.NewString(), UserID: user.ID, Title: "video", URL: "https://cdn.example.com/video.mp4", Status: ptrString("ready"), CreatedAt: &now, UpdatedAt: now, AdID: &videoAd.ID} + if err := db.Create(&video).Error; err != nil { + t.Fatalf("create video: %v", err) + } + if err := db.Create(&model.Domain{ID: uuid.NewString(), UserID: user.ID, Name: "free.example.com"}).Error; err != nil { + t.Fatalf("create domain: %v", err) + } + if err := db.Create(&model.AdTemplate{ID: uuid.NewString(), UserID: admin.ID, Name: "system-ad", VastTagURL: "https://ads.example.com/system", IsDefault: true, IsActive: ptrBool(true)}).Error; err != nil { + t.Fatalf("create system ad template: %v", err) + } + if err := db.Create(&model.PopupAd{ID: uuid.NewString(), UserID: admin.ID, Type: "banner", Label: "system-popup", Value: "https://ads.example.com/system-popup", IsActive: ptrBool(true), CreatedAt: &now, UpdatedAt: now}).Error; err != nil { + t.Fatalf("create system popup ad: %v", err) + } + if err := db.Create(&model.PlayerConfig{ID: uuid.NewString(), UserID: admin.ID, Name: "system-player", IsDefault: true, IsActive: ptrBool(true), CreatedAt: &now, UpdatedAt: now}).Error; err != nil { + t.Fatalf("create system player config: %v", err) + } + services := newTestAppServices(t, db) + _ = admin + conn, cleanup := newTestGRPCServer(t, services) + defer cleanup() + client := newVideoMetadataClient(conn) + resp, err := client.GetVideoMetadata(testInternalMetadataContext(), &appv1.GetVideoMetadataRequest{VideoId: video.ID}) + if err != nil { + t.Fatalf("GetVideoMetadata error = %v", err) + } + if resp.GetAdTemplate().GetName() != "video-ad" { + t.Fatalf("ad template = %q, want video-ad", resp.GetAdTemplate().GetName()) + } + }) + + t.Run("invalid marker returns unauthenticated", func(t *testing.T) { + db := newTestDB(t) + services := newTestAppServices(t, db) + conn, cleanup := newTestGRPCServer(t, services) + defer cleanup() + client := newVideoMetadataClient(conn) + _, err := client.GetVideoMetadata(testInvalidInternalMetadataContext(), &appv1.GetVideoMetadataRequest{VideoId: uuid.NewString()}) + assertGRPCCode(t, err, codes.Unauthenticated) + }) + + t.Run("missing marker returns unauthenticated", func(t *testing.T) { + db := newTestDB(t) + services := newTestAppServices(t, db) + conn, cleanup := newTestGRPCServer(t, services) + defer cleanup() + client := newVideoMetadataClient(conn) + _, err := client.GetVideoMetadata(context.Background(), &appv1.GetVideoMetadataRequest{VideoId: uuid.NewString()}) + assertGRPCCode(t, err, codes.Unauthenticated) + }) +} diff --git a/internal/service/__test__/testdb_setup_test.go b/internal/service/__test__/testdb_setup_test.go index ea8673b..e672e27 100644 --- a/internal/service/__test__/testdb_setup_test.go +++ b/internal/service/__test__/testdb_setup_test.go @@ -9,13 +9,13 @@ import ( "time" "github.com/google/uuid" + _ "github.com/mattn/go-sqlite3" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/metadata" "google.golang.org/grpc/test/bufconn" "gorm.io/driver/sqlite" "gorm.io/gorm" - _ "github.com/mattn/go-sqlite3" appv1 "stream.api/internal/api/proto/app/v1" "stream.api/internal/database/model" "stream.api/internal/database/query" @@ -210,14 +210,11 @@ func newTestDB(t *testing.T) *gorm.DB { `CREATE TABLE popup_ads ( id TEXT PRIMARY KEY, user_id TEXT NOT NULL, - title TEXT NOT NULL, - image_url TEXT NOT NULL, - target_url TEXT NOT NULL, + type TEXT NOT NULL, + label TEXT NOT NULL, + value TEXT NOT NULL, is_active BOOLEAN NOT NULL DEFAULT 1, - start_at DATETIME, - end_at DATETIME, - priority INTEGER NOT NULL DEFAULT 0, - close_cooldown_minutes INTEGER NOT NULL DEFAULT 60, + max_triggers_per_session INTEGER, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME, version INTEGER NOT NULL DEFAULT 1 @@ -273,6 +270,7 @@ func newTestGRPCServer(t *testing.T, services *appServices) (*grpc.ClientConn, f PlansServer: services, PaymentsServer: services, VideosServer: services, + VideoMetadataServer: services, AdminServer: services, }) @@ -407,6 +405,10 @@ func newAdminClient(conn *grpc.ClientConn) appv1.AdminClient { return appv1.NewAdminClient(conn) } +func newVideoMetadataClient(conn *grpc.ClientConn) appv1.VideoMetadataClient { + return appv1.NewVideoMetadataClient(conn) +} + func ptrTime(v time.Time) *time.Time { return &v } func seedTestPopupAd(t *testing.T, db *gorm.DB, item model.PopupAd) model.PopupAd { diff --git a/internal/service/admin_helpers.go b/internal/service/admin_helpers.go index a078267..716c48e 100644 --- a/internal/service/admin_helpers.go +++ b/internal/service/admin_helpers.go @@ -65,31 +65,61 @@ func buildAdminJob(job *model.Job) *appv1.AdminJob { if job == nil { return nil } - agentID := strconv.FormatInt(*job.AgentID, 10) + var agentID *string + if job.AgentID != nil { + value := strconv.FormatInt(*job.AgentID, 10) + agentID = &value + } return &appv1.AdminJob{ Id: job.ID, - Status: string(*job.Status), - Priority: int32(*job.Priority), - UserId: *job.UserID, + Status: stringValue(job.Status), + Priority: int32(int64Value(job.Priority)), + UserId: stringValue(job.UserID), Name: job.ID, - TimeLimit: *job.TimeLimit, - InputUrl: *job.InputURL, - OutputUrl: *job.OutputURL, - TotalDuration: *job.TotalDuration, - CurrentTime: *job.CurrentTime, - Progress: *job.Progress, - AgentId: &agentID, - Logs: *job.Logs, - Config: *job.Config, - Cancelled: *job.Cancelled, - RetryCount: int32(*job.RetryCount), - MaxRetries: int32(*job.MaxRetries), - CreatedAt: timestamppb.New(*job.CreatedAt), - UpdatedAt: timestamppb.New(*job.UpdatedAt), - VideoId: stringPointerOrNil(*job.VideoID), + TimeLimit: int64Value(job.TimeLimit), + InputUrl: stringValue(job.InputURL), + OutputUrl: stringValue(job.OutputURL), + TotalDuration: int64Value(job.TotalDuration), + CurrentTime: int64Value(job.CurrentTime), + Progress: float64Value(job.Progress), + AgentId: agentID, + Logs: stringValue(job.Logs), + Config: stringValue(job.Config), + Cancelled: boolValue(job.Cancelled), + RetryCount: int32(int64Value(job.RetryCount)), + MaxRetries: int32(int64Value(job.MaxRetries)), + CreatedAt: timeToProto(job.CreatedAt), + UpdatedAt: timeToProto(job.UpdatedAt), + VideoId: job.VideoID, } } +func buildAdminDlqEntry(entry *dto.DLQEntry) *appv1.AdminDlqEntry { + if entry == nil { + return nil + } + return &appv1.AdminDlqEntry{ + Job: buildAdminJob(entry.Job), + FailureTime: timestamppb.New(time.Unix(entry.FailureTime, 0).UTC()), + Reason: entry.Reason, + RetryCount: int32(entry.RetryCount), + } +} + +func int64Value(value *int64) int64 { + if value == nil { + return 0 + } + return *value +} + +func float64Value(value *float64) float64 { + if value == nil { + return 0 + } + return *value +} + func buildAdminAgent(agent *dto.AgentWithStats) *appv1.AdminAgent { if agent == nil || agent.Agent == nil { return nil @@ -526,15 +556,20 @@ func (s *appServices) buildAdminPopupAd(ctx context.Context, item *model.PopupAd } payload := &appv1.AdminPopupAd{ - Id: item.ID, - UserId: item.UserID, - Type: item.Type, - Label: item.Label, - Value: item.Value, - IsActive: boolValue(item.IsActive), - MaxTriggersPerSession: func() int32 { if item.MaxTriggersPerSession != nil { return *item.MaxTriggersPerSession }; return 0 }(), - CreatedAt: timeToProto(item.CreatedAt), - UpdatedAt: timeToProto(item.UpdatedAt), + Id: item.ID, + UserId: item.UserID, + Type: item.Type, + Label: item.Label, + Value: item.Value, + IsActive: boolValue(item.IsActive), + MaxTriggersPerSession: func() int32 { + if item.MaxTriggersPerSession != nil { + return *item.MaxTriggersPerSession + } + return 0 + }(), + CreatedAt: timeToProto(item.CreatedAt), + UpdatedAt: timeToProto(item.UpdatedAt), } ownerEmail, err := s.loadAdminUserEmail(ctx, item.UserID) diff --git a/internal/service/catalog_mapper.go b/internal/service/catalog_mapper.go index 73fb5a3..9bd7c68 100644 --- a/internal/service/catalog_mapper.go +++ b/internal/service/catalog_mapper.go @@ -40,14 +40,19 @@ func toProtoPopupAd(item *model.PopupAd) *appv1.PopupAd { return nil } return &appv1.PopupAd{ - Id: item.ID, - Type: item.Type, - Label: item.Label, - Value: item.Value, - IsActive: boolValue(item.IsActive), - MaxTriggersPerSession: func() int32 { if item.MaxTriggersPerSession != nil { return *item.MaxTriggersPerSession }; return 0 }(), - CreatedAt: timeToProto(item.CreatedAt), - UpdatedAt: timeToProto(item.UpdatedAt), + Id: item.ID, + Type: item.Type, + Label: item.Label, + Value: item.Value, + IsActive: boolValue(item.IsActive), + MaxTriggersPerSession: func() int32 { + if item.MaxTriggersPerSession != nil { + return *item.MaxTriggersPerSession + } + return 0 + }(), + CreatedAt: timeToProto(item.CreatedAt), + UpdatedAt: timeToProto(item.UpdatedAt), } } diff --git a/internal/service/interface.go b/internal/service/interface.go index f3daa84..a3aea13 100644 --- a/internal/service/interface.go +++ b/internal/service/interface.go @@ -125,6 +125,10 @@ type VideoWorkflow interface { CreateJob(ctx context.Context, userID string, videoID string, name string, config []byte, priority int, timeLimit int64) (*model.Job, error) CancelJob(ctx context.Context, id string) error RetryJob(ctx context.Context, id string) (*model.Job, error) + ListDLQ(ctx context.Context, offset, limit int) ([]*dto.DLQEntry, int64, error) + GetDLQ(ctx context.Context, id string) (*dto.DLQEntry, error) + RetryDLQ(ctx context.Context, id string) (*model.Job, error) + RemoveDLQ(ctx context.Context, id string) error } type VideoRepository interface { @@ -199,6 +203,12 @@ type JobRepository interface { ListByOffset(ctx context.Context, agentID string, offset int, limit int) ([]*model.Job, int64, error) Save(ctx context.Context, job *model.Job) error UpdateVideoStatus(ctx context.Context, videoID string, statusValue string, processingStatus string) error + AssignPendingJob(ctx context.Context, jobID string, agentID int64, now time.Time) (bool, error) + MarkJobStatusIfCurrent(ctx context.Context, jobID string, fromStatuses []string, toStatus string, now time.Time, clearAgent bool) (bool, error) + CancelJobIfActive(ctx context.Context, jobID string, now time.Time) (bool, error) + RequeueJob(ctx context.Context, jobID string, retryCount int64, logs *string, now time.Time) (bool, error) + MoveJobToFailure(ctx context.Context, jobID string, logs *string, now time.Time) (bool, error) + UpdateProgressAndLogsIfRunning(ctx context.Context, jobID string, progress *float64, logs *string, now time.Time) (bool, error) } type AgentRuntime interface { diff --git a/internal/service/notification_events_test.go b/internal/service/notification_events_test.go index 57017f6..5a89cbf 100644 --- a/internal/service/notification_events_test.go +++ b/internal/service/notification_events_test.go @@ -8,9 +8,9 @@ import ( "github.com/google/uuid" "google.golang.org/grpc/metadata" - _ "modernc.org/sqlite" "gorm.io/driver/sqlite" "gorm.io/gorm" + _ "modernc.org/sqlite" appv1 "stream.api/internal/api/proto/app/v1" "stream.api/internal/database/model" "stream.api/internal/database/query" @@ -89,8 +89,8 @@ func notificationTestContext(userID, role string) context.Context { )) } -func notificationPtrString(v string) *string { return &v } -func notificationPtrBool(v bool) *bool { return &v } +func notificationPtrString(v string) *string { return &v } +func notificationPtrBool(v bool) *bool { return &v } func notificationPtrFloat64(v float64) *float64 { return &v } func seedNotificationUser(t *testing.T, db *gorm.DB, user model.User) model.User { diff --git a/internal/service/referral_helpers.go b/internal/service/referral_helpers.go index e75a5f3..6e77df1 100644 --- a/internal/service/referral_helpers.go +++ b/internal/service/referral_helpers.go @@ -65,12 +65,7 @@ func (s *appServices) buildReferralShareLink(username *string) *string { return nil } path := "/ref/" + url.PathEscape(trimmed) - base := strings.TrimRight(strings.TrimSpace(s.frontendBaseURL), "/") - if base == "" { - return &path - } - link := base + path - return &link + return &path } func (s *appServices) loadReferralUsersByUsername(ctx context.Context, username string) ([]model.User, error) { diff --git a/internal/service/register.go b/internal/service/register.go index d49e555..136278a 100644 --- a/internal/service/register.go +++ b/internal/service/register.go @@ -16,5 +16,6 @@ func Register(server grpc.ServiceRegistrar, services *Services) { appv1.RegisterPlansServer(server, services.PlansServer) appv1.RegisterPaymentsServer(server, services.PaymentsServer) appv1.RegisterVideosServer(server, services.VideosServer) + appv1.RegisterVideoMetadataServer(server, services.VideoMetadataServer) appv1.RegisterAdminServer(server, services.AdminServer) } diff --git a/internal/service/service_admin_jobs_agents.go b/internal/service/service_admin_jobs_agents.go index 74f7344..812bb26 100644 --- a/internal/service/service_admin_jobs_agents.go +++ b/internal/service/service_admin_jobs_agents.go @@ -171,6 +171,88 @@ func (s *appServices) RetryAdminJob(ctx context.Context, req *appv1.RetryAdminJo } return &appv1.RetryAdminJobResponse{Job: buildAdminJob(job)}, nil } + +func (s *appServices) ListAdminDlqJobs(ctx context.Context, req *appv1.ListAdminDlqJobsRequest) (*appv1.ListAdminDlqJobsResponse, error) { + if _, err := s.requireAdmin(ctx); err != nil { + return nil, err + } + if s.videoWorkflowService == nil { + return nil, status.Error(codes.Unavailable, "Job service is unavailable") + } + offset := int(req.GetOffset()) + limit := int(req.GetLimit()) + items, total, err := s.videoWorkflowService.ListDLQ(ctx, offset, limit) + if err != nil { + return nil, status.Error(codes.Internal, "Failed to list DLQ jobs") + } + entries := make([]*appv1.AdminDlqEntry, 0, len(items)) + for _, item := range items { + entries = append(entries, buildAdminDlqEntry(item)) + } + return &appv1.ListAdminDlqJobsResponse{Items: entries, Total: total, Offset: int32(offset), Limit: int32(limit)}, nil +} + +func (s *appServices) GetAdminDlqJob(ctx context.Context, req *appv1.GetAdminDlqJobRequest) (*appv1.GetAdminDlqJobResponse, error) { + if _, err := s.requireAdmin(ctx); err != nil { + return nil, err + } + if s.videoWorkflowService == nil { + return nil, status.Error(codes.Unavailable, "Job service is unavailable") + } + id := strings.TrimSpace(req.GetId()) + if id == "" { + return nil, status.Error(codes.NotFound, "Job not found in DLQ") + } + item, err := s.videoWorkflowService.GetDLQ(ctx, id) + if err != nil { + if strings.Contains(strings.ToLower(err.Error()), "not found") { + return nil, status.Error(codes.NotFound, "Job not found in DLQ") + } + return nil, status.Error(codes.Internal, "Failed to load DLQ job") + } + return &appv1.GetAdminDlqJobResponse{Item: buildAdminDlqEntry(item)}, nil +} + +func (s *appServices) RetryAdminDlqJob(ctx context.Context, req *appv1.RetryAdminDlqJobRequest) (*appv1.RetryAdminDlqJobResponse, error) { + if _, err := s.requireAdmin(ctx); err != nil { + return nil, err + } + if s.videoWorkflowService == nil { + return nil, status.Error(codes.Unavailable, "Job service is unavailable") + } + id := strings.TrimSpace(req.GetId()) + if id == "" { + return nil, status.Error(codes.NotFound, "Job not found in DLQ") + } + job, err := s.videoWorkflowService.RetryDLQ(ctx, id) + if err != nil { + if strings.Contains(strings.ToLower(err.Error()), "not found") { + return nil, status.Error(codes.NotFound, "Job not found in DLQ") + } + return nil, status.Error(codes.FailedPrecondition, err.Error()) + } + return &appv1.RetryAdminDlqJobResponse{Job: buildAdminJob(job)}, nil +} + +func (s *appServices) RemoveAdminDlqJob(ctx context.Context, req *appv1.RemoveAdminDlqJobRequest) (*appv1.RemoveAdminDlqJobResponse, error) { + if _, err := s.requireAdmin(ctx); err != nil { + return nil, err + } + if s.videoWorkflowService == nil { + return nil, status.Error(codes.Unavailable, "Job service is unavailable") + } + id := strings.TrimSpace(req.GetId()) + if id == "" { + return nil, status.Error(codes.NotFound, "Job not found in DLQ") + } + if err := s.videoWorkflowService.RemoveDLQ(ctx, id); err != nil { + if strings.Contains(strings.ToLower(err.Error()), "not found") { + return nil, status.Error(codes.NotFound, "Job not found in DLQ") + } + return nil, status.Error(codes.Internal, "Failed to remove DLQ job") + } + return &appv1.RemoveAdminDlqJobResponse{Status: "removed", JobId: id}, nil +} func (s *appServices) ListAdminAgents(ctx context.Context, _ *appv1.ListAdminAgentsRequest) (*appv1.ListAdminAgentsResponse, error) { if _, err := s.requireAdmin(ctx); err != nil { return nil, err diff --git a/internal/service/service_core.go b/internal/service/service_core.go index 5413aa2..701a5e6 100644 --- a/internal/service/service_core.go +++ b/internal/service/service_core.go @@ -53,6 +53,7 @@ type Services struct { appv1.PlansServer appv1.PaymentsServer appv1.VideosServer + appv1.VideoMetadataServer appv1.AdminServer } @@ -79,6 +80,7 @@ type appServices struct { appv1.UnimplementedPlansServer appv1.UnimplementedPaymentsServer appv1.UnimplementedVideosServer + appv1.UnimplementedVideoMetadataServer appv1.UnimplementedAdminServer db *gorm.DB @@ -104,7 +106,6 @@ type appServices struct { googleOauth *oauth2.Config googleStateTTL time.Duration googleUserInfoURL string - frontendBaseURL string jobRepository JobRepository } @@ -172,11 +173,6 @@ func NewServices(c *redis.RedisAdapter, db *gorm.DB, l logger.Logger, cfg *confi } } - frontendBaseURL := "" - if cfg != nil { - frontendBaseURL = cfg.Frontend.BaseURL - } - service := &appServices{ db: db, logger: l, @@ -202,7 +198,6 @@ func NewServices(c *redis.RedisAdapter, db *gorm.DB, l logger.Logger, cfg *confi googleOauth: googleOauth, googleStateTTL: googleStateTTL, googleUserInfoURL: defaultGoogleUserInfoURL, - frontendBaseURL: frontendBaseURL, } return &Services{ AuthServer: &authAppService{appServices: service}, @@ -215,6 +210,7 @@ func NewServices(c *redis.RedisAdapter, db *gorm.DB, l logger.Logger, cfg *confi PlansServer: &plansAppService{appServices: service}, PaymentsServer: &paymentsAppService{appServices: service}, VideosServer: &videosAppService{appServices: service}, + VideoMetadataServer: &videosAppService{appServices: service}, AdminServer: &adminAppService{appServices: service}, } } diff --git a/internal/service/service_job.go b/internal/service/service_job.go index 56f3f19..f9dbb6e 100644 --- a/internal/service/service_job.go +++ b/internal/service/service_job.go @@ -12,9 +12,11 @@ import ( "time" "gorm.io/gorm" + redisadapter "stream.api/internal/adapters/redis" "stream.api/internal/database/model" "stream.api/internal/dto" "stream.api/internal/repository" + "stream.api/pkg/logger" ) type JobQueue interface { @@ -33,20 +35,36 @@ type LogPubSub interface { SubscribeJobUpdates(ctx context.Context) (<-chan string, error) } +type DeadLetterQueue interface { + Add(ctx context.Context, job *model.Job, reason string) error + Get(ctx context.Context, jobID string) (*redisadapter.DLQEntry, error) + List(ctx context.Context, offset, limit int64) ([]*redisadapter.DLQEntry, error) + Count(ctx context.Context) (int64, error) + Remove(ctx context.Context, jobID string) error + Retry(ctx context.Context, jobID string) (*model.Job, error) +} + type JobService struct { queue JobQueue pubsub LogPubSub + dlq DeadLetterQueue jobRepository JobRepository + logger logger.Logger } -func NewJobService(db *gorm.DB, queue JobQueue, pubsub LogPubSub) *JobService { +func NewJobService(db *gorm.DB, queue JobQueue, pubsub LogPubSub, dlq DeadLetterQueue) *JobService { return &JobService{ queue: queue, pubsub: pubsub, + dlq: dlq, jobRepository: repository.NewJobRepository(db), } } +func (s *JobService) SetLogger(l logger.Logger) { + s.logger = l +} + var ErrInvalidJobCursor = errors.New("invalid job cursor") func strPtr(v string) *string { return &v } @@ -165,11 +183,16 @@ func (s *JobService) CreateJob(ctx context.Context, userID string, videoID strin if err := syncVideoStatus(ctx, s.jobRepository, videoID, dto.JobStatusPending); err != nil { return nil, err } - // dtoJob := todtoJob(job) + if s.queue == nil { + return nil, errors.New("job queue is unavailable") + } + if err := s.removeFromQueue(ctx, job.ID); err != nil { + return nil, err + } if err := s.queue.Enqueue(ctx, job); err != nil { return nil, err } - _ = s.pubsub.PublishJobUpdate(ctx, job.ID, status, videoID) + _ = s.publishJobUpdate(ctx, job.ID, status, videoID) return job, nil } @@ -233,18 +256,55 @@ func (s *JobService) GetJob(ctx context.Context, id string) (*model.Job, error) } func (s *JobService) GetNextJob(ctx context.Context) (*model.Job, error) { - return s.queue.Dequeue(ctx) + if s.queue == nil { + return nil, errors.New("job queue is unavailable") + } + + for { + job, err := s.queue.Dequeue(ctx) + if err != nil || job == nil { + return job, err + } + + fresh, err := s.jobRepository.GetByID(ctx, job.ID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + continue + } + return nil, err + } + if !s.canDispatchJob(fresh) { + continue + } + return fresh, nil + } } + func (s *JobService) SubscribeSystemResources(ctx context.Context) (<-chan dto.SystemResource, error) { + if s.pubsub == nil { + return nil, errors.New("job pubsub is unavailable") + } return s.pubsub.SubscribeResources(ctx) } + func (s *JobService) SubscribeJobLogs(ctx context.Context, jobID string) (<-chan dto.LogEntry, error) { + if s.pubsub == nil { + return nil, errors.New("job pubsub is unavailable") + } return s.pubsub.Subscribe(ctx, jobID) } + func (s *JobService) SubscribeCancel(ctx context.Context, agentID string) (<-chan string, error) { + if s.pubsub == nil { + return nil, errors.New("job pubsub is unavailable") + } return s.pubsub.SubscribeCancel(ctx, agentID) } + func (s *JobService) SubscribeJobUpdates(ctx context.Context) (<-chan string, error) { + if s.pubsub == nil { + return nil, errors.New("job pubsub is unavailable") + } return s.pubsub.SubscribeJobUpdates(ctx) } @@ -253,17 +313,38 @@ func (s *JobService) UpdateJobStatus(ctx context.Context, jobID string, status d if err != nil { return err } + currentStatus := s.jobStatus(job) + if (currentStatus == dto.JobStatusCancelled || currentStatus == dto.JobStatusSuccess) && currentStatus != status { + return nil + } + if currentStatus == status { + return nil + } + now := time.Now() - job.Status = strPtr(string(status)) - job.UpdatedAt = &now - if err := s.jobRepository.Save(ctx, job); err != nil { + updated := false + switch status { + case dto.JobStatusRunning: + updated, err = s.jobRepository.MarkJobStatusIfCurrent(ctx, jobID, []string{string(dto.JobStatusPending)}, string(status), now, false) + case dto.JobStatusSuccess: + updated, err = s.jobRepository.MarkJobStatusIfCurrent(ctx, jobID, []string{string(dto.JobStatusRunning)}, string(status), now, true) + if err == nil && updated { + err = s.removeFromQueue(ctx, jobID) + } + default: + updated, err = s.jobRepository.MarkJobStatusIfCurrent(ctx, jobID, []string{string(currentStatus)}, string(status), now, status == dto.JobStatusFailure || status == dto.JobStatusCancelled) + } + if err != nil { return err } + if !updated { + return nil + } cfg := parseJobConfig(job.Config) if err := syncVideoStatus(ctx, s.jobRepository, cfg.VideoID, status); err != nil { return err } - return s.pubsub.PublishJobUpdate(ctx, jobID, string(status), cfg.VideoID) + return s.publishJobUpdate(ctx, jobID, string(status), cfg.VideoID) } func (s *JobService) AssignJob(ctx context.Context, jobID string, agentID string) error { @@ -271,23 +352,26 @@ func (s *JobService) AssignJob(ctx context.Context, jobID string, agentID string if err != nil { return err } + if !s.canDispatchJob(job) { + return fmt.Errorf("job %s is not dispatchable", jobID) + } + agentNumeric, err := strconv.ParseInt(agentID, 10, 64) if err != nil { return err } - now := time.Now() - status := string(dto.JobStatusRunning) - job.AgentID = &agentNumeric - job.Status = &status - job.UpdatedAt = &now - if err := s.jobRepository.Save(ctx, job); err != nil { + updated, err := s.jobRepository.AssignPendingJob(ctx, jobID, agentNumeric, time.Now()) + if err != nil { return err } + if !updated { + return fmt.Errorf("job %s is not dispatchable", jobID) + } cfg := parseJobConfig(job.Config) if err := syncVideoStatus(ctx, s.jobRepository, cfg.VideoID, dto.JobStatusRunning); err != nil { return err } - return s.pubsub.PublishJobUpdate(ctx, jobID, status, cfg.VideoID) + return s.publishJobUpdate(ctx, jobID, string(dto.JobStatusRunning), cfg.VideoID) } func (s *JobService) CancelJob(ctx context.Context, jobID string) error { @@ -295,31 +379,25 @@ func (s *JobService) CancelJob(ctx context.Context, jobID string) error { if err != nil { return fmt.Errorf("job not found: %w", err) } - currentStatus := "" - if job.Status != nil { - currentStatus = *job.Status - } - if currentStatus != string(dto.JobStatusPending) && currentStatus != string(dto.JobStatusRunning) { - return fmt.Errorf("cannot cancel job with status %s", currentStatus) - } - cancelled := true - status := string(dto.JobStatusCancelled) - now := time.Now() - job.Cancelled = &cancelled - job.Status = &status - job.UpdatedAt = &now - if err := s.jobRepository.Save(ctx, job); err != nil { + updated, err := s.jobRepository.CancelJobIfActive(ctx, jobID, time.Now()) + if err != nil { return err } + if !updated { + return fmt.Errorf("cannot cancel job with status %s", s.jobStatus(job)) + } cfg := parseJobConfig(job.Config) if err := syncVideoStatus(ctx, s.jobRepository, cfg.VideoID, dto.JobStatusCancelled); err != nil { return err } - _ = s.pubsub.PublishJobUpdate(ctx, jobID, status, cfg.VideoID) - if job.AgentID != nil { + _ = s.publishJobUpdate(ctx, jobID, string(dto.JobStatusCancelled), cfg.VideoID) + if err := s.removeFromQueue(ctx, job.ID); err != nil { + return err + } + if job.AgentID != nil && s.pubsub != nil { _ = s.pubsub.PublishCancel(ctx, strconv.FormatInt(*job.AgentID, 10), job.ID) } - return s.pubsub.Publish(ctx, jobID, "[SYSTEM] Job cancelled by admin", -1) + return s.publishLog(ctx, jobID, "[SYSTEM] Job cancelled by admin", -1) } func (s *JobService) RetryJob(ctx context.Context, jobID string) (*model.Job, error) { @@ -327,47 +405,17 @@ func (s *JobService) RetryJob(ctx context.Context, jobID string) (*model.Job, er if err != nil { return nil, fmt.Errorf("job not found: %w", err) } - currentStatus := "" - if job.Status != nil { - currentStatus = *job.Status - } - if currentStatus != string(dto.JobStatusFailure) && currentStatus != string(dto.JobStatusCancelled) { + currentStatus := s.jobStatus(job) + if currentStatus != dto.JobStatusFailure && currentStatus != dto.JobStatusCancelled { return nil, fmt.Errorf("cannot retry job with status %s", currentStatus) } - currentRetry := int64(0) - if job.RetryCount != nil { - currentRetry = *job.RetryCount - } - maxRetries := int64(3) - if job.MaxRetries != nil { - maxRetries = *job.MaxRetries - } - if currentRetry >= maxRetries { - return nil, fmt.Errorf("max retries (%d) exceeded", maxRetries) - } - pending := string(dto.JobStatusPending) - cancelled := false - progress := 0.0 - now := time.Now() - job.Status = &pending - job.Cancelled = &cancelled - job.RetryCount = int64Ptr(currentRetry + 1) - job.Progress = &progress - job.AgentID = nil - job.UpdatedAt = &now - if err := s.jobRepository.Save(ctx, job); err != nil { + if err := s.requeueJob(ctx, job, false); err != nil { return nil, err } - cfg := parseJobConfig(job.Config) - if err := syncVideoStatus(ctx, s.jobRepository, cfg.VideoID, dto.JobStatusPending); err != nil { - return nil, err + if s.dlq != nil { + _ = s.dlq.Remove(ctx, jobID) } - // dtoJob := todtoJob(job) - if err := s.queue.Enqueue(ctx, job); err != nil { - return nil, err - } - _ = s.pubsub.PublishJobUpdate(ctx, jobID, pending, cfg.VideoID) - return job, nil + return s.jobRepository.GetByID(ctx, jobID) } func (s *JobService) UpdateJobProgress(ctx context.Context, jobID string, progress float64) error { @@ -381,7 +429,7 @@ func (s *JobService) UpdateJobProgress(ctx context.Context, jobID string, progre if err := s.jobRepository.Save(ctx, job); err != nil { return err } - return s.pubsub.Publish(ctx, jobID, "", progress) + return s.publishLog(ctx, jobID, "", progress) } func (s *JobService) ProcessLog(ctx context.Context, jobID string, logData []byte) error { @@ -420,7 +468,7 @@ func (s *JobService) ProcessLog(ctx context.Context, jobID string, logData []byt if err := s.jobRepository.Save(ctx, job); err != nil { return err } - return s.pubsub.Publish(ctx, jobID, line, progress) + return s.publishLog(ctx, jobID, line, progress) } func syncVideoStatus(ctx context.Context, jobRepository JobRepository, videoID string, status dto.JobStatus) error { @@ -444,5 +492,345 @@ func syncVideoStatus(ctx context.Context, jobRepository JobRepository, videoID s } func (s *JobService) PublishSystemResources(ctx context.Context, agentID string, data []byte) error { + if s.pubsub == nil { + return errors.New("job pubsub is unavailable") + } return s.pubsub.PublishResource(ctx, agentID, data) } + +func (s *JobService) StartInflightReclaimLoop(ctx context.Context, interval time.Duration, batchSize int64) { + if interval <= 0 { + interval = 30 * time.Second + } + if s.logger != nil { + s.logger.Info("started inflight reclaim loop", "interval", interval.String(), "batch_size", batchSize) + } + ticker := time.NewTicker(interval) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + if s.logger != nil { + s.logger.Info("stopped inflight reclaim loop") + } + return + case <-ticker.C: + _ = s.reclaimExpiredOnce(ctx, batchSize) + } + } +} + +func (s *JobService) reclaimExpiredOnce(ctx context.Context, batchSize int64) error { + type expirable interface { + ListExpiredInflight(ctx context.Context, now time.Time, limit int64) ([]string, error) + } + queue, ok := s.queue.(expirable) + if !ok { + return nil + } + startedAt := time.Now() + jobIDs, err := queue.ListExpiredInflight(ctx, time.Now(), batchSize) + if err != nil { + if s.logger != nil { + s.logger.Error("failed to list expired inflight jobs", "error", err) + } + return err + } + for _, jobID := range jobIDs { + _ = s.handleExpiredInflightJob(ctx, jobID) + } + if s.logger != nil && len(jobIDs) > 0 { + s.logger.Info("completed inflight reclaim batch", "expired_count", len(jobIDs), "duration_ms", time.Since(startedAt).Milliseconds()) + } + return nil +} + +func (s *JobService) handleExpiredInflightJob(ctx context.Context, jobID string) error { + job, err := s.jobRepository.GetByID(ctx, jobID) + if err != nil { + if s.logger != nil { + s.logger.Warn("failed to load expired inflight job", "job_id", jobID, "error", err) + } + return err + } + status := s.jobStatus(job) + if s.logger != nil { + s.logger.Warn("processing expired inflight job", "job_id", jobID, "current_status", status) + } + switch status { + case dto.JobStatusRunning: + return s.HandleJobFailure(ctx, jobID, "lease_expired") + case dto.JobStatusPending, dto.JobStatusSuccess, dto.JobStatusFailure, dto.JobStatusCancelled: + return s.removeFromQueue(ctx, jobID) + default: + return s.removeFromQueue(ctx, jobID) + } +} + +func (s *JobService) RenewJobLease(ctx context.Context, jobID string) error { + type touchable interface { + TouchInflight(ctx context.Context, jobID string, ttl time.Duration) error + } + queue, ok := s.queue.(touchable) + if !ok { + return nil + } + return queue.TouchInflight(ctx, jobID, 15*time.Minute) +} + +func (s *JobService) HandleDispatchFailure(ctx context.Context, jobID string, reason string, retryable bool) error { + job, err := s.jobRepository.GetByID(ctx, jobID) + if err != nil { + return err + } + if !retryable { + return s.moveJobToDLQ(ctx, job, reason) + } + return s.requeueOrDLQ(ctx, job, reason) +} + +func (s *JobService) HandleJobFailure(ctx context.Context, jobID string, reason string) error { + job, err := s.jobRepository.GetByID(ctx, jobID) + if err != nil { + return err + } + return s.requeueOrDLQ(ctx, job, reason) +} + +func (s *JobService) HandleAgentDisconnect(ctx context.Context, jobID string) error { + job, err := s.jobRepository.GetByID(ctx, jobID) + if err != nil { + return err + } + return s.requeueOrDLQ(ctx, job, "agent_unregistered") +} + +func (s *JobService) requeueOrDLQ(ctx context.Context, job *model.Job, reason string) error { + if job == nil { + return nil + } + willRetry := s.canAutoRetry(job) + if s.logger != nil { + s.logger.Warn("evaluating retry vs dlq", "job_id", job.ID, "reason", reason, "retry_count", s.retryCount(job), "max_retries", s.maxRetries(job), "will_retry", willRetry) + } + if !willRetry { + return s.moveJobToDLQ(ctx, job, reason) + } + return s.requeueJob(ctx, job, true) +} + +func (s *JobService) requeueJob(ctx context.Context, job *model.Job, incrementRetry bool) error { + if job == nil { + return nil + } + if s.queue == nil { + return errors.New("job queue is unavailable") + } + + pending := string(dto.JobStatusPending) + cancelled := false + progress := 0.0 + now := time.Now() + job.Status = &pending + job.Cancelled = &cancelled + job.Progress = &progress + job.AgentID = nil + job.UpdatedAt = &now + if incrementRetry { + job.RetryCount = int64Ptr(s.retryCount(job) + 1) + job.Logs = appendSystemLog(job.Logs, fmt.Sprintf("[SYSTEM] Auto-retry scheduled at %s", now.UTC().Format(time.RFC3339))) + } + if err := s.jobRepository.Save(ctx, job); err != nil { + return err + } + cfg := parseJobConfig(job.Config) + if err := syncVideoStatus(ctx, s.jobRepository, cfg.VideoID, dto.JobStatusPending); err != nil { + return err + } + if err := s.queue.Enqueue(ctx, job); err != nil { + return err + } + if s.logger != nil { + s.logger.Info("requeued job", "job_id", job.ID, "retry_count", s.retryCount(job), "max_retries", s.maxRetries(job), "action", "requeue") + } + return s.publishJobUpdate(ctx, job.ID, pending, cfg.VideoID) +} + +func (s *JobService) moveJobToDLQ(ctx context.Context, job *model.Job, reason string) error { + if job == nil { + return nil + } + failure := string(dto.JobStatusFailure) + now := time.Now() + job.Status = &failure + job.AgentID = nil + job.UpdatedAt = &now + job.Logs = appendSystemLog(job.Logs, fmt.Sprintf("[SYSTEM] Sent to DLQ: %s", strings.TrimSpace(reason))) + if err := s.jobRepository.Save(ctx, job); err != nil { + return err + } + cfg := parseJobConfig(job.Config) + if err := syncVideoStatus(ctx, s.jobRepository, cfg.VideoID, dto.JobStatusFailure); err != nil { + return err + } + if err := s.removeFromQueue(ctx, job.ID); err != nil { + return err + } + if s.dlq != nil { + if err := s.dlq.Add(ctx, job, strings.TrimSpace(reason)); err != nil { + return err + } + if s.logger != nil { + dlqCount, _ := s.dlq.Count(ctx) + s.logger.Warn("moved job to dlq", "job_id", job.ID, "reason", strings.TrimSpace(reason), "retry_count", s.retryCount(job), "max_retries", s.maxRetries(job), "dlq_size", dlqCount) + } + } + return s.publishJobUpdate(ctx, job.ID, failure, cfg.VideoID) +} + +func (s *JobService) canDispatchJob(job *model.Job) bool { + if job == nil { + return false + } + if job.Cancelled != nil && *job.Cancelled { + return false + } + return s.jobStatus(job) == dto.JobStatusPending +} + +func (s *JobService) canAutoRetry(job *model.Job) bool { + return s.retryCount(job) < s.maxRetries(job) +} + +func (s *JobService) retryCount(job *model.Job) int64 { + if job == nil || job.RetryCount == nil { + return 0 + } + return *job.RetryCount +} + +func (s *JobService) maxRetries(job *model.Job) int64 { + if job == nil || job.MaxRetries == nil || *job.MaxRetries <= 0 { + return 3 + } + return *job.MaxRetries +} + +func (s *JobService) jobStatus(job *model.Job) dto.JobStatus { + if job == nil || job.Status == nil { + return dto.JobStatusPending + } + return dto.JobStatus(strings.TrimSpace(*job.Status)) +} + +func (s *JobService) publishJobUpdate(ctx context.Context, jobID string, status string, videoID string) error { + if s.pubsub == nil { + return nil + } + return s.pubsub.PublishJobUpdate(ctx, jobID, status, videoID) +} + +func (s *JobService) publishLog(ctx context.Context, jobID string, logLine string, progress float64) error { + if s.pubsub == nil { + return nil + } + return s.pubsub.Publish(ctx, jobID, logLine, progress) +} + +func (s *JobService) ListDLQ(ctx context.Context, offset, limit int) ([]*dto.DLQEntry, int64, error) { + if s.dlq == nil { + return []*dto.DLQEntry{}, 0, nil + } + if offset < 0 { + offset = 0 + } + if limit <= 0 { + limit = 20 + } + entries, err := s.dlq.List(ctx, int64(offset), int64(limit)) + if err != nil { + return nil, 0, err + } + count, err := s.dlq.Count(ctx) + if err != nil { + return nil, 0, err + } + items := make([]*dto.DLQEntry, 0, len(entries)) + for _, entry := range entries { + items = append(items, &dto.DLQEntry{Job: entry.Job, FailureTime: entry.FailureTime.Unix(), Reason: entry.Reason, RetryCount: entry.RetryCount}) + } + return items, count, nil +} + +func (s *JobService) GetDLQ(ctx context.Context, id string) (*dto.DLQEntry, error) { + if s.dlq == nil { + return nil, fmt.Errorf("job not found in DLQ") + } + entry, err := s.dlq.Get(ctx, strings.TrimSpace(id)) + if err != nil { + return nil, err + } + return &dto.DLQEntry{Job: entry.Job, FailureTime: entry.FailureTime.Unix(), Reason: entry.Reason, RetryCount: entry.RetryCount}, nil +} + +func (s *JobService) RetryDLQ(ctx context.Context, id string) (*model.Job, error) { + if s.dlq == nil { + return nil, fmt.Errorf("job not found in DLQ") + } + job, err := s.dlq.Retry(ctx, strings.TrimSpace(id)) + if err != nil { + return nil, err + } + if job == nil { + return nil, fmt.Errorf("job not found in DLQ") + } + if err := s.requeueJob(ctx, job, false); err != nil { + return nil, err + } + if s.logger != nil { + count, _ := s.dlq.Count(ctx) + s.logger.Info("retried job from dlq", "job_id", job.ID, "dlq_size", count) + } + return s.jobRepository.GetByID(ctx, job.ID) +} + +func (s *JobService) RemoveDLQ(ctx context.Context, id string) error { + if s.dlq == nil { + return fmt.Errorf("job not found in DLQ") + } + jobID := strings.TrimSpace(id) + if err := s.dlq.Remove(ctx, jobID); err != nil { + return err + } + if s.logger != nil { + count, _ := s.dlq.Count(ctx) + s.logger.Info("removed job from dlq", "job_id", jobID, "dlq_size", count) + } + return nil +} + +func (s *JobService) removeFromQueue(ctx context.Context, jobID string) error { + type ackable interface { + Ack(ctx context.Context, jobID string) error + } + if queue, ok := s.queue.(ackable); ok { + return queue.Ack(ctx, jobID) + } + return nil +} + +func appendSystemLog(logs *string, line string) *string { + line = strings.TrimSpace(line) + if line == "" { + return logs + } + existing := "" + if logs != nil { + existing = *logs + } + if existing != "" && !strings.HasSuffix(existing, "\n") { + existing += "\n" + } + existing += line + "\n" + return &existing +} diff --git a/internal/service/service_popup_ads_test.go b/internal/service/service_popup_ads_test.go index 8c98b1f..fcf8b6d 100644 --- a/internal/service/service_popup_ads_test.go +++ b/internal/service/service_popup_ads_test.go @@ -7,12 +7,12 @@ import ( "time" "github.com/google/uuid" - _ "modernc.org/sqlite" "google.golang.org/grpc/codes" "google.golang.org/grpc/metadata" "google.golang.org/grpc/status" "gorm.io/driver/sqlite" "gorm.io/gorm" + _ "modernc.org/sqlite" appv1 "stream.api/internal/api/proto/app/v1" "stream.api/internal/database/model" "stream.api/internal/database/query" @@ -152,9 +152,9 @@ func popupTestContext(userID, role string) context.Context { )) } -func popupPtrString(v string) *string { return &v } -func popupPtrBool(v bool) *bool { return &v } -func popupPtrInt32(v int32) *int32 { return &v } +func popupPtrString(v string) *string { return &v } +func popupPtrBool(v bool) *bool { return &v } +func popupPtrInt32(v int32) *int32 { return &v } func popupPtrTime(v time.Time) *time.Time { return &v } func popupSeedUser(t *testing.T, db *gorm.DB, user model.User) model.User { @@ -209,10 +209,10 @@ func TestPopupAdsUserFlow(t *testing.T) { user := popupSeedUser(t, db, model.User{ID: uuid.NewString(), Email: "user@example.com", Role: popupPtrString("USER")}) createResp, err := (&popupAdsAppService{appServices: services}).CreatePopupAd(popupTestContext(user.ID, "USER"), &appv1.CreatePopupAdRequest{ - Type: "url", - Label: "Homepage Campaign", - Value: "https://example.com/landing", - IsActive: popupPtrBool(true), + Type: "url", + Label: "Homepage Campaign", + Value: "https://example.com/landing", + IsActive: popupPtrBool(true), MaxTriggersPerSession: popupPtrInt32(5), }) if err != nil { @@ -231,11 +231,11 @@ func TestPopupAdsUserFlow(t *testing.T) { } updateResp, err := (&popupAdsAppService{appServices: services}).UpdatePopupAd(popupTestContext(user.ID, "USER"), &appv1.UpdatePopupAdRequest{ - Id: createResp.Item.Id, - Type: "script", - Label: "Homepage Campaign v2", - Value: ``, - IsActive: popupPtrBool(false), + Id: createResp.Item.Id, + Type: "script", + Label: "Homepage Campaign v2", + Value: ``, + IsActive: popupPtrBool(false), MaxTriggersPerSession: popupPtrInt32(8), }) if err != nil { @@ -302,11 +302,11 @@ func TestPopupAdsAdminFlow(t *testing.T) { user := popupSeedUser(t, db, model.User{ID: uuid.NewString(), Email: "user@example.com", Role: popupPtrString("USER")}) createResp, err := services.CreateAdminPopupAd(popupTestContext(admin.ID, "ADMIN"), &appv1.CreateAdminPopupAdRequest{ - UserId: user.ID, - Type: "url", - Label: "Admin Campaign", - Value: "https://example.com/admin", - IsActive: popupPtrBool(true), + UserId: user.ID, + Type: "url", + Label: "Admin Campaign", + Value: "https://example.com/admin", + IsActive: popupPtrBool(true), MaxTriggersPerSession: popupPtrInt32(7), }) if err != nil { @@ -325,12 +325,12 @@ func TestPopupAdsAdminFlow(t *testing.T) { } updateResp, err := services.UpdateAdminPopupAd(popupTestContext(admin.ID, "ADMIN"), &appv1.UpdateAdminPopupAdRequest{ - Id: createResp.Item.Id, - UserId: user.ID, - Type: "script", - Label: "Admin Campaign v2", - Value: ``, - IsActive: popupPtrBool(false), + Id: createResp.Item.Id, + UserId: user.ID, + Type: "script", + Label: "Admin Campaign v2", + Value: ``, + IsActive: popupPtrBool(false), MaxTriggersPerSession: popupPtrInt32(11), }) if err != nil { diff --git a/internal/service/service_videos.go b/internal/service/service_videos.go index 4bf69b4..70b2de8 100644 --- a/internal/service/service_videos.go +++ b/internal/service/service_videos.go @@ -12,6 +12,7 @@ import ( "google.golang.org/grpc/status" "gorm.io/gorm" appv1 "stream.api/internal/api/proto/app/v1" + "stream.api/internal/database/model" ) func (s *videosAppService) GetUploadUrl(ctx context.Context, req *appv1.GetUploadUrlRequest) (*appv1.GetUploadUrlResponse, error) { @@ -151,6 +152,94 @@ func (s *videosAppService) GetVideo(ctx context.Context, req *appv1.GetVideoRequ } return &appv1.GetVideoResponse{Video: payload}, nil } + +func (s *videosAppService) GetVideoMetadata(ctx context.Context, req *appv1.GetVideoMetadataRequest) (*appv1.GetVideoMetadataResponse, error) { + if _, err := s.authenticator.RequireTrustedMetadata(ctx); err != nil { + return nil, err + } + videoID := strings.TrimSpace(req.GetVideoId()) + if videoID == "" { + return nil, status.Error(codes.NotFound, "Video not found") + } + if s.videoRepository == nil { + return nil, status.Error(codes.Internal, "Video repository is unavailable") + } + video, err := s.videoRepository.GetByID(ctx, videoID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, status.Error(codes.NotFound, "Video not found") + } + s.logger.Error("Failed to fetch video metadata source video", "error", err, "video_id", videoID) + return nil, status.Error(codes.Internal, "Failed to fetch video metadata") + } + videoPayload, err := s.buildVideo(ctx, video) + if err != nil { + s.logger.Error("Failed to build video metadata video payload", "error", err, "video_id", video.ID) + return nil, status.Error(codes.Internal, "Failed to fetch video metadata") + } + + ownerID := strings.TrimSpace(video.UserID) + ownerUser, err := s.userRepository.GetByID(ctx, ownerID) + if err != nil { + s.logger.Error("Failed to load video owner for metadata", "error", err, "video_id", video.ID) + return nil, status.Error(codes.Internal, "Failed to fetch video metadata") + } + configOwnerID := ownerID + if ownerUser.PlanID == nil || strings.TrimSpace(*ownerUser.PlanID) == "" { + configOwnerID, err = s.resolveSystemConfigOwnerID(ctx) + if err != nil { + s.logger.Error("Failed to resolve system config owner", "error", err, "video_id", video.ID) + return nil, status.Error(codes.Internal, "Failed to fetch video metadata") + } + } + + domains, err := s.domainRepository.ListByUser(ctx, ownerID) + if err != nil { + s.logger.Error("Failed to load video metadata domains", "error", err, "video_id", video.ID) + return nil, status.Error(codes.Internal, "Failed to fetch video metadata") + } + protoDomains := make([]*appv1.Domain, 0, len(domains)) + for i := range domains { + item := domains[i] + protoDomains = append(protoDomains, toProtoDomain(&item)) + } + + playerConfig, err := s.resolveDefaultPlayerConfig(ctx, configOwnerID) + if err != nil { + s.logger.Error("Failed to load default player config for video metadata", "error", err, "video_id", video.ID) + return nil, status.Error(codes.Internal, "Failed to fetch video metadata") + } + if playerConfig == nil { + return nil, status.Error(codes.FailedPrecondition, "Default player config is required") + } + popupAd, err := s.resolveActivePopupAd(ctx, configOwnerID) + if err != nil { + s.logger.Error("Failed to load popup ad for video metadata", "error", err, "video_id", video.ID) + return nil, status.Error(codes.Internal, "Failed to fetch video metadata") + } + if popupAd == nil { + return nil, status.Error(codes.FailedPrecondition, "Active popup ad is required") + } + adTemplate, err := s.resolveEffectiveAdTemplate(ctx, video, ownerID, configOwnerID) + if err != nil { + s.logger.Error("Failed to load ad template for video metadata", "error", err, "video_id", video.ID) + return nil, status.Error(codes.Internal, "Failed to fetch video metadata") + } + if adTemplate == nil { + return nil, status.Error(codes.FailedPrecondition, "Ad template is required") + } + if len(protoDomains) == 0 { + return nil, status.Error(codes.FailedPrecondition, "At least one domain is required") + } + + return &appv1.GetVideoMetadataResponse{ + Video: videoPayload, + DefaultPlayerConfig: toProtoPlayerConfig(playerConfig), + AdTemplate: toProtoAdTemplate(adTemplate), + ActivePopupAd: toProtoPopupAd(popupAd), + Domains: protoDomains, + }, nil +} func (s *videosAppService) UpdateVideo(ctx context.Context, req *appv1.UpdateVideoRequest) (*appv1.UpdateVideoResponse, error) { result, err := s.authenticate(ctx) if err != nil { @@ -216,6 +305,59 @@ func (s *videosAppService) UpdateVideo(ctx context.Context, req *appv1.UpdateVid } return &appv1.UpdateVideoResponse{Video: payload}, nil } +func (s *videosAppService) resolveSystemConfigOwnerID(ctx context.Context) (string, error) { + users, _, err := s.userRepository.ListForAdmin(ctx, "", "ADMIN", 1, 0) + if err != nil { + return "", err + } + if len(users) == 0 { + return "", fmt.Errorf("system config owner not found") + } + return users[0].ID, nil +} + +func (s *videosAppService) resolveDefaultPlayerConfig(ctx context.Context, userID string) (*model.PlayerConfig, error) { + items, err := s.playerConfigRepo.ListByUser(ctx, userID) + if err != nil { + return nil, err + } + if len(items) == 0 { + return nil, nil + } + return &items[0], nil +} + +func (s *videosAppService) resolveActivePopupAd(ctx context.Context, userID string) (*model.PopupAd, error) { + item, err := s.popupAdRepository.GetActiveByUser(ctx, userID) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, nil + } + return nil, err + } + return item, nil +} + +func (s *videosAppService) resolveEffectiveAdTemplate(ctx context.Context, video *model.Video, ownerID string, configOwnerID string) (*model.AdTemplate, error) { + if video != nil && video.AdID != nil && strings.TrimSpace(*video.AdID) != "" { + item, err := s.adTemplateRepository.GetByIDAndUser(ctx, strings.TrimSpace(*video.AdID), ownerID) + if err == nil { + return item, nil + } + if !errors.Is(err, gorm.ErrRecordNotFound) { + return nil, err + } + } + items, err := s.adTemplateRepository.ListByUser(ctx, configOwnerID) + if err != nil { + return nil, err + } + if len(items) == 0 { + return nil, nil + } + return &items[0], nil +} + func (s *videosAppService) DeleteVideo(ctx context.Context, req *appv1.DeleteVideoRequest) (*appv1.MessageResponse, error) { result, err := s.authenticate(ctx) if err != nil { diff --git a/internal/transport/grpc/agent_lifecycle.go b/internal/transport/grpc/agent_lifecycle.go index fdf1d7e..f73bcf7 100644 --- a/internal/transport/grpc/agent_lifecycle.go +++ b/internal/transport/grpc/agent_lifecycle.go @@ -7,7 +7,6 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/status" proto "stream.api/internal/api/proto/agent/v1" - "stream.api/internal/dto" ) func (s *Server) RegisterAgent(ctx context.Context, req *proto.RegisterAgentRequest) (*proto.RegisterAgentResponse, error) { @@ -39,7 +38,7 @@ func (s *Server) UnregisterAgent(ctx context.Context, _ *proto.Empty) (*proto.Em return nil, status.Error(codes.Unauthenticated, "invalid session") } for _, jobID := range s.getAgentJobs(agentID) { - _ = s.jobService.UpdateJobStatus(ctx, jobID, dto.JobStatusFailure) + _ = s.jobService.HandleAgentDisconnect(ctx, jobID) s.untrackJobAssignment(agentID, jobID) } s.sessions.Delete(token) diff --git a/internal/transport/grpc/auth.go b/internal/transport/grpc/auth.go index 39e373d..7f4fca3 100644 --- a/internal/transport/grpc/auth.go +++ b/internal/transport/grpc/auth.go @@ -41,7 +41,7 @@ func (s *Server) getAgentIDFromContext(ctx context.Context) (string, string, boo func (s *Server) Auth(ctx context.Context, req *proto.AuthRequest) (*proto.AuthResponse, error) { if s.agentSecret != "" && req.AgentToken != s.agentSecret { - return nil, status.Error(codes.Unauthenticated, "invalid agent secret") + return nil, status.Error(codes.Unauthenticated, "invalid internal marker") } agentID := req.AgentId if len(agentID) > 6 && agentID[:6] == "agent-" { diff --git a/internal/transport/grpc/server.go b/internal/transport/grpc/server.go index b5f90cc..24fbfb9 100644 --- a/internal/transport/grpc/server.go +++ b/internal/transport/grpc/server.go @@ -3,6 +3,7 @@ package grpc import ( "context" "net" + "time" grpcpkg "google.golang.org/grpc" "gorm.io/gorm" @@ -21,11 +22,14 @@ type GRPCModule struct { mqttPublisher *mqtt.MQTTBootstrap grpcServer *grpcpkg.Server cfg *config.Config + cancel context.CancelFunc } func NewGRPCModule(ctx context.Context, cfg *config.Config, db *gorm.DB, rds *redisadapter.RedisAdapter, appLogger logger.Logger) (*GRPCModule, error) { - jobService := service.NewJobService(db, rds, rds) - agentRuntime := NewServer(jobService, cfg.Render.AgentSecret) + moduleCtx, cancel := context.WithCancel(ctx) + jobService := service.NewJobService(db, rds, rds, redisadapter.NewDeadLetterQueue(rds.Client())) + jobService.SetLogger(appLogger) + agentRuntime := NewServer(jobService, cfg.Internal.Marker) videoService := renderworkflow.New(db, jobService) grpcServer := grpcpkg.NewServer() @@ -34,6 +38,7 @@ func NewGRPCModule(ctx context.Context, cfg *config.Config, db *gorm.DB, rds *re agentRuntime: agentRuntime, grpcServer: grpcServer, cfg: cfg, + cancel: cancel, } var notificationPublisher service.NotificationEventPublisher = nil @@ -50,8 +55,9 @@ func NewGRPCModule(ctx context.Context, cfg *config.Config, db *gorm.DB, rds *re agentRuntime.Register(grpcServer) service.Register(grpcServer, service.NewServices(rds, db, appLogger, cfg, videoService, agentRuntime, notificationPublisher)) if module.mqttPublisher != nil { - module.mqttPublisher.Start(ctx) + module.mqttPublisher.Start(moduleCtx) } + go jobService.StartInflightReclaimLoop(moduleCtx, 30*time.Second, 100) return module, nil } @@ -62,6 +68,9 @@ func (m *GRPCModule) GRPCServer() *grpcpkg.Server { return m.grpcServe func (m *GRPCModule) GRPCAddress() string { return ":" + m.cfg.Server.GRPCPort } func (m *GRPCModule) ServeGRPC(listener net.Listener) error { return m.grpcServer.Serve(listener) } func (m *GRPCModule) Shutdown() { + if m.cancel != nil { + m.cancel() + } if m.grpcServer != nil { m.grpcServer.GracefulStop() } diff --git a/internal/transport/grpc/stream_handlers.go b/internal/transport/grpc/stream_handlers.go index 1c938a8..c22b741 100644 --- a/internal/transport/grpc/stream_handlers.go +++ b/internal/transport/grpc/stream_handlers.go @@ -53,12 +53,14 @@ func (s *Server) StreamJobs(_ *proto.StreamOptions, stream grpcpkg.ServerStreami } s.trackJobAssignment(agentID, job.ID) if err := s.jobService.AssignJob(ctx, job.ID, agentID); err != nil { + _ = s.jobService.HandleDispatchFailure(ctx, job.ID, "assign_failed", true) s.untrackJobAssignment(agentID, job.ID) continue } + var config map[string]any - if err := json.Unmarshal([]byte(*job.Config), &config); err != nil { - _ = s.jobService.UpdateJobStatus(ctx, job.ID, dto.JobStatusFailure) + if job.Config == nil || json.Unmarshal([]byte(*job.Config), &config) != nil { + _ = s.jobService.HandleDispatchFailure(ctx, job.ID, "invalid_config", false) s.untrackJobAssignment(agentID, job.ID) continue } @@ -80,7 +82,7 @@ func (s *Server) StreamJobs(_ *proto.StreamOptions, stream grpcpkg.ServerStreami } payload, _ := json.Marshal(map[string]any{"image": image, "commands": commands, "environment": map[string]string{}}) if err := stream.Send(&proto.Workflow{Id: job.ID, Timeout: 60 * 60 * 1000, Payload: payload}); err != nil { - _ = s.jobService.UpdateJobStatus(ctx, job.ID, dto.JobStatusPending) + _ = s.jobService.HandleDispatchFailure(ctx, job.ID, "stream_send_failed", true) s.untrackJobAssignment(agentID, job.ID) return err } @@ -101,8 +103,10 @@ func (s *Server) SubmitStatus(stream grpcpkg.ClientStreamingServer[proto.StatusU } switch update.Type { case 0, 1: + _ = s.jobService.RenewJobLease(ctx, update.StepUuid) _ = s.jobService.ProcessLog(ctx, update.StepUuid, update.Data) case 4: + _ = s.jobService.RenewJobLease(ctx, update.StepUuid) var progress float64 fmt.Sscanf(string(update.Data), "%f", &progress) _ = s.jobService.UpdateJobProgress(ctx, update.StepUuid, progress) @@ -126,6 +130,7 @@ func (s *Server) Init(ctx context.Context, req *proto.InitRequest) (*proto.Empty if err := s.jobService.UpdateJobStatus(ctx, req.Id, dto.JobStatusRunning); err != nil { return nil, status.Error(codes.Internal, "failed to update job status") } + _ = s.jobService.RenewJobLease(ctx, req.Id) return &proto.Empty{}, nil } @@ -138,11 +143,13 @@ func (s *Server) Done(ctx context.Context, req *proto.DoneRequest) (*proto.Empty if !ok { return nil, status.Error(codes.Unauthenticated, "invalid session") } - jobStatus := dto.JobStatusSuccess + var err error if req.State != nil && req.State.Error != "" { - jobStatus = dto.JobStatusFailure + err = s.jobService.HandleJobFailure(ctx, req.Id, req.State.Error) + } else { + err = s.jobService.UpdateJobStatus(ctx, req.Id, dto.JobStatusSuccess) } - if err := s.jobService.UpdateJobStatus(ctx, req.Id, jobStatus); err != nil { + if err != nil { return nil, status.Error(codes.Internal, "failed to update job status") } s.untrackJobAssignment(agentID, req.Id) @@ -165,6 +172,12 @@ func (s *Server) Log(ctx context.Context, req *proto.LogRequest) (*proto.Empty, return &proto.Empty{}, nil } -func (s *Server) Extend(context.Context, *proto.ExtendRequest) (*proto.Empty, error) { +func (s *Server) Extend(ctx context.Context, req *proto.ExtendRequest) (*proto.Empty, error) { + if _, _, ok := s.getAgentIDFromContext(ctx); !ok { + return nil, status.Error(codes.Unauthenticated, "invalid session") + } + if req != nil && req.Id != "" { + _ = s.jobService.RenewJobLease(ctx, req.Id) + } return &proto.Empty{}, nil } diff --git a/internal/transport/mqtt/notification_publisher.go b/internal/transport/mqtt/notification_publisher.go index 536d21b..d373036 100644 --- a/internal/transport/mqtt/notification_publisher.go +++ b/internal/transport/mqtt/notification_publisher.go @@ -25,7 +25,9 @@ func NewNotificationPublisher(client pahomqtt.Client, appLogger logger.Logger) s type serviceNotificationNoop struct{} -func (serviceNotificationNoop) PublishNotificationCreated(context.Context, *model.Notification) error { return nil } +func (serviceNotificationNoop) PublishNotificationCreated(context.Context, *model.Notification) error { + return nil +} func (p *notificationPublisher) PublishNotificationCreated(_ context.Context, notification *model.Notification) error { if p == nil || notification == nil { @@ -41,4 +43,3 @@ func (p *notificationPublisher) PublishNotificationCreated(_ context.Context, no func (p *notificationPublisher) notificationTopic(userID string) string { return fmt.Sprintf("%s/notifications/%s", p.prefix, userID) } - diff --git a/internal/workflow/agent/agent.go b/internal/workflow/agent/agent.go new file mode 100644 index 0000000..b566145 --- /dev/null +++ b/internal/workflow/agent/agent.go @@ -0,0 +1,576 @@ +package agent + +import ( + "context" + "encoding/json" + "fmt" + "log" + "os" + "strconv" + "strings" + "sync" + "time" + + grpcpkg "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/metadata" + proto "stream.api/internal/api/proto/agent/v1" +) + +type Agent struct { + client proto.WoodpeckerClient + authClient proto.WoodpeckerAuthClient + conn *grpcpkg.ClientConn + secret string + token string + capacity int + agentID string + docker *DockerExecutor + semaphore chan struct{} + wg sync.WaitGroup + activeJobs sync.Map + prevCPUTotal uint64 + prevCPUIdle uint64 +} + +type JobPayload struct { + Image string `json:"image"` + Commands []string `json:"commands"` + Environment map[string]string `json:"environment"` + Action string `json:"action"` +} + +func New(serverAddr, secret string, capacity int) (*Agent, error) { + conn, err := grpcpkg.NewClient(serverAddr, grpcpkg.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + return nil, fmt.Errorf("failed to connect to server: %w", err) + } + + docker, err := NewDockerExecutor() + if err != nil { + return nil, fmt.Errorf("failed to initialize docker executor: %w", err) + } + + return &Agent{ + client: proto.NewWoodpeckerClient(conn), + authClient: proto.NewWoodpeckerAuthClient(conn), + conn: conn, + secret: secret, + capacity: capacity, + docker: docker, + semaphore: make(chan struct{}, capacity), + }, nil +} + +func (a *Agent) Run(ctx context.Context) error { + defer func() { + unregisterCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + _ = a.unregister(unregisterCtx) + _ = a.conn.Close() + }() + + for { + if ctx.Err() != nil { + return ctx.Err() + } + + if err := a.registerWithRetry(ctx); err != nil { + log.Printf("Registration failed hard: %v", err) + return err + } + + log.Printf("Agent started/reconnected with ID: %s, Capacity: %d", a.agentID, a.capacity) + + sessionCtx, sessionCancel := context.WithCancel(ctx) + a.startBackgroundRoutines(sessionCtx) + err := a.streamJobs(sessionCtx) + sessionCancel() + a.wg.Wait() + + if ctx.Err() != nil { + return ctx.Err() + } + + log.Printf("Session ended: %v. Re-registering in 5s...", err) + select { + case <-ctx.Done(): + return ctx.Err() + case <-time.After(5 * time.Second): + } + } +} + +func (a *Agent) registerWithRetry(ctx context.Context) error { + backoff := 1 * time.Second + maxBackoff := 30 * time.Second + + for { + if err := a.register(ctx); err != nil { + log.Printf("Registration failed: %v. Retrying in %v...", err, backoff) + select { + case <-ctx.Done(): + return ctx.Err() + case <-time.After(backoff): + backoff *= 2 + if backoff > maxBackoff { + backoff = maxBackoff + } + continue + } + } + return nil + } +} + +func (a *Agent) startBackgroundRoutines(ctx context.Context) { + go a.cancelListener(ctx) + go a.submitStatusLoop(ctx) +} + +func (a *Agent) cancelListener(context.Context) {} + +func (a *Agent) register(ctx context.Context) error { + var savedID string + if os.Getenv("FORCE_NEW_ID") != "true" { + var err error + savedID, err = a.loadAgentID() + if err == nil && savedID != "" { + log.Printf("Loaded persisted Agent ID: %s", savedID) + a.agentID = savedID + } + } else { + log.Println("Forcing new Agent ID due to FORCE_NEW_ID=true") + } + + authResp, err := a.authClient.Auth(ctx, &proto.AuthRequest{ + AgentToken: a.secret, + AgentId: a.agentID, + Hostname: a.getHostFingerprint(), + }) + if err != nil { + return fmt.Errorf("auth failed: %w", err) + } + a.agentID = authResp.AgentId + + if a.agentID != savedID { + if err := a.saveAgentID(a.agentID); err != nil { + log.Printf("Failed to save agent ID: %v", err) + } else { + log.Printf("Persisted Agent ID: %s", a.agentID) + } + } + + a.token = authResp.AccessToken + mdCtx := metadata.AppendToOutgoingContext(ctx, "token", a.token) + hostname := a.getHostFingerprint() + + _, err = a.client.RegisterAgent(mdCtx, &proto.RegisterAgentRequest{ + Info: &proto.AgentInfo{ + Platform: "linux/amd64", + Backend: "ffmpeg", + Version: "stream-api-agent-v1", + Capacity: int32(a.capacity), + CustomLabels: map[string]string{ + "hostname": hostname, + }, + }, + }) + if err != nil { + return fmt.Errorf("registration failed: %w", err) + } + + return nil +} + +func (a *Agent) withToken(ctx context.Context) context.Context { + return metadata.AppendToOutgoingContext(ctx, "token", a.token) +} + +func (a *Agent) streamJobs(ctx context.Context) error { + mdCtx := a.withToken(ctx) + hostname, err := os.Hostname() + if err != nil { + hostname = "unknown-agent" + } + + stream, err := a.client.StreamJobs(mdCtx, &proto.StreamOptions{ + Filter: &proto.Filter{ + Labels: map[string]string{ + "hostname": hostname, + }, + }, + }) + if err != nil { + return fmt.Errorf("failed to start job stream: %w", err) + } + + log.Println("Connected to job stream") + + for { + select { + case a.semaphore <- struct{}{}: + case <-ctx.Done(): + return ctx.Err() + case <-stream.Context().Done(): + return stream.Context().Err() + } + + workflow, err := stream.Recv() + if err != nil { + <-a.semaphore + return fmt.Errorf("stream closed or error: %w", err) + } + + if workflow.Cancel { + <-a.semaphore + log.Printf("Received cancellation signal for job %s", workflow.Id) + if found := a.CancelJob(workflow.Id); found { + log.Printf("Job %s cancellation triggered", workflow.Id) + } else { + log.Printf("Job %s not found in active jobs", workflow.Id) + } + continue + } + + log.Printf("Received job from stream: %s (active: %d/%d)", workflow.Id, len(a.semaphore), a.capacity) + a.wg.Add(1) + go func(wf *proto.Workflow) { + defer a.wg.Done() + defer func() { <-a.semaphore }() + a.executeJob(ctx, wf) + }(workflow) + } +} + +func (a *Agent) submitStatusLoop(ctx context.Context) { + for { + if err := a.runStatusStream(ctx); err != nil { + log.Printf("Status stream error: %v. Retrying in 5s...", err) + select { + case <-ctx.Done(): + return + case <-time.After(5 * time.Second): + continue + } + } + return + } +} + +func (a *Agent) runStatusStream(ctx context.Context) error { + mdCtx := a.withToken(ctx) + stream, err := a.client.SubmitStatus(mdCtx) + if err != nil { + return err + } + + ticker := time.NewTicker(5 * time.Second) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + _, _ = stream.CloseAndRecv() + return ctx.Err() + case <-ticker.C: + cpu, ram := a.collectSystemResources() + data := fmt.Sprintf(`{"cpu": %.2f, "ram": %.2f}`, cpu, ram) + if err := stream.Send(&proto.StatusUpdate{Type: 5, Time: time.Now().Unix(), Data: []byte(data)}); err != nil { + return err + } + } + } +} + +func (a *Agent) collectSystemResources() (float64, float64) { + var memTotal, memAvailable uint64 + + data, err := os.ReadFile("/proc/meminfo") + if err == nil { + lines := strings.Split(string(data), "\n") + for _, line := range lines { + fields := strings.Fields(line) + if len(fields) < 2 { + continue + } + switch fields[0] { + case "MemTotal:": + memTotal, _ = strconv.ParseUint(fields[1], 10, 64) + case "MemAvailable:": + memAvailable, _ = strconv.ParseUint(fields[1], 10, 64) + } + } + } + + usedRAM := 0.0 + if memTotal > 0 { + usedRAM = float64(memTotal-memAvailable) / 1024.0 + } + + var cpuUsage float64 + data, err = os.ReadFile("/proc/stat") + if err == nil { + lines := strings.Split(string(data), "\n") + for _, line := range lines { + if !strings.HasPrefix(line, "cpu ") { + continue + } + fields := strings.Fields(line) + if len(fields) < 5 { + break + } + + var user, nice, system, idle, iowait, irq, softirq, steal uint64 + user, _ = strconv.ParseUint(fields[1], 10, 64) + nice, _ = strconv.ParseUint(fields[2], 10, 64) + system, _ = strconv.ParseUint(fields[3], 10, 64) + idle, _ = strconv.ParseUint(fields[4], 10, 64) + if len(fields) > 5 { + iowait, _ = strconv.ParseUint(fields[5], 10, 64) + } + if len(fields) > 6 { + irq, _ = strconv.ParseUint(fields[6], 10, 64) + } + if len(fields) > 7 { + softirq, _ = strconv.ParseUint(fields[7], 10, 64) + } + if len(fields) > 8 { + steal, _ = strconv.ParseUint(fields[8], 10, 64) + } + + currentIdle := idle + iowait + currentNonIdle := user + nice + system + irq + softirq + steal + currentTotal := currentIdle + currentNonIdle + totalDiff := currentTotal - a.prevCPUTotal + idleDiff := currentIdle - a.prevCPUIdle + + if totalDiff > 0 && a.prevCPUTotal > 0 { + cpuUsage = float64(totalDiff-idleDiff) / float64(totalDiff) * 100.0 + } + + a.prevCPUTotal = currentTotal + a.prevCPUIdle = currentIdle + break + } + } + + return cpuUsage, usedRAM +} + +func (a *Agent) executeJob(ctx context.Context, workflow *proto.Workflow) { + log.Printf("Executing job %s", workflow.Id) + + jobCtx, jobCancel := context.WithCancel(ctx) + defer jobCancel() + + if workflow.Timeout > 0 { + timeoutDuration := time.Duration(workflow.Timeout) * time.Second + log.Printf("Job %s has timeout of %v", workflow.Id, timeoutDuration) + jobCtx, jobCancel = context.WithTimeout(jobCtx, timeoutDuration) + defer jobCancel() + } + + a.activeJobs.Store(workflow.Id, jobCancel) + defer a.activeJobs.Delete(workflow.Id) + + var payload JobPayload + if err := json.Unmarshal(workflow.Payload, &payload); err != nil { + log.Printf("Failed to parse payload for job %s: %v", workflow.Id, err) + a.reportDone(ctx, workflow.Id, fmt.Sprintf("invalid payload: %v", err)) + return + } + + if payload.Action != "" { + log.Printf("Received system command: %s", payload.Action) + a.reportDone(ctx, workflow.Id, "") + + switch payload.Action { + case "restart": + log.Println("Restarting agent...") + os.Exit(0) + case "update": + log.Println("Updating agent...") + imageName := os.Getenv("AGENT_IMAGE") + if imageName == "" { + imageName = "stream-api-agent:latest" + } + + if err := a.docker.SelfUpdate(context.Background(), imageName, a.agentID); err != nil { + log.Printf("Update failed: %v", err) + } else { + os.Exit(0) + } + } + return + } + + mdCtx := a.withToken(ctx) + if _, err := a.client.Init(mdCtx, &proto.InitRequest{Id: workflow.Id}); err != nil { + log.Printf("Failed to init job %s: %v", workflow.Id, err) + return + } + + log.Printf("Running container with image: %s", payload.Image) + done := make(chan error, 1) + go a.extendLoop(jobCtx, workflow.Id) + go func() { + done <- a.docker.Run(jobCtx, payload.Image, payload.Commands, payload.Environment, func(line string) { + progress := -1.0 + if val, ok := parseProgress(line); ok { + progress = val + } + + entries := []*proto.LogEntry{{ + StepUuid: workflow.Id, + Data: []byte(line), + Time: time.Now().Unix(), + Type: 1, + }} + + if progress >= 0 { + entries = append(entries, &proto.LogEntry{ + StepUuid: workflow.Id, + Time: time.Now().Unix(), + Type: 4, + Data: []byte(fmt.Sprintf("%f", progress)), + }) + } + + if _, err := a.client.Log(mdCtx, &proto.LogRequest{LogEntries: entries}); err != nil { + log.Printf("Failed to send log for job %s: %v", workflow.Id, err) + } + }) + }() + + var err error + select { + case err = <-done: + case <-jobCtx.Done(): + if jobCtx.Err() == context.DeadlineExceeded { + err = fmt.Errorf("job timeout exceeded") + log.Printf("Job %s timed out", workflow.Id) + } else { + err = fmt.Errorf("job cancelled") + log.Printf("Job %s was cancelled", workflow.Id) + } + } + + if err != nil { + log.Printf("Job %s failed: %v", workflow.Id, err) + a.reportDone(ctx, workflow.Id, err.Error()) + } else { + log.Printf("Job %s succeeded", workflow.Id) + a.reportDone(ctx, workflow.Id, "") + } +} + +func (a *Agent) CancelJob(jobID string) bool { + if cancelFunc, ok := a.activeJobs.Load(jobID); ok { + log.Printf("Cancelling job %s", jobID) + cancelFunc.(context.CancelFunc)() + return true + } + return false +} + +func (a *Agent) extendLoop(ctx context.Context, jobID string) { + ticker := time.NewTicker(5 * time.Minute) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + mdCtx := a.withToken(ctx) + if _, err := a.client.Extend(mdCtx, &proto.ExtendRequest{Id: jobID}); err != nil { + log.Printf("Failed to extend lease for job %s: %v", jobID, err) + } + } + } +} + +func (a *Agent) reportDone(_ context.Context, id string, errStr string) { + state := &proto.WorkflowState{Finished: time.Now().Unix()} + if errStr != "" { + state.Error = errStr + } + + reportCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + mdCtx := a.withToken(reportCtx) + _, err := a.client.Done(mdCtx, &proto.DoneRequest{Id: id, State: state}) + if err != nil { + log.Printf("Failed to report Done for job %s: %v", id, err) + } +} + +func (a *Agent) unregister(ctx context.Context) error { + if a.token == "" { + return nil + } + + mdCtx := a.withToken(ctx) + _, err := a.client.UnregisterAgent(mdCtx, &proto.Empty{}) + if err != nil { + return fmt.Errorf("failed to unregister agent: %w", err) + } + return nil +} + +const ( + AgentIDFile = "/data/agent_id" + HostnameFile = "/host_hostname" +) + +type AgentIdentity struct { + ID string `json:"id"` + Fingerprint string `json:"fingerprint"` +} + +func (a *Agent) getHostFingerprint() string { + data, err := os.ReadFile(HostnameFile) + if err == nil { + return strings.TrimSpace(string(data)) + } + hostname, _ := os.Hostname() + return hostname +} + +func (a *Agent) loadAgentID() (string, error) { + data, err := os.ReadFile(AgentIDFile) + if err != nil { + return "", err + } + + var identity AgentIdentity + if err := json.Unmarshal(data, &identity); err == nil { + currentFP := a.getHostFingerprint() + if identity.Fingerprint != "" && identity.Fingerprint != currentFP { + log.Printf("Environment changed (Hostname mismatch: saved=%s, current=%s). Resetting Agent ID.", identity.Fingerprint, currentFP) + return "", fmt.Errorf("environment changed") + } + return identity.ID, nil + } + + id := strings.TrimSpace(string(data)) + if id == "" { + return "", fmt.Errorf("empty ID") + } + return id, nil +} + +func (a *Agent) saveAgentID(id string) error { + if err := os.MkdirAll("/data", 0755); err != nil { + return err + } + + identity := AgentIdentity{ID: id, Fingerprint: a.getHostFingerprint()} + data, err := json.Marshal(identity) + if err != nil { + return err + } + + return os.WriteFile(AgentIDFile, data, 0644) +} diff --git a/internal/workflow/agent/docker.go b/internal/workflow/agent/docker.go new file mode 100644 index 0000000..c5cd404 --- /dev/null +++ b/internal/workflow/agent/docker.go @@ -0,0 +1,154 @@ +package agent + +import ( + "bufio" + "context" + "fmt" + "io" + "log" + "os" + "strings" + "sync" + "time" + + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/image" + "github.com/docker/docker/client" +) + +type DockerExecutor struct { + cli *client.Client +} + +func NewDockerExecutor() (*DockerExecutor, error) { + cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) + if err != nil { + return nil, err + } + return &DockerExecutor{cli: cli}, nil +} + +func (d *DockerExecutor) Run(ctx context.Context, imageName string, commands []string, env map[string]string, logCallback func(string)) error { + reader, err := d.cli.ImagePull(ctx, imageName, image.PullOptions{}) + if err != nil { + log.Printf("Warning: Failed to pull image %s (might exist locally): %v", imageName, err) + } else { + _, _ = io.Copy(io.Discard, reader) + _ = reader.Close() + } + + envSlice := make([]string, 0, len(env)) + for k, v := range env { + envSlice = append(envSlice, fmt.Sprintf("%s=%s", k, v)) + } + + script := strings.Join(commands, " && ") + if len(commands) == 0 { + script = "echo 'No commands to execute'" + } + + cmd := []string{"/bin/sh", "-c", script} + + resp, err := d.cli.ContainerCreate(ctx, &container.Config{ + Image: imageName, + Entrypoint: cmd, + Cmd: nil, + Env: envSlice, + Tty: true, + }, nil, nil, nil, "") + if err != nil { + return fmt.Errorf("failed to create container: %w", err) + } + + containerID := resp.ID + defer d.cleanup(context.Background(), containerID) + + if err := d.cli.ContainerStart(ctx, containerID, container.StartOptions{}); err != nil { + return fmt.Errorf("failed to start container: %w", err) + } + + var wg sync.WaitGroup + + out, err := d.cli.ContainerLogs(ctx, containerID, container.LogsOptions{ + ShowStdout: true, + ShowStderr: true, + Follow: true, + }) + if err != nil { + log.Printf("Failed to get logs: %v", err) + } else { + wg.Add(1) + go func() { + defer wg.Done() + defer out.Close() + scanner := bufio.NewScanner(out) + for scanner.Scan() { + logCallback(scanner.Text()) + } + }() + } + + statusCh, errCh := d.cli.ContainerWait(ctx, containerID, container.WaitConditionNotRunning) + select { + case err := <-errCh: + if err != nil { + return fmt.Errorf("error waiting for container: %w", err) + } + case status := <-statusCh: + if status.StatusCode != 0 { + wg.Wait() + return fmt.Errorf("container exited with code %d", status.StatusCode) + } + } + + wg.Wait() + return nil +} + +func (d *DockerExecutor) cleanup(ctx context.Context, containerID string) { + _ = d.cli.ContainerRemove(ctx, containerID, container.RemoveOptions{Force: true}) +} + +func (d *DockerExecutor) SelfUpdate(ctx context.Context, imageTag string, agentID string) error { + log.Printf("Initiating self-update using Watchtower...") + + containerID, err := os.Hostname() + if err != nil { + return fmt.Errorf("failed to get hostname (container ID): %w", err) + } + log.Printf("Current Container ID: %s", containerID) + + watchtowerImage := "containrrr/watchtower:latest" + reader, err := d.cli.ImagePull(ctx, watchtowerImage, image.PullOptions{}) + if err != nil { + log.Printf("Failed to pull watchtower: %v", err) + return fmt.Errorf("failed to pull watchtower: %w", err) + } + _, _ = io.Copy(io.Discard, reader) + _ = reader.Close() + + hostSock := os.Getenv("HOST_DOCKER_SOCK") + if hostSock == "" { + hostSock = "/var/run/docker.sock" + } + + cmd := []string{"/watchtower", "--run-once", "--cleanup", "--debug", containerID} + + resp, err := d.cli.ContainerCreate(ctx, &container.Config{ + Image: watchtowerImage, + Cmd: cmd, + }, &container.HostConfig{ + Binds: []string{fmt.Sprintf("%s:/var/run/docker.sock", hostSock)}, + }, nil, nil, fmt.Sprintf("watchtower-updater-%s-%d", agentID, time.Now().Unix())) + if err != nil { + return fmt.Errorf("failed to create watchtower container: %w", err) + } + + if err := d.cli.ContainerStart(ctx, resp.ID, container.StartOptions{}); err != nil { + return fmt.Errorf("failed to start watchtower: %w", err) + } + + log.Printf("Watchtower started with ID: %s. Monitoring...", resp.ID) + time.Sleep(2 * time.Second) + return nil +} diff --git a/internal/workflow/agent/parser.go b/internal/workflow/agent/parser.go new file mode 100644 index 0000000..39d62b5 --- /dev/null +++ b/internal/workflow/agent/parser.go @@ -0,0 +1,26 @@ +package agent + +import ( + "regexp" + "strconv" + "strings" +) + +func parseProgress(line string) (float64, bool) { + if !strings.Contains(line, "out_time_us=") { + return 0, false + } + + re := regexp.MustCompile(`out_time_us=(\d+)`) + matches := re.FindStringSubmatch(line) + if len(matches) <= 1 { + return 0, false + } + + us, err := strconv.ParseInt(matches[1], 10, 64) + if err != nil { + return 0, false + } + + return float64(us) / 1000000.0, true +} diff --git a/internal/workflow/agent/parser_test.go b/internal/workflow/agent/parser_test.go new file mode 100644 index 0000000..6e3c9b9 --- /dev/null +++ b/internal/workflow/agent/parser_test.go @@ -0,0 +1,43 @@ +package agent + +import "testing" + +func TestParseProgress(t *testing.T) { + tests := []struct { + name string + line string + expected float64 + ok bool + }{ + { + name: "valid ffmpeg output", + line: "frame= 171 fps=0.0 q=-1.0 size= 1024kB time=00:00:06.84 bitrate=1225.6kbits/s speed=13.6x out_time_us=1234567", + expected: 1.234567, + ok: true, + }, + { + name: "line without out_time_us", + line: "frame= 171 fps=0.0 q=-1.0 size= 1024kB time=00:00:06.84", + expected: 0, + ok: false, + }, + { + name: "invalid out_time_us value", + line: "out_time_us=invalid", + expected: 0, + ok: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, ok := parseProgress(tt.line) + if ok != tt.ok { + t.Errorf("parseProgress() ok = %v, want %v", ok, tt.ok) + } + if got != tt.expected { + t.Errorf("parseProgress() got = %v, want %v", got, tt.expected) + } + }) + } +} diff --git a/internal/workflow/render/workflow.go b/internal/workflow/render/workflow.go index c32a8a2..f913b2a 100644 --- a/internal/workflow/render/workflow.go +++ b/internal/workflow/render/workflow.go @@ -28,6 +28,10 @@ type JobService interface { CreateJob(ctx context.Context, userID string, videoID string, name string, config []byte, priority int, timeLimit int64) (*model.Job, error) CancelJob(ctx context.Context, id string) error RetryJob(ctx context.Context, id string) (*model.Job, error) + ListDLQ(ctx context.Context, offset, limit int) ([]*dto.DLQEntry, int64, error) + GetDLQ(ctx context.Context, id string) (*dto.DLQEntry, error) + RetryDLQ(ctx context.Context, id string) (*model.Job, error) + RemoveDLQ(ctx context.Context, id string) error } type Workflow struct { @@ -187,6 +191,34 @@ func (w *Workflow) RetryJob(ctx context.Context, id string) (*model.Job, error) return w.jobService.RetryJob(ctx, id) } +func (w *Workflow) ListDLQ(ctx context.Context, offset, limit int) ([]*dto.DLQEntry, int64, error) { + if w == nil || w.jobService == nil { + return nil, 0, ErrJobServiceUnavailable + } + return w.jobService.ListDLQ(ctx, offset, limit) +} + +func (w *Workflow) GetDLQ(ctx context.Context, id string) (*dto.DLQEntry, error) { + if w == nil || w.jobService == nil { + return nil, ErrJobServiceUnavailable + } + return w.jobService.GetDLQ(ctx, id) +} + +func (w *Workflow) RetryDLQ(ctx context.Context, id string) (*model.Job, error) { + if w == nil || w.jobService == nil { + return nil, ErrJobServiceUnavailable + } + return w.jobService.RetryDLQ(ctx, id) +} + +func (w *Workflow) RemoveDLQ(ctx context.Context, id string) error { + if w == nil || w.jobService == nil { + return ErrJobServiceUnavailable + } + return w.jobService.RemoveDLQ(ctx, id) +} + func buildJobPayload(videoID, userID, videoURL, format string) ([]byte, error) { return json.Marshal(map[string]any{ "video_id": videoID, diff --git a/proto/app/v1/admin.proto b/proto/app/v1/admin.proto index 8e3e5e5..db82940 100644 --- a/proto/app/v1/admin.proto +++ b/proto/app/v1/admin.proto @@ -49,6 +49,10 @@ service Admin { rpc CreateAdminJob(CreateAdminJobRequest) returns (CreateAdminJobResponse); rpc CancelAdminJob(CancelAdminJobRequest) returns (CancelAdminJobResponse); rpc RetryAdminJob(RetryAdminJobRequest) returns (RetryAdminJobResponse); + rpc ListAdminDlqJobs(ListAdminDlqJobsRequest) returns (ListAdminDlqJobsResponse); + rpc GetAdminDlqJob(GetAdminDlqJobRequest) returns (GetAdminDlqJobResponse); + rpc RetryAdminDlqJob(RetryAdminDlqJobRequest) returns (RetryAdminDlqJobResponse); + rpc RemoveAdminDlqJob(RemoveAdminDlqJobRequest) returns (RemoveAdminDlqJobResponse); rpc ListAdminAgents(ListAdminAgentsRequest) returns (ListAdminAgentsResponse); rpc RestartAdminAgent(RestartAdminAgentRequest) returns (AdminAgentCommandResponse); rpc UpdateAdminAgent(UpdateAdminAgentRequest) returns (AdminAgentCommandResponse); @@ -533,6 +537,43 @@ message RetryAdminJobResponse { AdminJob job = 1; } +message ListAdminDlqJobsRequest { + int32 offset = 1; + int32 limit = 2; +} + +message ListAdminDlqJobsResponse { + repeated AdminDlqEntry items = 1; + int64 total = 2; + int32 offset = 3; + int32 limit = 4; +} + +message GetAdminDlqJobRequest { + string id = 1; +} + +message GetAdminDlqJobResponse { + AdminDlqEntry item = 1; +} + +message RetryAdminDlqJobRequest { + string id = 1; +} + +message RetryAdminDlqJobResponse { + AdminJob job = 1; +} + +message RemoveAdminDlqJobRequest { + string id = 1; +} + +message RemoveAdminDlqJobResponse { + string status = 1; + string job_id = 2; +} + message ListAdminAgentsRequest {} message ListAdminAgentsResponse { diff --git a/proto/app/v1/common.proto b/proto/app/v1/common.proto index a5293d9..617247e 100644 --- a/proto/app/v1/common.proto +++ b/proto/app/v1/common.proto @@ -392,3 +392,10 @@ message AdminAgent { google.protobuf.Timestamp created_at = 11; google.protobuf.Timestamp updated_at = 12; } + +message AdminDlqEntry { + AdminJob job = 1; + google.protobuf.Timestamp failure_time = 2; + string reason = 3; + int32 retry_count = 4; +} diff --git a/proto/app/v1/video_metadata.proto b/proto/app/v1/video_metadata.proto new file mode 100644 index 0000000..6f44947 --- /dev/null +++ b/proto/app/v1/video_metadata.proto @@ -0,0 +1,23 @@ +syntax = "proto3"; + +package stream.app.v1; + +option go_package = "stream.api/internal/gen/proto/app/v1;appv1"; + +import "app/v1/common.proto"; + +service VideoMetadata { + rpc GetVideoMetadata(GetVideoMetadataRequest) returns (GetVideoMetadataResponse); +} + +message GetVideoMetadataRequest { + string video_id = 1; +} + +message GetVideoMetadataResponse { + Video video = 1; + PlayerConfig default_player_config = 2; + AdTemplate ad_template = 3; + PopupAd active_popup_ad = 4; + repeated Domain domains = 5; +} diff --git a/MIGRATION_GUIDE.md b/script/MIGRATION_GUIDE.md similarity index 100% rename from MIGRATION_GUIDE.md rename to script/MIGRATION_GUIDE.md diff --git a/script/create_database.sql b/script/create_database.sql new file mode 100644 index 0000000..23928be --- /dev/null +++ b/script/create_database.sql @@ -0,0 +1,376 @@ +-- DROP SCHEMA public; + +CREATE SCHEMA public AUTHORIZATION pg_database_owner; + +COMMENT ON SCHEMA public IS 'standard public schema'; +-- public.ad_templates definition + +-- Drop table + +-- DROP TABLE public.ad_templates; + +CREATE TABLE public.ad_templates ( + id uuid NOT NULL, + user_id uuid NOT NULL, + "name" text NOT NULL, + description text NULL, + vast_tag_url text NOT NULL, + ad_format varchar(50) DEFAULT 'pre-roll'::character varying NOT NULL, + duration int8 NULL, + is_active bool DEFAULT true NOT NULL, + created_at timestamptz NULL, + updated_at timestamptz NULL, + is_default bool DEFAULT false NOT NULL, + "version" int8 DEFAULT 1 NOT NULL, + CONSTRAINT ad_templates_pkey PRIMARY KEY (id) +); +CREATE INDEX idx_ad_templates_user_id ON public.ad_templates USING btree (user_id); + + +-- public.domains definition + +-- Drop table + +-- DROP TABLE public.domains; + +CREATE TABLE public.domains ( + id uuid NOT NULL, + user_id uuid NOT NULL, + "name" text NOT NULL, + created_at timestamptz NULL, + updated_at timestamptz NULL, + "version" int8 DEFAULT 1 NOT NULL, + CONSTRAINT domains_pkey PRIMARY KEY (id) +); +CREATE INDEX idx_domains_user_id ON public.domains USING btree (user_id); + + +-- public.jobs definition + +-- Drop table + +-- DROP TABLE public.jobs; + +CREATE TABLE public.jobs ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + status text NULL, + priority int8 DEFAULT 0 NULL, + input_url text NULL, + output_url text NULL, + total_duration int8 NULL, + current_time int8 NULL, + progress numeric NULL, + agent_id int8 NULL, + logs text NULL, + config text NULL, + cancelled bool DEFAULT false NULL, + retry_count int8 DEFAULT 0 NULL, + max_retries int8 DEFAULT 3 NULL, + created_at timestamptz NULL, + updated_at timestamptz NULL, + "version" int8 NULL, + video_id uuid NULL, + user_id uuid NULL, + time_limit int8 DEFAULT 3600000 NULL, + CONSTRAINT jobs_pkey PRIMARY KEY (id) +); +CREATE INDEX idx_jobs_priority ON public.jobs USING btree (priority); + + +-- public.notifications definition + +-- Drop table + +-- DROP TABLE public.notifications; + +CREATE TABLE public.notifications ( + id uuid NOT NULL, + user_id uuid NOT NULL, + "type" varchar(50) NOT NULL, + title text NOT NULL, + message text NOT NULL, + metadata text NULL, + action_url text NULL, + action_label text NULL, + is_read bool DEFAULT false NOT NULL, + created_at timestamptz NULL, + updated_at timestamptz NULL, + "version" int8 DEFAULT 1 NOT NULL, + CONSTRAINT notifications_pkey PRIMARY KEY (id) +); +CREATE INDEX idx_notifications_user_id ON public.notifications USING btree (user_id); + + +-- public."plan" definition + +-- Drop table + +-- DROP TABLE public."plan"; + +CREATE TABLE public."plan" ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + "name" text NOT NULL, + description text NULL, + price numeric(65, 30) NOT NULL, + "cycle" varchar(20) NOT NULL, + storage_limit int8 NOT NULL, + upload_limit int4 NOT NULL, + duration_limit int4 NOT NULL, + quality_limit text NOT NULL, + features _text NULL, + is_active bool DEFAULT true NOT NULL, + "version" int8 DEFAULT 1 NOT NULL, + CONSTRAINT plan_pkey PRIMARY KEY (id) +); + + +-- public.plan_subscriptions definition + +-- Drop table + +-- DROP TABLE public.plan_subscriptions; + +CREATE TABLE public.plan_subscriptions ( + id uuid NOT NULL, + user_id uuid NOT NULL, + payment_id uuid NOT NULL, + plan_id uuid NOT NULL, + term_months int4 NOT NULL, + payment_method varchar(20) NOT NULL, + wallet_amount numeric(65, 30) NOT NULL, + topup_amount numeric(65, 30) NOT NULL, + started_at timestamptz NOT NULL, + expires_at timestamptz NOT NULL, + reminder_7d_sent_at timestamptz NULL, + reminder_3d_sent_at timestamptz NULL, + reminder_1d_sent_at timestamptz NULL, + created_at timestamptz NULL, + updated_at timestamptz NULL, + "version" int8 DEFAULT 1 NOT NULL, + CONSTRAINT plan_subscriptions_pkey PRIMARY KEY (id) +); +CREATE INDEX idx_plan_subscriptions_expires_at ON public.plan_subscriptions USING btree (expires_at); +CREATE INDEX idx_plan_subscriptions_payment_id ON public.plan_subscriptions USING btree (payment_id); +CREATE INDEX idx_plan_subscriptions_plan_id ON public.plan_subscriptions USING btree (plan_id); +CREATE INDEX idx_plan_subscriptions_user_id ON public.plan_subscriptions USING btree (user_id); + + +-- public.user_preferences definition + +-- Drop table + +-- DROP TABLE public.user_preferences; + +CREATE TABLE public.user_preferences ( + user_id uuid NOT NULL, + "language" text DEFAULT 'en'::text NOT NULL, + locale text DEFAULT 'en'::text NOT NULL, + email_notifications bool DEFAULT true NOT NULL, + push_notifications bool DEFAULT true NOT NULL, + marketing_notifications bool DEFAULT false NOT NULL, + telegram_notifications bool DEFAULT false NOT NULL, + created_at timestamptz NULL, + updated_at timestamptz NULL, + "version" int8 DEFAULT 1 NOT NULL, + CONSTRAINT user_preferences_pkey PRIMARY KEY (user_id) +); + + +-- public.wallet_transactions definition + +-- Drop table + +-- DROP TABLE public.wallet_transactions; + +CREATE TABLE public.wallet_transactions ( + id uuid NOT NULL, + user_id uuid NOT NULL, + "type" varchar(50) NOT NULL, + amount numeric(65, 30) NOT NULL, + currency text DEFAULT 'USD'::text NOT NULL, + note text NULL, + created_at timestamptz NULL, + updated_at timestamptz NULL, + payment_id uuid NULL, + plan_id uuid NULL, + term_months int4 NULL, + "version" int8 DEFAULT 1 NOT NULL, + CONSTRAINT wallet_transactions_pkey PRIMARY KEY (id) +); +CREATE INDEX idx_wallet_transactions_payment_id ON public.wallet_transactions USING btree (payment_id); +CREATE INDEX idx_wallet_transactions_plan_id ON public.wallet_transactions USING btree (plan_id); +CREATE INDEX idx_wallet_transactions_user_id ON public.wallet_transactions USING btree (user_id); + + +-- public."user" definition + +-- Drop table + +-- DROP TABLE public."user"; + +CREATE TABLE public."user" ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + email text NOT NULL, + "password" text NULL, + username text NULL, + avatar text NULL, + "role" varchar(20) DEFAULT 'USER'::character varying NOT NULL, + google_id text NULL, + storage_used int8 DEFAULT 0 NOT NULL, + plan_id uuid NULL, + created_at timestamp(3) DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at timestamp(3) NOT NULL, + "version" int8 DEFAULT 1 NOT NULL, + telegram_id varchar NULL, + referred_by_user_id uuid NULL, + referral_eligible bool DEFAULT true NOT NULL, + referral_reward_bps int4 NULL, + referral_reward_granted_at timestamptz NULL, + referral_reward_payment_id uuid NULL, + referral_reward_amount numeric(65, 30) NULL, + CONSTRAINT user_pkey PRIMARY KEY (id), + CONSTRAINT user_plan_id_fkey FOREIGN KEY (plan_id) REFERENCES public."plan"(id) ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT user_referred_by_user_id_fkey FOREIGN KEY (referred_by_user_id) REFERENCES public."user"(id) ON DELETE SET NULL +); +CREATE INDEX idx_user_referred_by_user_id ON public."user" USING btree (referred_by_user_id); +CREATE UNIQUE INDEX user_email_key ON public."user" USING btree (email); +CREATE UNIQUE INDEX user_google_id_key ON public."user" USING btree (google_id); + + +-- public.video definition + +-- Drop table + +-- DROP TABLE public.video; + +CREATE TABLE public.video ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + "name" text NOT NULL, + title text NOT NULL, + description text NULL, + url text NOT NULL, + thumbnail text NULL, + hls_token text NULL, + hls_path text NULL, + duration int4 NOT NULL, + "size" int8 NOT NULL, + storage_type varchar(20) DEFAULT 'tiktok_avatar'::character varying NOT NULL, + format text NOT NULL, + status varchar(20) DEFAULT 'PUBLIC'::character varying NOT NULL, + processing_status varchar(20) DEFAULT 'PENDING'::character varying NOT NULL, + "views" int4 DEFAULT 0 NOT NULL, + user_id uuid NOT NULL, + created_at timestamp(3) DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at timestamp(3) NOT NULL, + "version" int8 DEFAULT 1 NOT NULL, + ad_id uuid NULL, + metadata jsonb NULL, + CONSTRAINT video_pkey PRIMARY KEY (id), + CONSTRAINT video_user_id_fkey FOREIGN KEY (user_id) REFERENCES public."user"(id) ON DELETE RESTRICT ON UPDATE CASCADE +); + + +-- public.payment definition + +-- Drop table + +-- DROP TABLE public.payment; + +CREATE TABLE public.payment ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + user_id uuid NOT NULL, + plan_id uuid NULL, + amount numeric(65, 30) NOT NULL, + currency text DEFAULT 'USD'::text NOT NULL, + status varchar(20) DEFAULT 'PENDING'::character varying NOT NULL, + provider varchar(20) DEFAULT 'STRIPE'::character varying NOT NULL, + transaction_id text NULL, + created_at timestamp(3) DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at timestamp(3) NOT NULL, + "version" int8 DEFAULT 1 NOT NULL, + CONSTRAINT payment_pkey PRIMARY KEY (id), + CONSTRAINT payment_plan_id_fkey FOREIGN KEY (plan_id) REFERENCES public."plan"(id) ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT payment_user_id_fkey FOREIGN KEY (user_id) REFERENCES public."user"(id) ON DELETE RESTRICT ON UPDATE CASCADE +); + + +-- public.player_configs definition + +-- Drop table + +-- DROP TABLE public.player_configs; + +CREATE TABLE public.player_configs ( + id uuid DEFAULT gen_random_uuid() NOT NULL, + user_id uuid NOT NULL, + "name" text NOT NULL, + description text NULL, + autoplay bool DEFAULT false NOT NULL, + "loop" bool DEFAULT false NOT NULL, + muted bool DEFAULT false NOT NULL, + show_controls bool DEFAULT true NOT NULL, + pip bool DEFAULT true NOT NULL, + airplay bool DEFAULT true NOT NULL, + chromecast bool DEFAULT true NOT NULL, + is_active bool DEFAULT true NOT NULL, + is_default bool DEFAULT false NOT NULL, + created_at timestamp(3) DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at timestamp(3) NOT NULL, + "version" int8 DEFAULT 1 NOT NULL, + encrytion_m3u8 bool DEFAULT true NOT NULL, + logo_url varchar(500) NULL, + CONSTRAINT player_configs_pkey PRIMARY KEY (id), + CONSTRAINT player_configs_url_check CHECK (((logo_url)::text ~* '^https?://'::text)), + CONSTRAINT player_configs_user_id_fkey FOREIGN KEY (user_id) REFERENCES public."user"(id) ON DELETE CASCADE +); +CREATE INDEX idx_player_configs_is_default ON public.player_configs USING btree (is_default); +CREATE UNIQUE INDEX idx_player_configs_one_default_per_user ON public.player_configs USING btree (user_id) WHERE (is_default = true); +CREATE INDEX idx_player_configs_user_default ON public.player_configs USING btree (user_id, is_default); +CREATE INDEX idx_player_configs_user_id ON public.player_configs USING btree (user_id); + +-- Table Triggers + +create trigger trg_update_player_configs before +update + on + public.player_configs for each row execute function update_player_configs_updated_at(); + + +-- public.popup_ads definition + +-- Drop table + +-- DROP TABLE public.popup_ads; + +CREATE TABLE public.popup_ads ( + id uuid NOT NULL, + user_id uuid NOT NULL, + "type" varchar(20) NOT NULL, + "label" text NOT NULL, + value text NOT NULL, + is_active bool DEFAULT true NOT NULL, + max_triggers_per_session int4 DEFAULT 3 NOT NULL, + created_at timestamptz DEFAULT CURRENT_TIMESTAMP NULL, + updated_at timestamptz NULL, + "version" int8 DEFAULT 1 NOT NULL, + CONSTRAINT popup_ads_pkey PRIMARY KEY (id), + CONSTRAINT popup_ads_user_id_fkey FOREIGN KEY (user_id) REFERENCES public."user"(id) ON DELETE CASCADE +); +CREATE INDEX idx_popup_ads_user_active ON public.popup_ads USING btree (user_id, is_active); +CREATE INDEX idx_popup_ads_user_id ON public.popup_ads USING btree (user_id); + + + +-- DROP FUNCTION public.update_player_configs_updated_at(); + +CREATE OR REPLACE FUNCTION public.update_player_configs_updated_at() + RETURNS trigger + LANGUAGE plpgsql +AS $function$ +BEGIN + NEW.updated_at = CURRENT_TIMESTAMP; + NEW.version = OLD.version + 1; + RETURN NEW; +END; +$function$ +; \ No newline at end of file diff --git a/full_player_configs_migration.sql b/script/full_player_configs_migration.sql similarity index 100% rename from full_player_configs_migration.sql rename to script/full_player_configs_migration.sql diff --git a/install_player_configs.sql b/script/install_player_configs.sql similarity index 100% rename from install_player_configs.sql rename to script/install_player_configs.sql diff --git a/migrate_player_configs.sh b/script/migrate_player_configs.sh similarity index 100% rename from migrate_player_configs.sh rename to script/migrate_player_configs.sh diff --git a/run_migration.sh b/script/run_migration.sh similarity index 100% rename from run_migration.sh rename to script/run_migration.sh