update cicd

This commit is contained in:
2026-04-02 11:01:30 +00:00
parent 863a0ea2f6
commit 5a7f29c116
54 changed files with 4298 additions and 473 deletions

View File

@@ -0,0 +1,7 @@
{
"permissions": {
"allow": [
"Bash(go test:*)"
]
}
}

View File

@@ -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

View File

@@ -3,14 +3,14 @@ FROM golang:1.25.6-alpine AS builder
WORKDIR /app WORKDIR /app
COPY go.mod go.sum ./ COPY go.mod go.sum ./
RUN go mod download RUN go mod download
COPY . . 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 FROM scratch
COPY --from=builder /app/main /main COPY --from=builder /app/server /server
EXPOSE 8080 EXPOSE 9000
ENTRYPOINT ["/main"] ENTRYPOINT ["/server"]

16
Dockerfile.agent Normal file
View File

@@ -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"]

54
cmd/agent/main.go Normal file
View File

@@ -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)
}
}

View File

@@ -45,7 +45,10 @@ func main() {
// 4. Initialize Components // 4. Initialize Components
appLogger := logger.NewLogger(cfg.Server.Mode) 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 { if err != nil {
log.Fatalf("Failed to setup gRPC runtime module: %v", err) log.Fatalf("Failed to setup gRPC runtime module: %v", err)
} }

View File

@@ -1 +0,0 @@
package worker

View File

@@ -1,5 +1,4 @@
server: server:
port: "8080"
grpc_port: "9000" grpc_port: "9000"
mode: "debug" # debug or release mode: "debug" # debug or release
@@ -11,28 +10,15 @@ redis:
password: "" password: ""
db: 0 db: 0
jwt:
secret: "your_super_secret_jwt_key"
google: google:
client_id: "your_google_client_id" client_id: "your_google_client_id"
client_secret: "your_google_client_secret" client_secret: "your_google_client_secret"
redirect_url: "http://localhost:8080/auth/google/callback" redirect_url: "http://localhost:8080/auth/google/callback"
state_ttl_minutes: 10 state_ttl_minutes: 10
frontend:
base_url: "http://localhost:5173"
google_auth_finalize_path: "/auth/google/finalize"
internal: internal:
marker: "your_shared_internal_auth_marker" marker: "your_shared_internal_auth_marker"
cors:
allow_origins:
- "http://localhost:5173"
- "http://localhost:8080"
- "http://localhost:8081"
email: email:
from: "no-reply@picpic.com" from: "no-reply@picpic.com"
@@ -43,8 +29,11 @@ aws:
secret_key: "your_secret_key" secret_key: "your_secret_key"
render: render:
agent_secret: "your_render_agent_secret"
enable_metrics: true enable_metrics: true
enable_tracing: false enable_tracing: false
otlp_endpoint: "" otlp_endpoint: ""
service_name: "stream-api-render" 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

View File

@@ -10,16 +10,19 @@ redis:
password: "pass123" password: "pass123"
db: 3 db: 3
jwt:
secret: "your_super_secret_jwt_key"
google: google:
client_id: "your_google_client_id" client_id: "781933579930-avgrrvdj26ajqujs0snajk62jgch2jl5.apps.googleusercontent.com"
client_secret: "your_google_client_secret" client_secret: "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"
redirect_url: "http://localhost:8080/auth/google/callback" redirect_url: "https://hlstiktok.com/auth/google/callback"
email: email:
from: "no-reply@picpic.com" from: "no-reply@picpic.com"
internal: internal:
marker: "your-secret-marker" marker: "your-secret-marker"
aws:
region: "us-east-1"
bucket: "your-bucket-name"
access_key: "your_access_key"
secret_key: "your_secret_key"

29
go.mod
View File

@@ -7,6 +7,7 @@ require (
github.com/aws/aws-sdk-go-v2/config v1.32.7 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/credentials v1.19.7
github.com/aws/aws-sdk-go-v2/service/s3 v1.95.1 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/eclipse/paho.mqtt.golang v1.5.1
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/lib/pq v1.11.2 github.com/lib/pq v1.11.2
@@ -26,10 +27,33 @@ require (
) )
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/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/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/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 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/libc v1.70.0 // indirect
modernc.org/mathutil v1.7.1 // indirect modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect modernc.org/memory v1.11.0 // indirect
@@ -67,7 +91,6 @@ require (
github.com/jinzhu/now v1.1.5 // indirect github.com/jinzhu/now v1.1.5 // indirect
github.com/mattn/go-sqlite3 v1.14.22 // indirect github.com/mattn/go-sqlite3 v1.14.22 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // 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/sagikazarmark/locafero v0.11.0 // indirect
github.com/sony/gobreaker v1.0.0 github.com/sony/gobreaker v1.0.0
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect 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.uber.org/multierr v1.10.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/mod v0.33.0 // 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/sync v0.19.0 // indirect
golang.org/x/sys v0.42.0 // indirect golang.org/x/sys v0.42.0 // indirect
golang.org/x/text v0.34.0 // indirect golang.org/x/text v0.34.0 // indirect
golang.org/x/tools v0.42.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/datatypes v1.2.4
gorm.io/driver/mysql v1.5.7 // indirect gorm.io/driver/mysql v1.5.7 // indirect
gorm.io/hints v1.1.0 // indirect gorm.io/hints v1.1.0 // indirect

112
go.sum
View File

@@ -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= 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 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= 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 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 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= 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/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 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= 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 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 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.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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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 h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 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 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= 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 h1:/VSOv3oDLlpqR2Epjn1Q7b2bSTplJIeV2ISgCl2W7nE=
github.com/eclipse/paho.mqtt.golang v1.5.1/go.mod h1:1/yJCneuyOoCOzKSsOTUc0AJfpsItBGWvYpBLimhArU= 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 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 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 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= 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 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2 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-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 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= 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 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= github.com/golang-sql/sqlexp v0.1.0 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/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 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 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 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= 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.2/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 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 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 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/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 h1:Fto83dMZPnYv1Zwx5vHHxpNraeEaUlQ/hhHLgZiaenE=
github.com/microsoft/go-mssqldb v0.17.0/go.mod h1:OkoNGhGEs8EZqchVTtochlXruEhEOaO4S0d2sB5aeGQ= 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 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= 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 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= 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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 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= 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/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 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= 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 h1:feX5fGGXSl3dYd4aHZItw+FpHLvvoaqkawKjVNiFMNQ=
github.com/sony/gobreaker v1.0.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= github.com/sony/gobreaker v1.0.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= 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 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= 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/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.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.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 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 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 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= 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 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= 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/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o=
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg=
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho=
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc=
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 h1:THuZiwpQZuHPul65w4WcwEnkX2QIuMT+UFoOrygtoJw=
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0/go.mod h1:J2pvYM5NGHofZ2/Ru6zw/TNWnEQp5crgyDeSrYpXkAw=
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.42.0 h1:uLXP+3mghfMf7XmV4PkGfFhFKuNWoCvvx5wP/wOXo0o=
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.42.0/go.mod h1:v0Tj04armyT59mnURNUJf7RCKcKzq+lgJs6QSjHjaTc=
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4=
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= 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 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= 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.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 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= 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 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= 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 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= 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.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= 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 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= 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 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= 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.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 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= 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 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= 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 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= 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 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= 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/api v0.0.0-20260209200024-4cfbd4190f57 h1:JLQynH/LBHfCTSbDWl+py8C+Rg/k1OVH3xfcaiANuF0=
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/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 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU=
google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= 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/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 h1:F4b85TenghUeITqe3+epPSUtHH7RIk3fXr5l83DF8Pc=
gorm.io/plugin/dbresolver v1.6.2/go.mod h1:tctw63jdrOezFR9HmrKnPkmig3m5Edem9fdxk9bQSzM= 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 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= 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= modernc.org/ccgo/v4 v4.32.0 h1:hjG66bI/kqIPX1b2yT6fr/jt+QedtP2fqojG2VrFuVw=

View File

@@ -3,7 +3,9 @@ package redis
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"strings"
"time" "time"
goredis "github.com/redis/go-redis/v9" goredis "github.com/redis/go-redis/v9"
@@ -12,14 +14,24 @@ import (
) )
const ( const (
JobQueueKey = "render:jobs:queue" JobQueueKey = "render:jobs:queue:v2"
LogChannel = "render:jobs:logs" JobInflightKey = "render:jobs:inflight"
ResourceChannel = "render:agents:resources" JobInflightMetaKey = "render:jobs:inflight:meta"
JobUpdateChannel = "render:jobs:updates" 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 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) { func NewAdapter(addr, password string, db int) (*RedisAdapter, error) {
client := goredis.NewClient(&goredis.Options{Addr: addr, Password: password, DB: db}) client := goredis.NewClient(&goredis.Options{Addr: addr, Password: password, DB: db})
if err := client.Ping(context.Background()).Err(); err != nil { 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) Client() *goredis.Client { return r.client }
func (r *RedisAdapter) Enqueue(ctx context.Context, job *model.Job) error { 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 { if err != nil {
return err return err
} }
timestamp := time.Now().UnixNano() score := float64((-priority * 1_000_000_000_000) + seq)
score := float64(-(int64(*job.Priority) * 1000000000) - timestamp) jobID := strings.TrimSpace(job.ID)
return r.client.ZAdd(ctx, JobQueueKey, goredis.Z{Score: score, Member: data}).Err() 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) { 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 { select {
case <-ctx.Done(): case <-ctx.Done():
return nil, ctx.Err() return nil, ctx.Err()
case <-time.After(time.Second): case <-time.After(defaultQueuePoll):
continue continue
} }
} }
var raw []byte jobID := fmt.Sprintf("%v", res[0].Member)
switch member := res[0].Member.(type) { meta, err := json.Marshal(inflightMeta{ReadyScore: res[0].Score, ClaimedAt: time.Now().Unix()})
case string: if err != nil {
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 {
return nil, err 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 { 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}) payload, err := json.Marshal(dto.LogEntry{JobID: jobID, Line: logLine, Progress: progress})
if err != nil { if err != nil {
@@ -173,6 +228,7 @@ func (r *RedisAdapter) SubscribeJobUpdates(ctx context.Context) (<-chan string,
}() }()
return ch, nil return ch, nil
} }
func (c *RedisAdapter) Set(ctx context.Context, key string, value interface{}, expiration time.Duration) error { func (c *RedisAdapter) Set(ctx context.Context, key string, value interface{}, expiration time.Duration) error {
return c.client.Set(ctx, key, value, expiration).Err() return c.client.Set(ctx, key, value, expiration).Err()
} }

View File

@@ -12,8 +12,8 @@ import (
) )
const ( const (
dlqKey = "picpic:dlq" dlqKey = "render:jobs:dlq"
dlqMetaPrefix = "picpic:dlq:meta:" dlqMetaPrefix = "render:jobs:dlq:meta:"
) )
type DeadLetterQueue struct { type DeadLetterQueue struct {
@@ -33,11 +33,16 @@ func NewDeadLetterQueue(client *redis.Client) *DeadLetterQueue {
// Add adds a failed job to the DLQ // Add adds a failed job to the DLQ
func (dlq *DeadLetterQueue) Add(ctx context.Context, job *model.Job, reason string) error { 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{ entry := DLQEntry{
Job: job, Job: job,
FailureTime: time.Now(), FailureTime: time.Now(),
Reason: reason, Reason: reason,
RetryCount: *job.RetryCount, RetryCount: retryCount,
} }
data, err := json.Marshal(entry) data, err := json.Marshal(entry)

View File

@@ -4803,6 +4803,398 @@ func (x *RetryAdminJobResponse) GetJob() *AdminJob {
return nil 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 { type ListAdminAgentsRequest struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
@@ -4811,7 +5203,7 @@ type ListAdminAgentsRequest struct {
func (x *ListAdminAgentsRequest) Reset() { func (x *ListAdminAgentsRequest) Reset() {
*x = ListAdminAgentsRequest{} *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 := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -4823,7 +5215,7 @@ func (x *ListAdminAgentsRequest) String() string {
func (*ListAdminAgentsRequest) ProtoMessage() {} func (*ListAdminAgentsRequest) ProtoMessage() {}
func (x *ListAdminAgentsRequest) ProtoReflect() protoreflect.Message { 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 { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -4836,7 +5228,7 @@ func (x *ListAdminAgentsRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use ListAdminAgentsRequest.ProtoReflect.Descriptor instead. // Deprecated: Use ListAdminAgentsRequest.ProtoReflect.Descriptor instead.
func (*ListAdminAgentsRequest) Descriptor() ([]byte, []int) { 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 { type ListAdminAgentsResponse struct {
@@ -4848,7 +5240,7 @@ type ListAdminAgentsResponse struct {
func (x *ListAdminAgentsResponse) Reset() { func (x *ListAdminAgentsResponse) Reset() {
*x = ListAdminAgentsResponse{} *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 := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -4860,7 +5252,7 @@ func (x *ListAdminAgentsResponse) String() string {
func (*ListAdminAgentsResponse) ProtoMessage() {} func (*ListAdminAgentsResponse) ProtoMessage() {}
func (x *ListAdminAgentsResponse) ProtoReflect() protoreflect.Message { 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 { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -4873,7 +5265,7 @@ func (x *ListAdminAgentsResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use ListAdminAgentsResponse.ProtoReflect.Descriptor instead. // Deprecated: Use ListAdminAgentsResponse.ProtoReflect.Descriptor instead.
func (*ListAdminAgentsResponse) Descriptor() ([]byte, []int) { 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 { func (x *ListAdminAgentsResponse) GetAgents() []*AdminAgent {
@@ -4892,7 +5284,7 @@ type RestartAdminAgentRequest struct {
func (x *RestartAdminAgentRequest) Reset() { func (x *RestartAdminAgentRequest) Reset() {
*x = RestartAdminAgentRequest{} *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 := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -4904,7 +5296,7 @@ func (x *RestartAdminAgentRequest) String() string {
func (*RestartAdminAgentRequest) ProtoMessage() {} func (*RestartAdminAgentRequest) ProtoMessage() {}
func (x *RestartAdminAgentRequest) ProtoReflect() protoreflect.Message { 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 { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -4917,7 +5309,7 @@ func (x *RestartAdminAgentRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use RestartAdminAgentRequest.ProtoReflect.Descriptor instead. // Deprecated: Use RestartAdminAgentRequest.ProtoReflect.Descriptor instead.
func (*RestartAdminAgentRequest) Descriptor() ([]byte, []int) { 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 { func (x *RestartAdminAgentRequest) GetId() string {
@@ -4936,7 +5328,7 @@ type UpdateAdminAgentRequest struct {
func (x *UpdateAdminAgentRequest) Reset() { func (x *UpdateAdminAgentRequest) Reset() {
*x = UpdateAdminAgentRequest{} *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 := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -4948,7 +5340,7 @@ func (x *UpdateAdminAgentRequest) String() string {
func (*UpdateAdminAgentRequest) ProtoMessage() {} func (*UpdateAdminAgentRequest) ProtoMessage() {}
func (x *UpdateAdminAgentRequest) ProtoReflect() protoreflect.Message { 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 { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -4961,7 +5353,7 @@ func (x *UpdateAdminAgentRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use UpdateAdminAgentRequest.ProtoReflect.Descriptor instead. // Deprecated: Use UpdateAdminAgentRequest.ProtoReflect.Descriptor instead.
func (*UpdateAdminAgentRequest) Descriptor() ([]byte, []int) { 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 { func (x *UpdateAdminAgentRequest) GetId() string {
@@ -4980,7 +5372,7 @@ type AdminAgentCommandResponse struct {
func (x *AdminAgentCommandResponse) Reset() { func (x *AdminAgentCommandResponse) Reset() {
*x = AdminAgentCommandResponse{} *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 := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -4992,7 +5384,7 @@ func (x *AdminAgentCommandResponse) String() string {
func (*AdminAgentCommandResponse) ProtoMessage() {} func (*AdminAgentCommandResponse) ProtoMessage() {}
func (x *AdminAgentCommandResponse) ProtoReflect() protoreflect.Message { 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 { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -5005,7 +5397,7 @@ func (x *AdminAgentCommandResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use AdminAgentCommandResponse.ProtoReflect.Descriptor instead. // Deprecated: Use AdminAgentCommandResponse.ProtoReflect.Descriptor instead.
func (*AdminAgentCommandResponse) Descriptor() ([]byte, []int) { 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 { func (x *AdminAgentCommandResponse) GetStatus() string {
@@ -5448,7 +5840,28 @@ const file_app_v1_admin_proto_rawDesc = "" +
"\x14RetryAdminJobRequest\x12\x0e\n" + "\x14RetryAdminJobRequest\x12\x0e\n" +
"\x02id\x18\x01 \x01(\tR\x02id\"B\n" + "\x02id\x18\x01 \x01(\tR\x02id\"B\n" +
"\x15RetryAdminJobResponse\x12)\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" + "\x16ListAdminAgentsRequest\"L\n" +
"\x17ListAdminAgentsResponse\x121\n" + "\x17ListAdminAgentsResponse\x121\n" +
"\x06agents\x18\x01 \x03(\v2\x19.stream.app.v1.AdminAgentR\x06agents\"*\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" + "\x17UpdateAdminAgentRequest\x12\x0e\n" +
"\x02id\x18\x01 \x01(\tR\x02id\"3\n" + "\x02id\x18\x01 \x01(\tR\x02id\"3\n" +
"\x19AdminAgentCommandResponse\x12\x16\n" + "\x19AdminAgentCommandResponse\x12\x16\n" +
"\x06status\x18\x01 \x01(\tR\x06status2\x9d$\n" + "\x06status\x18\x01 \x01(\tR\x06status2\xae'\n" +
"\x05Admin\x12f\n" + "\x05Admin\x12f\n" +
"\x11GetAdminDashboard\x12'.stream.app.v1.GetAdminDashboardRequest\x1a(.stream.app.v1.GetAdminDashboardResponse\x12]\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" + "\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" + "\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" + "\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" + "\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" + "\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" + "\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" "\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 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{ var file_app_v1_admin_proto_goTypes = []any{
(*GetAdminDashboardRequest)(nil), // 0: stream.app.v1.GetAdminDashboardRequest (*GetAdminDashboardRequest)(nil), // 0: stream.app.v1.GetAdminDashboardRequest
(*GetAdminDashboardResponse)(nil), // 1: stream.app.v1.GetAdminDashboardResponse (*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 (*CancelAdminJobResponse)(nil), // 76: stream.app.v1.CancelAdminJobResponse
(*RetryAdminJobRequest)(nil), // 77: stream.app.v1.RetryAdminJobRequest (*RetryAdminJobRequest)(nil), // 77: stream.app.v1.RetryAdminJobRequest
(*RetryAdminJobResponse)(nil), // 78: stream.app.v1.RetryAdminJobResponse (*RetryAdminJobResponse)(nil), // 78: stream.app.v1.RetryAdminJobResponse
(*ListAdminAgentsRequest)(nil), // 79: stream.app.v1.ListAdminAgentsRequest (*ListAdminDlqJobsRequest)(nil), // 79: stream.app.v1.ListAdminDlqJobsRequest
(*ListAdminAgentsResponse)(nil), // 80: stream.app.v1.ListAdminAgentsResponse (*ListAdminDlqJobsResponse)(nil), // 80: stream.app.v1.ListAdminDlqJobsResponse
(*RestartAdminAgentRequest)(nil), // 81: stream.app.v1.RestartAdminAgentRequest (*GetAdminDlqJobRequest)(nil), // 81: stream.app.v1.GetAdminDlqJobRequest
(*UpdateAdminAgentRequest)(nil), // 82: stream.app.v1.UpdateAdminAgentRequest (*GetAdminDlqJobResponse)(nil), // 82: stream.app.v1.GetAdminDlqJobResponse
(*AdminAgentCommandResponse)(nil), // 83: stream.app.v1.AdminAgentCommandResponse (*RetryAdminDlqJobRequest)(nil), // 83: stream.app.v1.RetryAdminDlqJobRequest
nil, // 84: stream.app.v1.CreateAdminJobRequest.EnvEntry (*RetryAdminDlqJobResponse)(nil), // 84: stream.app.v1.RetryAdminDlqJobResponse
(*AdminDashboard)(nil), // 85: stream.app.v1.AdminDashboard (*RemoveAdminDlqJobRequest)(nil), // 85: stream.app.v1.RemoveAdminDlqJobRequest
(*AdminUser)(nil), // 86: stream.app.v1.AdminUser (*RemoveAdminDlqJobResponse)(nil), // 86: stream.app.v1.RemoveAdminDlqJobResponse
(*AdminUserDetail)(nil), // 87: stream.app.v1.AdminUserDetail (*ListAdminAgentsRequest)(nil), // 87: stream.app.v1.ListAdminAgentsRequest
(*AdminVideo)(nil), // 88: stream.app.v1.AdminVideo (*ListAdminAgentsResponse)(nil), // 88: stream.app.v1.ListAdminAgentsResponse
(*AdminPayment)(nil), // 89: stream.app.v1.AdminPayment (*RestartAdminAgentRequest)(nil), // 89: stream.app.v1.RestartAdminAgentRequest
(*PlanSubscription)(nil), // 90: stream.app.v1.PlanSubscription (*UpdateAdminAgentRequest)(nil), // 90: stream.app.v1.UpdateAdminAgentRequest
(*AdminPlan)(nil), // 91: stream.app.v1.AdminPlan (*AdminAgentCommandResponse)(nil), // 91: stream.app.v1.AdminAgentCommandResponse
(*AdminAdTemplate)(nil), // 92: stream.app.v1.AdminAdTemplate nil, // 92: stream.app.v1.CreateAdminJobRequest.EnvEntry
(*AdminPopupAd)(nil), // 93: stream.app.v1.AdminPopupAd (*AdminDashboard)(nil), // 93: stream.app.v1.AdminDashboard
(*AdminPlayerConfig)(nil), // 94: stream.app.v1.AdminPlayerConfig (*AdminUser)(nil), // 94: stream.app.v1.AdminUser
(*AdminJob)(nil), // 95: stream.app.v1.AdminJob (*AdminUserDetail)(nil), // 95: stream.app.v1.AdminUserDetail
(*AdminAgent)(nil), // 96: stream.app.v1.AdminAgent (*AdminVideo)(nil), // 96: stream.app.v1.AdminVideo
(*MessageResponse)(nil), // 97: stream.app.v1.MessageResponse (*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{ var file_app_v1_admin_proto_depIdxs = []int32{
85, // 0: stream.app.v1.GetAdminDashboardResponse.dashboard:type_name -> stream.app.v1.AdminDashboard 93, // 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 94, // 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 95, // 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 94, // 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 94, // 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 95, // 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 96, // 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 96, // 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 96, // 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 96, // 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 97, // 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 97, // 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 97, // 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 98, // 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 97, // 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 99, // 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 99, // 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 99, // 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 100, // 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 100, // 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 100, // 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 100, // 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 101, // 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 101, // 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 101, // 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 101, // 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 102, // 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 102, // 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 102, // 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 102, // 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 103, // 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 103, // 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 92, // 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 103, // 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 103, // 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 104, // 35: stream.app.v1.ListAdminDlqJobsResponse.items:type_name -> stream.app.v1.AdminDlqEntry
0, // 36: stream.app.v1.Admin.GetAdminDashboard:input_type -> stream.app.v1.GetAdminDashboardRequest 104, // 36: stream.app.v1.GetAdminDlqJobResponse.item:type_name -> stream.app.v1.AdminDlqEntry
2, // 37: stream.app.v1.Admin.ListAdminUsers:input_type -> stream.app.v1.ListAdminUsersRequest 103, // 37: stream.app.v1.RetryAdminDlqJobResponse.job:type_name -> stream.app.v1.AdminJob
4, // 38: stream.app.v1.Admin.GetAdminUser:input_type -> stream.app.v1.GetAdminUserRequest 105, // 38: stream.app.v1.ListAdminAgentsResponse.agents:type_name -> stream.app.v1.AdminAgent
6, // 39: stream.app.v1.Admin.CreateAdminUser:input_type -> stream.app.v1.CreateAdminUserRequest 0, // 39: stream.app.v1.Admin.GetAdminDashboard:input_type -> stream.app.v1.GetAdminDashboardRequest
8, // 40: stream.app.v1.Admin.UpdateAdminUser:input_type -> stream.app.v1.UpdateAdminUserRequest 2, // 40: stream.app.v1.Admin.ListAdminUsers:input_type -> stream.app.v1.ListAdminUsersRequest
10, // 41: stream.app.v1.Admin.UpdateAdminUserReferralSettings:input_type -> stream.app.v1.UpdateAdminUserReferralSettingsRequest 4, // 41: stream.app.v1.Admin.GetAdminUser:input_type -> stream.app.v1.GetAdminUserRequest
12, // 42: stream.app.v1.Admin.UpdateAdminUserRole:input_type -> stream.app.v1.UpdateAdminUserRoleRequest 6, // 42: stream.app.v1.Admin.CreateAdminUser:input_type -> stream.app.v1.CreateAdminUserRequest
14, // 43: stream.app.v1.Admin.DeleteAdminUser:input_type -> stream.app.v1.DeleteAdminUserRequest 8, // 43: stream.app.v1.Admin.UpdateAdminUser:input_type -> stream.app.v1.UpdateAdminUserRequest
15, // 44: stream.app.v1.Admin.ListAdminVideos:input_type -> stream.app.v1.ListAdminVideosRequest 10, // 44: stream.app.v1.Admin.UpdateAdminUserReferralSettings:input_type -> stream.app.v1.UpdateAdminUserReferralSettingsRequest
17, // 45: stream.app.v1.Admin.GetAdminVideo:input_type -> stream.app.v1.GetAdminVideoRequest 12, // 45: stream.app.v1.Admin.UpdateAdminUserRole:input_type -> stream.app.v1.UpdateAdminUserRoleRequest
19, // 46: stream.app.v1.Admin.CreateAdminVideo:input_type -> stream.app.v1.CreateAdminVideoRequest 14, // 46: stream.app.v1.Admin.DeleteAdminUser:input_type -> stream.app.v1.DeleteAdminUserRequest
21, // 47: stream.app.v1.Admin.UpdateAdminVideo:input_type -> stream.app.v1.UpdateAdminVideoRequest 15, // 47: stream.app.v1.Admin.ListAdminVideos:input_type -> stream.app.v1.ListAdminVideosRequest
23, // 48: stream.app.v1.Admin.DeleteAdminVideo:input_type -> stream.app.v1.DeleteAdminVideoRequest 17, // 48: stream.app.v1.Admin.GetAdminVideo:input_type -> stream.app.v1.GetAdminVideoRequest
24, // 49: stream.app.v1.Admin.ListAdminPayments:input_type -> stream.app.v1.ListAdminPaymentsRequest 19, // 49: stream.app.v1.Admin.CreateAdminVideo:input_type -> stream.app.v1.CreateAdminVideoRequest
26, // 50: stream.app.v1.Admin.GetAdminPayment:input_type -> stream.app.v1.GetAdminPaymentRequest 21, // 50: stream.app.v1.Admin.UpdateAdminVideo:input_type -> stream.app.v1.UpdateAdminVideoRequest
28, // 51: stream.app.v1.Admin.CreateAdminPayment:input_type -> stream.app.v1.CreateAdminPaymentRequest 23, // 51: stream.app.v1.Admin.DeleteAdminVideo:input_type -> stream.app.v1.DeleteAdminVideoRequest
30, // 52: stream.app.v1.Admin.UpdateAdminPayment:input_type -> stream.app.v1.UpdateAdminPaymentRequest 24, // 52: stream.app.v1.Admin.ListAdminPayments:input_type -> stream.app.v1.ListAdminPaymentsRequest
32, // 53: stream.app.v1.Admin.ListAdminPlans:input_type -> stream.app.v1.ListAdminPlansRequest 26, // 53: stream.app.v1.Admin.GetAdminPayment:input_type -> stream.app.v1.GetAdminPaymentRequest
34, // 54: stream.app.v1.Admin.CreateAdminPlan:input_type -> stream.app.v1.CreateAdminPlanRequest 28, // 54: stream.app.v1.Admin.CreateAdminPayment:input_type -> stream.app.v1.CreateAdminPaymentRequest
36, // 55: stream.app.v1.Admin.UpdateAdminPlan:input_type -> stream.app.v1.UpdateAdminPlanRequest 30, // 55: stream.app.v1.Admin.UpdateAdminPayment:input_type -> stream.app.v1.UpdateAdminPaymentRequest
38, // 56: stream.app.v1.Admin.DeleteAdminPlan:input_type -> stream.app.v1.DeleteAdminPlanRequest 32, // 56: stream.app.v1.Admin.ListAdminPlans:input_type -> stream.app.v1.ListAdminPlansRequest
40, // 57: stream.app.v1.Admin.ListAdminAdTemplates:input_type -> stream.app.v1.ListAdminAdTemplatesRequest 34, // 57: stream.app.v1.Admin.CreateAdminPlan:input_type -> stream.app.v1.CreateAdminPlanRequest
42, // 58: stream.app.v1.Admin.GetAdminAdTemplate:input_type -> stream.app.v1.GetAdminAdTemplateRequest 36, // 58: stream.app.v1.Admin.UpdateAdminPlan:input_type -> stream.app.v1.UpdateAdminPlanRequest
44, // 59: stream.app.v1.Admin.CreateAdminAdTemplate:input_type -> stream.app.v1.CreateAdminAdTemplateRequest 38, // 59: stream.app.v1.Admin.DeleteAdminPlan:input_type -> stream.app.v1.DeleteAdminPlanRequest
46, // 60: stream.app.v1.Admin.UpdateAdminAdTemplate:input_type -> stream.app.v1.UpdateAdminAdTemplateRequest 40, // 60: stream.app.v1.Admin.ListAdminAdTemplates:input_type -> stream.app.v1.ListAdminAdTemplatesRequest
48, // 61: stream.app.v1.Admin.DeleteAdminAdTemplate:input_type -> stream.app.v1.DeleteAdminAdTemplateRequest 42, // 61: stream.app.v1.Admin.GetAdminAdTemplate:input_type -> stream.app.v1.GetAdminAdTemplateRequest
49, // 62: stream.app.v1.Admin.ListAdminPopupAds:input_type -> stream.app.v1.ListAdminPopupAdsRequest 44, // 62: stream.app.v1.Admin.CreateAdminAdTemplate:input_type -> stream.app.v1.CreateAdminAdTemplateRequest
51, // 63: stream.app.v1.Admin.GetAdminPopupAd:input_type -> stream.app.v1.GetAdminPopupAdRequest 46, // 63: stream.app.v1.Admin.UpdateAdminAdTemplate:input_type -> stream.app.v1.UpdateAdminAdTemplateRequest
53, // 64: stream.app.v1.Admin.CreateAdminPopupAd:input_type -> stream.app.v1.CreateAdminPopupAdRequest 48, // 64: stream.app.v1.Admin.DeleteAdminAdTemplate:input_type -> stream.app.v1.DeleteAdminAdTemplateRequest
55, // 65: stream.app.v1.Admin.UpdateAdminPopupAd:input_type -> stream.app.v1.UpdateAdminPopupAdRequest 49, // 65: stream.app.v1.Admin.ListAdminPopupAds:input_type -> stream.app.v1.ListAdminPopupAdsRequest
57, // 66: stream.app.v1.Admin.DeleteAdminPopupAd:input_type -> stream.app.v1.DeleteAdminPopupAdRequest 51, // 66: stream.app.v1.Admin.GetAdminPopupAd:input_type -> stream.app.v1.GetAdminPopupAdRequest
58, // 67: stream.app.v1.Admin.ListAdminPlayerConfigs:input_type -> stream.app.v1.ListAdminPlayerConfigsRequest 53, // 67: stream.app.v1.Admin.CreateAdminPopupAd:input_type -> stream.app.v1.CreateAdminPopupAdRequest
60, // 68: stream.app.v1.Admin.GetAdminPlayerConfig:input_type -> stream.app.v1.GetAdminPlayerConfigRequest 55, // 68: stream.app.v1.Admin.UpdateAdminPopupAd:input_type -> stream.app.v1.UpdateAdminPopupAdRequest
62, // 69: stream.app.v1.Admin.CreateAdminPlayerConfig:input_type -> stream.app.v1.CreateAdminPlayerConfigRequest 57, // 69: stream.app.v1.Admin.DeleteAdminPopupAd:input_type -> stream.app.v1.DeleteAdminPopupAdRequest
64, // 70: stream.app.v1.Admin.UpdateAdminPlayerConfig:input_type -> stream.app.v1.UpdateAdminPlayerConfigRequest 58, // 70: stream.app.v1.Admin.ListAdminPlayerConfigs:input_type -> stream.app.v1.ListAdminPlayerConfigsRequest
66, // 71: stream.app.v1.Admin.DeleteAdminPlayerConfig:input_type -> stream.app.v1.DeleteAdminPlayerConfigRequest 60, // 71: stream.app.v1.Admin.GetAdminPlayerConfig:input_type -> stream.app.v1.GetAdminPlayerConfigRequest
67, // 72: stream.app.v1.Admin.ListAdminJobs:input_type -> stream.app.v1.ListAdminJobsRequest 62, // 72: stream.app.v1.Admin.CreateAdminPlayerConfig:input_type -> stream.app.v1.CreateAdminPlayerConfigRequest
69, // 73: stream.app.v1.Admin.GetAdminJob:input_type -> stream.app.v1.GetAdminJobRequest 64, // 73: stream.app.v1.Admin.UpdateAdminPlayerConfig:input_type -> stream.app.v1.UpdateAdminPlayerConfigRequest
71, // 74: stream.app.v1.Admin.GetAdminJobLogs:input_type -> stream.app.v1.GetAdminJobLogsRequest 66, // 74: stream.app.v1.Admin.DeleteAdminPlayerConfig:input_type -> stream.app.v1.DeleteAdminPlayerConfigRequest
73, // 75: stream.app.v1.Admin.CreateAdminJob:input_type -> stream.app.v1.CreateAdminJobRequest 67, // 75: stream.app.v1.Admin.ListAdminJobs:input_type -> stream.app.v1.ListAdminJobsRequest
75, // 76: stream.app.v1.Admin.CancelAdminJob:input_type -> stream.app.v1.CancelAdminJobRequest 69, // 76: stream.app.v1.Admin.GetAdminJob:input_type -> stream.app.v1.GetAdminJobRequest
77, // 77: stream.app.v1.Admin.RetryAdminJob:input_type -> stream.app.v1.RetryAdminJobRequest 71, // 77: stream.app.v1.Admin.GetAdminJobLogs:input_type -> stream.app.v1.GetAdminJobLogsRequest
79, // 78: stream.app.v1.Admin.ListAdminAgents:input_type -> stream.app.v1.ListAdminAgentsRequest 73, // 78: stream.app.v1.Admin.CreateAdminJob:input_type -> stream.app.v1.CreateAdminJobRequest
81, // 79: stream.app.v1.Admin.RestartAdminAgent:input_type -> stream.app.v1.RestartAdminAgentRequest 75, // 79: stream.app.v1.Admin.CancelAdminJob:input_type -> stream.app.v1.CancelAdminJobRequest
82, // 80: stream.app.v1.Admin.UpdateAdminAgent:input_type -> stream.app.v1.UpdateAdminAgentRequest 77, // 80: stream.app.v1.Admin.RetryAdminJob:input_type -> stream.app.v1.RetryAdminJobRequest
1, // 81: stream.app.v1.Admin.GetAdminDashboard:output_type -> stream.app.v1.GetAdminDashboardResponse 79, // 81: stream.app.v1.Admin.ListAdminDlqJobs:input_type -> stream.app.v1.ListAdminDlqJobsRequest
3, // 82: stream.app.v1.Admin.ListAdminUsers:output_type -> stream.app.v1.ListAdminUsersResponse 81, // 82: stream.app.v1.Admin.GetAdminDlqJob:input_type -> stream.app.v1.GetAdminDlqJobRequest
5, // 83: stream.app.v1.Admin.GetAdminUser:output_type -> stream.app.v1.GetAdminUserResponse 83, // 83: stream.app.v1.Admin.RetryAdminDlqJob:input_type -> stream.app.v1.RetryAdminDlqJobRequest
7, // 84: stream.app.v1.Admin.CreateAdminUser:output_type -> stream.app.v1.CreateAdminUserResponse 85, // 84: stream.app.v1.Admin.RemoveAdminDlqJob:input_type -> stream.app.v1.RemoveAdminDlqJobRequest
9, // 85: stream.app.v1.Admin.UpdateAdminUser:output_type -> stream.app.v1.UpdateAdminUserResponse 87, // 85: stream.app.v1.Admin.ListAdminAgents:input_type -> stream.app.v1.ListAdminAgentsRequest
11, // 86: stream.app.v1.Admin.UpdateAdminUserReferralSettings:output_type -> stream.app.v1.UpdateAdminUserReferralSettingsResponse 89, // 86: stream.app.v1.Admin.RestartAdminAgent:input_type -> stream.app.v1.RestartAdminAgentRequest
13, // 87: stream.app.v1.Admin.UpdateAdminUserRole:output_type -> stream.app.v1.UpdateAdminUserRoleResponse 90, // 87: stream.app.v1.Admin.UpdateAdminAgent:input_type -> stream.app.v1.UpdateAdminAgentRequest
97, // 88: stream.app.v1.Admin.DeleteAdminUser:output_type -> stream.app.v1.MessageResponse 1, // 88: stream.app.v1.Admin.GetAdminDashboard:output_type -> stream.app.v1.GetAdminDashboardResponse
16, // 89: stream.app.v1.Admin.ListAdminVideos:output_type -> stream.app.v1.ListAdminVideosResponse 3, // 89: stream.app.v1.Admin.ListAdminUsers:output_type -> stream.app.v1.ListAdminUsersResponse
18, // 90: stream.app.v1.Admin.GetAdminVideo:output_type -> stream.app.v1.GetAdminVideoResponse 5, // 90: stream.app.v1.Admin.GetAdminUser:output_type -> stream.app.v1.GetAdminUserResponse
20, // 91: stream.app.v1.Admin.CreateAdminVideo:output_type -> stream.app.v1.CreateAdminVideoResponse 7, // 91: stream.app.v1.Admin.CreateAdminUser:output_type -> stream.app.v1.CreateAdminUserResponse
22, // 92: stream.app.v1.Admin.UpdateAdminVideo:output_type -> stream.app.v1.UpdateAdminVideoResponse 9, // 92: stream.app.v1.Admin.UpdateAdminUser:output_type -> stream.app.v1.UpdateAdminUserResponse
97, // 93: stream.app.v1.Admin.DeleteAdminVideo:output_type -> stream.app.v1.MessageResponse 11, // 93: stream.app.v1.Admin.UpdateAdminUserReferralSettings:output_type -> stream.app.v1.UpdateAdminUserReferralSettingsResponse
25, // 94: stream.app.v1.Admin.ListAdminPayments:output_type -> stream.app.v1.ListAdminPaymentsResponse 13, // 94: stream.app.v1.Admin.UpdateAdminUserRole:output_type -> stream.app.v1.UpdateAdminUserRoleResponse
27, // 95: stream.app.v1.Admin.GetAdminPayment:output_type -> stream.app.v1.GetAdminPaymentResponse 106, // 95: stream.app.v1.Admin.DeleteAdminUser:output_type -> stream.app.v1.MessageResponse
29, // 96: stream.app.v1.Admin.CreateAdminPayment:output_type -> stream.app.v1.CreateAdminPaymentResponse 16, // 96: stream.app.v1.Admin.ListAdminVideos:output_type -> stream.app.v1.ListAdminVideosResponse
31, // 97: stream.app.v1.Admin.UpdateAdminPayment:output_type -> stream.app.v1.UpdateAdminPaymentResponse 18, // 97: stream.app.v1.Admin.GetAdminVideo:output_type -> stream.app.v1.GetAdminVideoResponse
33, // 98: stream.app.v1.Admin.ListAdminPlans:output_type -> stream.app.v1.ListAdminPlansResponse 20, // 98: stream.app.v1.Admin.CreateAdminVideo:output_type -> stream.app.v1.CreateAdminVideoResponse
35, // 99: stream.app.v1.Admin.CreateAdminPlan:output_type -> stream.app.v1.CreateAdminPlanResponse 22, // 99: stream.app.v1.Admin.UpdateAdminVideo:output_type -> stream.app.v1.UpdateAdminVideoResponse
37, // 100: stream.app.v1.Admin.UpdateAdminPlan:output_type -> stream.app.v1.UpdateAdminPlanResponse 106, // 100: stream.app.v1.Admin.DeleteAdminVideo:output_type -> stream.app.v1.MessageResponse
39, // 101: stream.app.v1.Admin.DeleteAdminPlan:output_type -> stream.app.v1.DeleteAdminPlanResponse 25, // 101: stream.app.v1.Admin.ListAdminPayments:output_type -> stream.app.v1.ListAdminPaymentsResponse
41, // 102: stream.app.v1.Admin.ListAdminAdTemplates:output_type -> stream.app.v1.ListAdminAdTemplatesResponse 27, // 102: stream.app.v1.Admin.GetAdminPayment:output_type -> stream.app.v1.GetAdminPaymentResponse
43, // 103: stream.app.v1.Admin.GetAdminAdTemplate:output_type -> stream.app.v1.GetAdminAdTemplateResponse 29, // 103: stream.app.v1.Admin.CreateAdminPayment:output_type -> stream.app.v1.CreateAdminPaymentResponse
45, // 104: stream.app.v1.Admin.CreateAdminAdTemplate:output_type -> stream.app.v1.CreateAdminAdTemplateResponse 31, // 104: stream.app.v1.Admin.UpdateAdminPayment:output_type -> stream.app.v1.UpdateAdminPaymentResponse
47, // 105: stream.app.v1.Admin.UpdateAdminAdTemplate:output_type -> stream.app.v1.UpdateAdminAdTemplateResponse 33, // 105: stream.app.v1.Admin.ListAdminPlans:output_type -> stream.app.v1.ListAdminPlansResponse
97, // 106: stream.app.v1.Admin.DeleteAdminAdTemplate:output_type -> stream.app.v1.MessageResponse 35, // 106: stream.app.v1.Admin.CreateAdminPlan:output_type -> stream.app.v1.CreateAdminPlanResponse
50, // 107: stream.app.v1.Admin.ListAdminPopupAds:output_type -> stream.app.v1.ListAdminPopupAdsResponse 37, // 107: stream.app.v1.Admin.UpdateAdminPlan:output_type -> stream.app.v1.UpdateAdminPlanResponse
52, // 108: stream.app.v1.Admin.GetAdminPopupAd:output_type -> stream.app.v1.GetAdminPopupAdResponse 39, // 108: stream.app.v1.Admin.DeleteAdminPlan:output_type -> stream.app.v1.DeleteAdminPlanResponse
54, // 109: stream.app.v1.Admin.CreateAdminPopupAd:output_type -> stream.app.v1.CreateAdminPopupAdResponse 41, // 109: stream.app.v1.Admin.ListAdminAdTemplates:output_type -> stream.app.v1.ListAdminAdTemplatesResponse
56, // 110: stream.app.v1.Admin.UpdateAdminPopupAd:output_type -> stream.app.v1.UpdateAdminPopupAdResponse 43, // 110: stream.app.v1.Admin.GetAdminAdTemplate:output_type -> stream.app.v1.GetAdminAdTemplateResponse
97, // 111: stream.app.v1.Admin.DeleteAdminPopupAd:output_type -> stream.app.v1.MessageResponse 45, // 111: stream.app.v1.Admin.CreateAdminAdTemplate:output_type -> stream.app.v1.CreateAdminAdTemplateResponse
59, // 112: stream.app.v1.Admin.ListAdminPlayerConfigs:output_type -> stream.app.v1.ListAdminPlayerConfigsResponse 47, // 112: stream.app.v1.Admin.UpdateAdminAdTemplate:output_type -> stream.app.v1.UpdateAdminAdTemplateResponse
61, // 113: stream.app.v1.Admin.GetAdminPlayerConfig:output_type -> stream.app.v1.GetAdminPlayerConfigResponse 106, // 113: stream.app.v1.Admin.DeleteAdminAdTemplate:output_type -> stream.app.v1.MessageResponse
63, // 114: stream.app.v1.Admin.CreateAdminPlayerConfig:output_type -> stream.app.v1.CreateAdminPlayerConfigResponse 50, // 114: stream.app.v1.Admin.ListAdminPopupAds:output_type -> stream.app.v1.ListAdminPopupAdsResponse
65, // 115: stream.app.v1.Admin.UpdateAdminPlayerConfig:output_type -> stream.app.v1.UpdateAdminPlayerConfigResponse 52, // 115: stream.app.v1.Admin.GetAdminPopupAd:output_type -> stream.app.v1.GetAdminPopupAdResponse
97, // 116: stream.app.v1.Admin.DeleteAdminPlayerConfig:output_type -> stream.app.v1.MessageResponse 54, // 116: stream.app.v1.Admin.CreateAdminPopupAd:output_type -> stream.app.v1.CreateAdminPopupAdResponse
68, // 117: stream.app.v1.Admin.ListAdminJobs:output_type -> stream.app.v1.ListAdminJobsResponse 56, // 117: stream.app.v1.Admin.UpdateAdminPopupAd:output_type -> stream.app.v1.UpdateAdminPopupAdResponse
70, // 118: stream.app.v1.Admin.GetAdminJob:output_type -> stream.app.v1.GetAdminJobResponse 106, // 118: stream.app.v1.Admin.DeleteAdminPopupAd:output_type -> stream.app.v1.MessageResponse
72, // 119: stream.app.v1.Admin.GetAdminJobLogs:output_type -> stream.app.v1.GetAdminJobLogsResponse 59, // 119: stream.app.v1.Admin.ListAdminPlayerConfigs:output_type -> stream.app.v1.ListAdminPlayerConfigsResponse
74, // 120: stream.app.v1.Admin.CreateAdminJob:output_type -> stream.app.v1.CreateAdminJobResponse 61, // 120: stream.app.v1.Admin.GetAdminPlayerConfig:output_type -> stream.app.v1.GetAdminPlayerConfigResponse
76, // 121: stream.app.v1.Admin.CancelAdminJob:output_type -> stream.app.v1.CancelAdminJobResponse 63, // 121: stream.app.v1.Admin.CreateAdminPlayerConfig:output_type -> stream.app.v1.CreateAdminPlayerConfigResponse
78, // 122: stream.app.v1.Admin.RetryAdminJob:output_type -> stream.app.v1.RetryAdminJobResponse 65, // 122: stream.app.v1.Admin.UpdateAdminPlayerConfig:output_type -> stream.app.v1.UpdateAdminPlayerConfigResponse
80, // 123: stream.app.v1.Admin.ListAdminAgents:output_type -> stream.app.v1.ListAdminAgentsResponse 106, // 123: stream.app.v1.Admin.DeleteAdminPlayerConfig:output_type -> stream.app.v1.MessageResponse
83, // 124: stream.app.v1.Admin.RestartAdminAgent:output_type -> stream.app.v1.AdminAgentCommandResponse 68, // 124: stream.app.v1.Admin.ListAdminJobs:output_type -> stream.app.v1.ListAdminJobsResponse
83, // 125: stream.app.v1.Admin.UpdateAdminAgent:output_type -> stream.app.v1.AdminAgentCommandResponse 70, // 125: stream.app.v1.Admin.GetAdminJob:output_type -> stream.app.v1.GetAdminJobResponse
81, // [81:126] is the sub-list for method output_type 72, // 126: stream.app.v1.Admin.GetAdminJobLogs:output_type -> stream.app.v1.GetAdminJobLogsResponse
36, // [36:81] is the sub-list for method input_type 74, // 127: stream.app.v1.Admin.CreateAdminJob:output_type -> stream.app.v1.CreateAdminJobResponse
36, // [36:36] is the sub-list for extension type_name 76, // 128: stream.app.v1.Admin.CancelAdminJob:output_type -> stream.app.v1.CancelAdminJobResponse
36, // [36:36] is the sub-list for extension extendee 78, // 129: stream.app.v1.Admin.RetryAdminJob:output_type -> stream.app.v1.RetryAdminJobResponse
0, // [0:36] is the sub-list for field type_name 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() } func init() { file_app_v1_admin_proto_init() }
@@ -5787,7 +6224,7 @@ func file_app_v1_admin_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(), GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_app_v1_admin_proto_rawDesc), len(file_app_v1_admin_proto_rawDesc)), RawDescriptor: unsafe.Slice(unsafe.StringData(file_app_v1_admin_proto_rawDesc), len(file_app_v1_admin_proto_rawDesc)),
NumEnums: 0, NumEnums: 0,
NumMessages: 85, NumMessages: 93,
NumExtensions: 0, NumExtensions: 0,
NumServices: 1, NumServices: 1,
}, },

View File

@@ -61,6 +61,10 @@ const (
Admin_CreateAdminJob_FullMethodName = "/stream.app.v1.Admin/CreateAdminJob" Admin_CreateAdminJob_FullMethodName = "/stream.app.v1.Admin/CreateAdminJob"
Admin_CancelAdminJob_FullMethodName = "/stream.app.v1.Admin/CancelAdminJob" Admin_CancelAdminJob_FullMethodName = "/stream.app.v1.Admin/CancelAdminJob"
Admin_RetryAdminJob_FullMethodName = "/stream.app.v1.Admin/RetryAdminJob" 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_ListAdminAgents_FullMethodName = "/stream.app.v1.Admin/ListAdminAgents"
Admin_RestartAdminAgent_FullMethodName = "/stream.app.v1.Admin/RestartAdminAgent" Admin_RestartAdminAgent_FullMethodName = "/stream.app.v1.Admin/RestartAdminAgent"
Admin_UpdateAdminAgent_FullMethodName = "/stream.app.v1.Admin/UpdateAdminAgent" 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) CreateAdminJob(ctx context.Context, in *CreateAdminJobRequest, opts ...grpc.CallOption) (*CreateAdminJobResponse, error)
CancelAdminJob(ctx context.Context, in *CancelAdminJobRequest, opts ...grpc.CallOption) (*CancelAdminJobResponse, error) CancelAdminJob(ctx context.Context, in *CancelAdminJobRequest, opts ...grpc.CallOption) (*CancelAdminJobResponse, error)
RetryAdminJob(ctx context.Context, in *RetryAdminJobRequest, opts ...grpc.CallOption) (*RetryAdminJobResponse, 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) ListAdminAgents(ctx context.Context, in *ListAdminAgentsRequest, opts ...grpc.CallOption) (*ListAdminAgentsResponse, error)
RestartAdminAgent(ctx context.Context, in *RestartAdminAgentRequest, opts ...grpc.CallOption) (*AdminAgentCommandResponse, error) RestartAdminAgent(ctx context.Context, in *RestartAdminAgentRequest, opts ...grpc.CallOption) (*AdminAgentCommandResponse, error)
UpdateAdminAgent(ctx context.Context, in *UpdateAdminAgentRequest, 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 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) { func (c *adminClient) ListAdminAgents(ctx context.Context, in *ListAdminAgentsRequest, opts ...grpc.CallOption) (*ListAdminAgentsResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ListAdminAgentsResponse) out := new(ListAdminAgentsResponse)
@@ -621,6 +669,10 @@ type AdminServer interface {
CreateAdminJob(context.Context, *CreateAdminJobRequest) (*CreateAdminJobResponse, error) CreateAdminJob(context.Context, *CreateAdminJobRequest) (*CreateAdminJobResponse, error)
CancelAdminJob(context.Context, *CancelAdminJobRequest) (*CancelAdminJobResponse, error) CancelAdminJob(context.Context, *CancelAdminJobRequest) (*CancelAdminJobResponse, error)
RetryAdminJob(context.Context, *RetryAdminJobRequest) (*RetryAdminJobResponse, 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) ListAdminAgents(context.Context, *ListAdminAgentsRequest) (*ListAdminAgentsResponse, error)
RestartAdminAgent(context.Context, *RestartAdminAgentRequest) (*AdminAgentCommandResponse, error) RestartAdminAgent(context.Context, *RestartAdminAgentRequest) (*AdminAgentCommandResponse, error)
UpdateAdminAgent(context.Context, *UpdateAdminAgentRequest) (*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) { func (UnimplementedAdminServer) RetryAdminJob(context.Context, *RetryAdminJobRequest) (*RetryAdminJobResponse, error) {
return nil, status.Error(codes.Unimplemented, "method RetryAdminJob not implemented") 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) { func (UnimplementedAdminServer) ListAdminAgents(context.Context, *ListAdminAgentsRequest) (*ListAdminAgentsResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListAdminAgents not implemented") 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) 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) { func _Admin_ListAdminAgents_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListAdminAgentsRequest) in := new(ListAdminAgentsRequest)
if err := dec(in); err != nil { if err := dec(in); err != nil {
@@ -1775,6 +1911,22 @@ var Admin_ServiceDesc = grpc.ServiceDesc{
MethodName: "RetryAdminJob", MethodName: "RetryAdminJob",
Handler: _Admin_RetryAdminJob_Handler, 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", MethodName: "ListAdminAgents",
Handler: _Admin_ListAdminAgents_Handler, Handler: _Admin_ListAdminAgents_Handler,

View File

@@ -3442,6 +3442,74 @@ func (x *AdminAgent) GetUpdatedAt() *timestamppb.Timestamp {
return nil 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 var File_app_v1_common_proto protoreflect.FileDescriptor
const file_app_v1_common_proto_rawDesc = "" + const file_app_v1_common_proto_rawDesc = "" +
@@ -3954,7 +4022,13 @@ const file_app_v1_common_proto_rawDesc = "" +
"\n" + "\n" +
"created_at\x18\v \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\x129\n" + "created_at\x18\v \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\x129\n" +
"\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 ( var (
file_app_v1_common_proto_rawDescOnce sync.Once 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 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{ var file_app_v1_common_proto_goTypes = []any{
(*MessageResponse)(nil), // 0: stream.app.v1.MessageResponse (*MessageResponse)(nil), // 0: stream.app.v1.MessageResponse
(*User)(nil), // 1: stream.app.v1.User (*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 (*AdminPopupAd)(nil), // 24: stream.app.v1.AdminPopupAd
(*AdminJob)(nil), // 25: stream.app.v1.AdminJob (*AdminJob)(nil), // 25: stream.app.v1.AdminJob
(*AdminAgent)(nil), // 26: stream.app.v1.AdminAgent (*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{ var file_app_v1_common_proto_depIdxs = []int32{
27, // 0: stream.app.v1.User.plan_started_at:type_name -> google.protobuf.Timestamp 28, // 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 28, // 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 28, // 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 28, // 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 28, // 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 28, // 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 28, // 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 28, // 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 28, // 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 28, // 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 28, // 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 28, // 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 28, // 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 28, // 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 28, // 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 28, // 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 28, // 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 28, // 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 28, // 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 28, // 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 28, // 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 28, // 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 28, // 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 28, // 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 28, // 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 28, // 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 28, // 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 28, // 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, // 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 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 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 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 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 28, // 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 28, // 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 28, // 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 28, // 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 28, // 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 28, // 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 28, // 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 28, // 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 28, // 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 28, // 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 28, // 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 28, // 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 28, // 46: stream.app.v1.AdminAgent.updated_at:type_name -> google.protobuf.Timestamp
47, // [47:47] is the sub-list for method output_type 25, // 47: stream.app.v1.AdminDlqEntry.job:type_name -> stream.app.v1.AdminJob
47, // [47:47] is the sub-list for method input_type 28, // 48: stream.app.v1.AdminDlqEntry.failure_time:type_name -> google.protobuf.Timestamp
47, // [47:47] is the sub-list for extension type_name 49, // [49:49] is the sub-list for method output_type
47, // [47:47] is the sub-list for extension extendee 49, // [49:49] is the sub-list for method input_type
0, // [0:47] is the sub-list for field type_name 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() } func init() { file_app_v1_common_proto_init() }
@@ -4085,7 +4162,7 @@ func file_app_v1_common_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(), GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_app_v1_common_proto_rawDesc), len(file_app_v1_common_proto_rawDesc)), RawDescriptor: unsafe.Slice(unsafe.StringData(file_app_v1_common_proto_rawDesc), len(file_app_v1_common_proto_rawDesc)),
NumEnums: 0, NumEnums: 0,
NumMessages: 27, NumMessages: 28,
NumExtensions: 0, NumExtensions: 0,
NumServices: 0, NumServices: 0,
}, },

View File

@@ -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
}

View File

@@ -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",
}

View File

@@ -11,7 +11,6 @@ type Config struct {
Database DatabaseConfig Database DatabaseConfig
Redis RedisConfig Redis RedisConfig
Google GoogleConfig Google GoogleConfig
Frontend FrontendConfig
Email EmailConfig Email EmailConfig
AWS AWSConfig AWS AWSConfig
Render RenderConfig Render RenderConfig
@@ -25,7 +24,6 @@ type ServerConfig struct {
} }
type RenderConfig struct { type RenderConfig struct {
AgentSecret string `mapstructure:"agent_secret"`
EnableMetrics bool `mapstructure:"enable_metrics"` EnableMetrics bool `mapstructure:"enable_metrics"`
EnableTracing bool `mapstructure:"enable_tracing"` EnableTracing bool `mapstructure:"enable_tracing"`
OTLPEndpoint string `mapstructure:"otlp_endpoint"` OTLPEndpoint string `mapstructure:"otlp_endpoint"`
@@ -53,11 +51,6 @@ type GoogleConfig struct {
StateTTLMinute int `mapstructure:"state_ttl_minutes"` StateTTLMinute int `mapstructure:"state_ttl_minutes"`
} }
type FrontendConfig struct {
BaseURL string `mapstructure:"base_url"`
GoogleAuthFinalizePath string `mapstructure:"google_auth_finalize_path"`
}
type EmailConfig struct { type EmailConfig struct {
From string From string
// Add SMTP settings here later // Add SMTP settings here later
@@ -83,7 +76,6 @@ func LoadConfig() (*Config, error) {
v.SetDefault("render.enable_tracing", false) v.SetDefault("render.enable_tracing", false)
v.SetDefault("render.service_name", "stream-api-render") v.SetDefault("render.service_name", "stream-api-render")
v.SetDefault("google.state_ttl_minutes", 10) v.SetDefault("google.state_ttl_minutes", 10)
v.SetDefault("frontend.google_auth_finalize_path", "/auth/google/finalize")
v.SetDefault("internal.marker", "") v.SetDefault("internal.marker", "")
// Environment variable settings // Environment variable settings

View File

@@ -44,3 +44,10 @@ type JobConfigEnvelope struct {
VideoID string `json:"video_id,omitempty"` VideoID string `json:"video_id,omitempty"`
TimeLimit int64 `json:"time_limit,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"`
}

View File

@@ -100,3 +100,82 @@ func (r *jobRepository) GetLatestByVideoID(ctx context.Context, videoID string)
} }
return &job, nil 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
}

View File

@@ -1,15 +1,17 @@
package service package service
import ( import (
"context"
"fmt"
"testing" "testing"
"time" "time"
"github.com/google/uuid" "github.com/google/uuid"
"google.golang.org/grpc/codes" "google.golang.org/grpc/codes"
"gorm.io/gorm" "gorm.io/gorm"
redisadapter "stream.api/internal/adapters/redis"
appv1 "stream.api/internal/api/proto/app/v1" appv1 "stream.api/internal/api/proto/app/v1"
"stream.api/internal/database/model" "stream.api/internal/database/model"
runtimeservices "stream.api/internal/service/runtime/services"
renderworkflow "stream.api/internal/workflow/render" renderworkflow "stream.api/internal/workflow/render"
) )
@@ -18,7 +20,7 @@ func TestListAdminJobsCursorPagination(t *testing.T) {
ensureTestJobsTable(t, db) ensureTestJobsTable(t, db)
services := newTestAppServices(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")}) 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) baseTime := time.Date(2026, 3, 22, 10, 0, 0, 0, time.UTC)
@@ -67,7 +69,7 @@ func TestListAdminJobsInvalidCursor(t *testing.T) {
ensureTestJobsTable(t, db) ensureTestJobsTable(t, db)
services := newTestAppServices(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")}) admin := seedTestUser(t, db, model.User{ID: uuid.NewString(), Email: "admin@example.com", Role: ptrString("ADMIN")})
conn, cleanup := newTestGRPCServer(t, services) conn, cleanup := newTestGRPCServer(t, services)
@@ -86,7 +88,7 @@ func TestListAdminJobsCursorRejectsAgentMismatch(t *testing.T) {
ensureTestJobsTable(t, db) ensureTestJobsTable(t, db)
services := newTestAppServices(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")}) 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) 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)
})
}

View File

@@ -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)
})
}

View File

@@ -9,13 +9,13 @@ import (
"time" "time"
"github.com/google/uuid" "github.com/google/uuid"
_ "github.com/mattn/go-sqlite3"
"google.golang.org/grpc" "google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/metadata" "google.golang.org/grpc/metadata"
"google.golang.org/grpc/test/bufconn" "google.golang.org/grpc/test/bufconn"
"gorm.io/driver/sqlite" "gorm.io/driver/sqlite"
"gorm.io/gorm" "gorm.io/gorm"
_ "github.com/mattn/go-sqlite3"
appv1 "stream.api/internal/api/proto/app/v1" appv1 "stream.api/internal/api/proto/app/v1"
"stream.api/internal/database/model" "stream.api/internal/database/model"
"stream.api/internal/database/query" "stream.api/internal/database/query"
@@ -210,14 +210,11 @@ func newTestDB(t *testing.T) *gorm.DB {
`CREATE TABLE popup_ads ( `CREATE TABLE popup_ads (
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
user_id TEXT NOT NULL, user_id TEXT NOT NULL,
title TEXT NOT NULL, type TEXT NOT NULL,
image_url TEXT NOT NULL, label TEXT NOT NULL,
target_url TEXT NOT NULL, value TEXT NOT NULL,
is_active BOOLEAN NOT NULL DEFAULT 1, is_active BOOLEAN NOT NULL DEFAULT 1,
start_at DATETIME, max_triggers_per_session INTEGER,
end_at DATETIME,
priority INTEGER NOT NULL DEFAULT 0,
close_cooldown_minutes INTEGER NOT NULL DEFAULT 60,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP, created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME, updated_at DATETIME,
version INTEGER NOT NULL DEFAULT 1 version INTEGER NOT NULL DEFAULT 1
@@ -273,6 +270,7 @@ func newTestGRPCServer(t *testing.T, services *appServices) (*grpc.ClientConn, f
PlansServer: services, PlansServer: services,
PaymentsServer: services, PaymentsServer: services,
VideosServer: services, VideosServer: services,
VideoMetadataServer: services,
AdminServer: services, AdminServer: services,
}) })
@@ -407,6 +405,10 @@ func newAdminClient(conn *grpc.ClientConn) appv1.AdminClient {
return appv1.NewAdminClient(conn) 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 ptrTime(v time.Time) *time.Time { return &v }
func seedTestPopupAd(t *testing.T, db *gorm.DB, item model.PopupAd) model.PopupAd { func seedTestPopupAd(t *testing.T, db *gorm.DB, item model.PopupAd) model.PopupAd {

View File

@@ -65,31 +65,61 @@ func buildAdminJob(job *model.Job) *appv1.AdminJob {
if job == nil { if job == nil {
return 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{ return &appv1.AdminJob{
Id: job.ID, Id: job.ID,
Status: string(*job.Status), Status: stringValue(job.Status),
Priority: int32(*job.Priority), Priority: int32(int64Value(job.Priority)),
UserId: *job.UserID, UserId: stringValue(job.UserID),
Name: job.ID, Name: job.ID,
TimeLimit: *job.TimeLimit, TimeLimit: int64Value(job.TimeLimit),
InputUrl: *job.InputURL, InputUrl: stringValue(job.InputURL),
OutputUrl: *job.OutputURL, OutputUrl: stringValue(job.OutputURL),
TotalDuration: *job.TotalDuration, TotalDuration: int64Value(job.TotalDuration),
CurrentTime: *job.CurrentTime, CurrentTime: int64Value(job.CurrentTime),
Progress: *job.Progress, Progress: float64Value(job.Progress),
AgentId: &agentID, AgentId: agentID,
Logs: *job.Logs, Logs: stringValue(job.Logs),
Config: *job.Config, Config: stringValue(job.Config),
Cancelled: *job.Cancelled, Cancelled: boolValue(job.Cancelled),
RetryCount: int32(*job.RetryCount), RetryCount: int32(int64Value(job.RetryCount)),
MaxRetries: int32(*job.MaxRetries), MaxRetries: int32(int64Value(job.MaxRetries)),
CreatedAt: timestamppb.New(*job.CreatedAt), CreatedAt: timeToProto(job.CreatedAt),
UpdatedAt: timestamppb.New(*job.UpdatedAt), UpdatedAt: timeToProto(job.UpdatedAt),
VideoId: stringPointerOrNil(*job.VideoID), 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 { func buildAdminAgent(agent *dto.AgentWithStats) *appv1.AdminAgent {
if agent == nil || agent.Agent == nil { if agent == nil || agent.Agent == nil {
return nil return nil
@@ -526,15 +556,20 @@ func (s *appServices) buildAdminPopupAd(ctx context.Context, item *model.PopupAd
} }
payload := &appv1.AdminPopupAd{ payload := &appv1.AdminPopupAd{
Id: item.ID, Id: item.ID,
UserId: item.UserID, UserId: item.UserID,
Type: item.Type, Type: item.Type,
Label: item.Label, Label: item.Label,
Value: item.Value, Value: item.Value,
IsActive: boolValue(item.IsActive), IsActive: boolValue(item.IsActive),
MaxTriggersPerSession: func() int32 { if item.MaxTriggersPerSession != nil { return *item.MaxTriggersPerSession }; return 0 }(), MaxTriggersPerSession: func() int32 {
CreatedAt: timeToProto(item.CreatedAt), if item.MaxTriggersPerSession != nil {
UpdatedAt: timeToProto(item.UpdatedAt), return *item.MaxTriggersPerSession
}
return 0
}(),
CreatedAt: timeToProto(item.CreatedAt),
UpdatedAt: timeToProto(item.UpdatedAt),
} }
ownerEmail, err := s.loadAdminUserEmail(ctx, item.UserID) ownerEmail, err := s.loadAdminUserEmail(ctx, item.UserID)

View File

@@ -40,14 +40,19 @@ func toProtoPopupAd(item *model.PopupAd) *appv1.PopupAd {
return nil return nil
} }
return &appv1.PopupAd{ return &appv1.PopupAd{
Id: item.ID, Id: item.ID,
Type: item.Type, Type: item.Type,
Label: item.Label, Label: item.Label,
Value: item.Value, Value: item.Value,
IsActive: boolValue(item.IsActive), IsActive: boolValue(item.IsActive),
MaxTriggersPerSession: func() int32 { if item.MaxTriggersPerSession != nil { return *item.MaxTriggersPerSession }; return 0 }(), MaxTriggersPerSession: func() int32 {
CreatedAt: timeToProto(item.CreatedAt), if item.MaxTriggersPerSession != nil {
UpdatedAt: timeToProto(item.UpdatedAt), return *item.MaxTriggersPerSession
}
return 0
}(),
CreatedAt: timeToProto(item.CreatedAt),
UpdatedAt: timeToProto(item.UpdatedAt),
} }
} }

View File

@@ -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) 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 CancelJob(ctx context.Context, id string) error
RetryJob(ctx context.Context, id string) (*model.Job, 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 { type VideoRepository interface {
@@ -199,6 +203,12 @@ type JobRepository interface {
ListByOffset(ctx context.Context, agentID string, offset int, limit int) ([]*model.Job, int64, error) ListByOffset(ctx context.Context, agentID string, offset int, limit int) ([]*model.Job, int64, error)
Save(ctx context.Context, job *model.Job) error Save(ctx context.Context, job *model.Job) error
UpdateVideoStatus(ctx context.Context, videoID string, statusValue string, processingStatus string) 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 { type AgentRuntime interface {

View File

@@ -8,9 +8,9 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
"google.golang.org/grpc/metadata" "google.golang.org/grpc/metadata"
_ "modernc.org/sqlite"
"gorm.io/driver/sqlite" "gorm.io/driver/sqlite"
"gorm.io/gorm" "gorm.io/gorm"
_ "modernc.org/sqlite"
appv1 "stream.api/internal/api/proto/app/v1" appv1 "stream.api/internal/api/proto/app/v1"
"stream.api/internal/database/model" "stream.api/internal/database/model"
"stream.api/internal/database/query" "stream.api/internal/database/query"
@@ -89,8 +89,8 @@ func notificationTestContext(userID, role string) context.Context {
)) ))
} }
func notificationPtrString(v string) *string { return &v } func notificationPtrString(v string) *string { return &v }
func notificationPtrBool(v bool) *bool { return &v } func notificationPtrBool(v bool) *bool { return &v }
func notificationPtrFloat64(v float64) *float64 { return &v } func notificationPtrFloat64(v float64) *float64 { return &v }
func seedNotificationUser(t *testing.T, db *gorm.DB, user model.User) model.User { func seedNotificationUser(t *testing.T, db *gorm.DB, user model.User) model.User {

View File

@@ -65,12 +65,7 @@ func (s *appServices) buildReferralShareLink(username *string) *string {
return nil return nil
} }
path := "/ref/" + url.PathEscape(trimmed) path := "/ref/" + url.PathEscape(trimmed)
base := strings.TrimRight(strings.TrimSpace(s.frontendBaseURL), "/") return &path
if base == "" {
return &path
}
link := base + path
return &link
} }
func (s *appServices) loadReferralUsersByUsername(ctx context.Context, username string) ([]model.User, error) { func (s *appServices) loadReferralUsersByUsername(ctx context.Context, username string) ([]model.User, error) {

View File

@@ -16,5 +16,6 @@ func Register(server grpc.ServiceRegistrar, services *Services) {
appv1.RegisterPlansServer(server, services.PlansServer) appv1.RegisterPlansServer(server, services.PlansServer)
appv1.RegisterPaymentsServer(server, services.PaymentsServer) appv1.RegisterPaymentsServer(server, services.PaymentsServer)
appv1.RegisterVideosServer(server, services.VideosServer) appv1.RegisterVideosServer(server, services.VideosServer)
appv1.RegisterVideoMetadataServer(server, services.VideoMetadataServer)
appv1.RegisterAdminServer(server, services.AdminServer) appv1.RegisterAdminServer(server, services.AdminServer)
} }

View File

@@ -171,6 +171,88 @@ func (s *appServices) RetryAdminJob(ctx context.Context, req *appv1.RetryAdminJo
} }
return &appv1.RetryAdminJobResponse{Job: buildAdminJob(job)}, nil 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) { func (s *appServices) ListAdminAgents(ctx context.Context, _ *appv1.ListAdminAgentsRequest) (*appv1.ListAdminAgentsResponse, error) {
if _, err := s.requireAdmin(ctx); err != nil { if _, err := s.requireAdmin(ctx); err != nil {
return nil, err return nil, err

View File

@@ -53,6 +53,7 @@ type Services struct {
appv1.PlansServer appv1.PlansServer
appv1.PaymentsServer appv1.PaymentsServer
appv1.VideosServer appv1.VideosServer
appv1.VideoMetadataServer
appv1.AdminServer appv1.AdminServer
} }
@@ -79,6 +80,7 @@ type appServices struct {
appv1.UnimplementedPlansServer appv1.UnimplementedPlansServer
appv1.UnimplementedPaymentsServer appv1.UnimplementedPaymentsServer
appv1.UnimplementedVideosServer appv1.UnimplementedVideosServer
appv1.UnimplementedVideoMetadataServer
appv1.UnimplementedAdminServer appv1.UnimplementedAdminServer
db *gorm.DB db *gorm.DB
@@ -104,7 +106,6 @@ type appServices struct {
googleOauth *oauth2.Config googleOauth *oauth2.Config
googleStateTTL time.Duration googleStateTTL time.Duration
googleUserInfoURL string googleUserInfoURL string
frontendBaseURL string
jobRepository JobRepository 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{ service := &appServices{
db: db, db: db,
logger: l, logger: l,
@@ -202,7 +198,6 @@ func NewServices(c *redis.RedisAdapter, db *gorm.DB, l logger.Logger, cfg *confi
googleOauth: googleOauth, googleOauth: googleOauth,
googleStateTTL: googleStateTTL, googleStateTTL: googleStateTTL,
googleUserInfoURL: defaultGoogleUserInfoURL, googleUserInfoURL: defaultGoogleUserInfoURL,
frontendBaseURL: frontendBaseURL,
} }
return &Services{ return &Services{
AuthServer: &authAppService{appServices: service}, 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}, PlansServer: &plansAppService{appServices: service},
PaymentsServer: &paymentsAppService{appServices: service}, PaymentsServer: &paymentsAppService{appServices: service},
VideosServer: &videosAppService{appServices: service}, VideosServer: &videosAppService{appServices: service},
VideoMetadataServer: &videosAppService{appServices: service},
AdminServer: &adminAppService{appServices: service}, AdminServer: &adminAppService{appServices: service},
} }
} }

View File

@@ -12,9 +12,11 @@ import (
"time" "time"
"gorm.io/gorm" "gorm.io/gorm"
redisadapter "stream.api/internal/adapters/redis"
"stream.api/internal/database/model" "stream.api/internal/database/model"
"stream.api/internal/dto" "stream.api/internal/dto"
"stream.api/internal/repository" "stream.api/internal/repository"
"stream.api/pkg/logger"
) )
type JobQueue interface { type JobQueue interface {
@@ -33,20 +35,36 @@ type LogPubSub interface {
SubscribeJobUpdates(ctx context.Context) (<-chan string, error) 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 { type JobService struct {
queue JobQueue queue JobQueue
pubsub LogPubSub pubsub LogPubSub
dlq DeadLetterQueue
jobRepository JobRepository 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{ return &JobService{
queue: queue, queue: queue,
pubsub: pubsub, pubsub: pubsub,
dlq: dlq,
jobRepository: repository.NewJobRepository(db), jobRepository: repository.NewJobRepository(db),
} }
} }
func (s *JobService) SetLogger(l logger.Logger) {
s.logger = l
}
var ErrInvalidJobCursor = errors.New("invalid job cursor") var ErrInvalidJobCursor = errors.New("invalid job cursor")
func strPtr(v string) *string { return &v } 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 { if err := syncVideoStatus(ctx, s.jobRepository, videoID, dto.JobStatusPending); err != nil {
return nil, err 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 { if err := s.queue.Enqueue(ctx, job); err != nil {
return nil, err return nil, err
} }
_ = s.pubsub.PublishJobUpdate(ctx, job.ID, status, videoID) _ = s.publishJobUpdate(ctx, job.ID, status, videoID)
return job, nil 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) { 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) { 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) return s.pubsub.SubscribeResources(ctx)
} }
func (s *JobService) SubscribeJobLogs(ctx context.Context, jobID string) (<-chan dto.LogEntry, error) { 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) return s.pubsub.Subscribe(ctx, jobID)
} }
func (s *JobService) SubscribeCancel(ctx context.Context, agentID string) (<-chan string, error) { 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) return s.pubsub.SubscribeCancel(ctx, agentID)
} }
func (s *JobService) SubscribeJobUpdates(ctx context.Context) (<-chan string, error) { 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) return s.pubsub.SubscribeJobUpdates(ctx)
} }
@@ -253,17 +313,38 @@ func (s *JobService) UpdateJobStatus(ctx context.Context, jobID string, status d
if err != nil { if err != nil {
return err 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() now := time.Now()
job.Status = strPtr(string(status)) updated := false
job.UpdatedAt = &now switch status {
if err := s.jobRepository.Save(ctx, job); err != nil { 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 return err
} }
if !updated {
return nil
}
cfg := parseJobConfig(job.Config) cfg := parseJobConfig(job.Config)
if err := syncVideoStatus(ctx, s.jobRepository, cfg.VideoID, status); err != nil { if err := syncVideoStatus(ctx, s.jobRepository, cfg.VideoID, status); err != nil {
return err 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 { 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 { if err != nil {
return err return err
} }
if !s.canDispatchJob(job) {
return fmt.Errorf("job %s is not dispatchable", jobID)
}
agentNumeric, err := strconv.ParseInt(agentID, 10, 64) agentNumeric, err := strconv.ParseInt(agentID, 10, 64)
if err != nil { if err != nil {
return err return err
} }
now := time.Now() updated, err := s.jobRepository.AssignPendingJob(ctx, jobID, agentNumeric, time.Now())
status := string(dto.JobStatusRunning) if err != nil {
job.AgentID = &agentNumeric
job.Status = &status
job.UpdatedAt = &now
if err := s.jobRepository.Save(ctx, job); err != nil {
return err return err
} }
if !updated {
return fmt.Errorf("job %s is not dispatchable", jobID)
}
cfg := parseJobConfig(job.Config) cfg := parseJobConfig(job.Config)
if err := syncVideoStatus(ctx, s.jobRepository, cfg.VideoID, dto.JobStatusRunning); err != nil { if err := syncVideoStatus(ctx, s.jobRepository, cfg.VideoID, dto.JobStatusRunning); err != nil {
return err 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 { 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 { if err != nil {
return fmt.Errorf("job not found: %w", err) return fmt.Errorf("job not found: %w", err)
} }
currentStatus := "" updated, err := s.jobRepository.CancelJobIfActive(ctx, jobID, time.Now())
if job.Status != nil { if err != 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 {
return err return err
} }
if !updated {
return fmt.Errorf("cannot cancel job with status %s", s.jobStatus(job))
}
cfg := parseJobConfig(job.Config) cfg := parseJobConfig(job.Config)
if err := syncVideoStatus(ctx, s.jobRepository, cfg.VideoID, dto.JobStatusCancelled); err != nil { if err := syncVideoStatus(ctx, s.jobRepository, cfg.VideoID, dto.JobStatusCancelled); err != nil {
return err return err
} }
_ = s.pubsub.PublishJobUpdate(ctx, jobID, status, cfg.VideoID) _ = s.publishJobUpdate(ctx, jobID, string(dto.JobStatusCancelled), cfg.VideoID)
if job.AgentID != nil { 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) _ = 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) { 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 { if err != nil {
return nil, fmt.Errorf("job not found: %w", err) return nil, fmt.Errorf("job not found: %w", err)
} }
currentStatus := "" currentStatus := s.jobStatus(job)
if job.Status != nil { if currentStatus != dto.JobStatusFailure && currentStatus != dto.JobStatusCancelled {
currentStatus = *job.Status
}
if currentStatus != string(dto.JobStatusFailure) && currentStatus != string(dto.JobStatusCancelled) {
return nil, fmt.Errorf("cannot retry job with status %s", currentStatus) return nil, fmt.Errorf("cannot retry job with status %s", currentStatus)
} }
currentRetry := int64(0) if err := s.requeueJob(ctx, job, false); err != nil {
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 {
return nil, err return nil, err
} }
cfg := parseJobConfig(job.Config) if s.dlq != nil {
if err := syncVideoStatus(ctx, s.jobRepository, cfg.VideoID, dto.JobStatusPending); err != nil { _ = s.dlq.Remove(ctx, jobID)
return nil, err
} }
// dtoJob := todtoJob(job) return s.jobRepository.GetByID(ctx, jobID)
if err := s.queue.Enqueue(ctx, job); err != nil {
return nil, err
}
_ = s.pubsub.PublishJobUpdate(ctx, jobID, pending, cfg.VideoID)
return job, nil
} }
func (s *JobService) UpdateJobProgress(ctx context.Context, jobID string, progress float64) error { 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 { if err := s.jobRepository.Save(ctx, job); err != nil {
return err 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 { 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 { if err := s.jobRepository.Save(ctx, job); err != nil {
return err 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 { 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 { 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) 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
}

View File

@@ -7,12 +7,12 @@ import (
"time" "time"
"github.com/google/uuid" "github.com/google/uuid"
_ "modernc.org/sqlite"
"google.golang.org/grpc/codes" "google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata" "google.golang.org/grpc/metadata"
"google.golang.org/grpc/status" "google.golang.org/grpc/status"
"gorm.io/driver/sqlite" "gorm.io/driver/sqlite"
"gorm.io/gorm" "gorm.io/gorm"
_ "modernc.org/sqlite"
appv1 "stream.api/internal/api/proto/app/v1" appv1 "stream.api/internal/api/proto/app/v1"
"stream.api/internal/database/model" "stream.api/internal/database/model"
"stream.api/internal/database/query" "stream.api/internal/database/query"
@@ -152,9 +152,9 @@ func popupTestContext(userID, role string) context.Context {
)) ))
} }
func popupPtrString(v string) *string { return &v } func popupPtrString(v string) *string { return &v }
func popupPtrBool(v bool) *bool { return &v } func popupPtrBool(v bool) *bool { return &v }
func popupPtrInt32(v int32) *int32 { return &v } func popupPtrInt32(v int32) *int32 { return &v }
func popupPtrTime(v time.Time) *time.Time { return &v } func popupPtrTime(v time.Time) *time.Time { return &v }
func popupSeedUser(t *testing.T, db *gorm.DB, user model.User) model.User { 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")}) 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{ createResp, err := (&popupAdsAppService{appServices: services}).CreatePopupAd(popupTestContext(user.ID, "USER"), &appv1.CreatePopupAdRequest{
Type: "url", Type: "url",
Label: "Homepage Campaign", Label: "Homepage Campaign",
Value: "https://example.com/landing", Value: "https://example.com/landing",
IsActive: popupPtrBool(true), IsActive: popupPtrBool(true),
MaxTriggersPerSession: popupPtrInt32(5), MaxTriggersPerSession: popupPtrInt32(5),
}) })
if err != nil { if err != nil {
@@ -231,11 +231,11 @@ func TestPopupAdsUserFlow(t *testing.T) {
} }
updateResp, err := (&popupAdsAppService{appServices: services}).UpdatePopupAd(popupTestContext(user.ID, "USER"), &appv1.UpdatePopupAdRequest{ updateResp, err := (&popupAdsAppService{appServices: services}).UpdatePopupAd(popupTestContext(user.ID, "USER"), &appv1.UpdatePopupAdRequest{
Id: createResp.Item.Id, Id: createResp.Item.Id,
Type: "script", Type: "script",
Label: "Homepage Campaign v2", Label: "Homepage Campaign v2",
Value: `<script async src="//example.com/ad.js"></script>`, Value: `<script async src="//example.com/ad.js"></script>`,
IsActive: popupPtrBool(false), IsActive: popupPtrBool(false),
MaxTriggersPerSession: popupPtrInt32(8), MaxTriggersPerSession: popupPtrInt32(8),
}) })
if err != nil { 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")}) 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{ createResp, err := services.CreateAdminPopupAd(popupTestContext(admin.ID, "ADMIN"), &appv1.CreateAdminPopupAdRequest{
UserId: user.ID, UserId: user.ID,
Type: "url", Type: "url",
Label: "Admin Campaign", Label: "Admin Campaign",
Value: "https://example.com/admin", Value: "https://example.com/admin",
IsActive: popupPtrBool(true), IsActive: popupPtrBool(true),
MaxTriggersPerSession: popupPtrInt32(7), MaxTriggersPerSession: popupPtrInt32(7),
}) })
if err != nil { if err != nil {
@@ -325,12 +325,12 @@ func TestPopupAdsAdminFlow(t *testing.T) {
} }
updateResp, err := services.UpdateAdminPopupAd(popupTestContext(admin.ID, "ADMIN"), &appv1.UpdateAdminPopupAdRequest{ updateResp, err := services.UpdateAdminPopupAd(popupTestContext(admin.ID, "ADMIN"), &appv1.UpdateAdminPopupAdRequest{
Id: createResp.Item.Id, Id: createResp.Item.Id,
UserId: user.ID, UserId: user.ID,
Type: "script", Type: "script",
Label: "Admin Campaign v2", Label: "Admin Campaign v2",
Value: `<script async src="//example.com/admin-v2.js"></script>`, Value: `<script async src="//example.com/admin-v2.js"></script>`,
IsActive: popupPtrBool(false), IsActive: popupPtrBool(false),
MaxTriggersPerSession: popupPtrInt32(11), MaxTriggersPerSession: popupPtrInt32(11),
}) })
if err != nil { if err != nil {

View File

@@ -12,6 +12,7 @@ import (
"google.golang.org/grpc/status" "google.golang.org/grpc/status"
"gorm.io/gorm" "gorm.io/gorm"
appv1 "stream.api/internal/api/proto/app/v1" 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) { 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 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) { func (s *videosAppService) UpdateVideo(ctx context.Context, req *appv1.UpdateVideoRequest) (*appv1.UpdateVideoResponse, error) {
result, err := s.authenticate(ctx) result, err := s.authenticate(ctx)
if err != nil { if err != nil {
@@ -216,6 +305,59 @@ func (s *videosAppService) UpdateVideo(ctx context.Context, req *appv1.UpdateVid
} }
return &appv1.UpdateVideoResponse{Video: payload}, nil 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) { func (s *videosAppService) DeleteVideo(ctx context.Context, req *appv1.DeleteVideoRequest) (*appv1.MessageResponse, error) {
result, err := s.authenticate(ctx) result, err := s.authenticate(ctx)
if err != nil { if err != nil {

View File

@@ -7,7 +7,6 @@ import (
"google.golang.org/grpc/codes" "google.golang.org/grpc/codes"
"google.golang.org/grpc/status" "google.golang.org/grpc/status"
proto "stream.api/internal/api/proto/agent/v1" 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) { 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") return nil, status.Error(codes.Unauthenticated, "invalid session")
} }
for _, jobID := range s.getAgentJobs(agentID) { for _, jobID := range s.getAgentJobs(agentID) {
_ = s.jobService.UpdateJobStatus(ctx, jobID, dto.JobStatusFailure) _ = s.jobService.HandleAgentDisconnect(ctx, jobID)
s.untrackJobAssignment(agentID, jobID) s.untrackJobAssignment(agentID, jobID)
} }
s.sessions.Delete(token) s.sessions.Delete(token)

View File

@@ -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) { func (s *Server) Auth(ctx context.Context, req *proto.AuthRequest) (*proto.AuthResponse, error) {
if s.agentSecret != "" && req.AgentToken != s.agentSecret { 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 agentID := req.AgentId
if len(agentID) > 6 && agentID[:6] == "agent-" { if len(agentID) > 6 && agentID[:6] == "agent-" {

View File

@@ -3,6 +3,7 @@ package grpc
import ( import (
"context" "context"
"net" "net"
"time"
grpcpkg "google.golang.org/grpc" grpcpkg "google.golang.org/grpc"
"gorm.io/gorm" "gorm.io/gorm"
@@ -21,11 +22,14 @@ type GRPCModule struct {
mqttPublisher *mqtt.MQTTBootstrap mqttPublisher *mqtt.MQTTBootstrap
grpcServer *grpcpkg.Server grpcServer *grpcpkg.Server
cfg *config.Config 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) { 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) moduleCtx, cancel := context.WithCancel(ctx)
agentRuntime := NewServer(jobService, cfg.Render.AgentSecret) jobService := service.NewJobService(db, rds, rds, redisadapter.NewDeadLetterQueue(rds.Client()))
jobService.SetLogger(appLogger)
agentRuntime := NewServer(jobService, cfg.Internal.Marker)
videoService := renderworkflow.New(db, jobService) videoService := renderworkflow.New(db, jobService)
grpcServer := grpcpkg.NewServer() grpcServer := grpcpkg.NewServer()
@@ -34,6 +38,7 @@ func NewGRPCModule(ctx context.Context, cfg *config.Config, db *gorm.DB, rds *re
agentRuntime: agentRuntime, agentRuntime: agentRuntime,
grpcServer: grpcServer, grpcServer: grpcServer,
cfg: cfg, cfg: cfg,
cancel: cancel,
} }
var notificationPublisher service.NotificationEventPublisher = nil 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) agentRuntime.Register(grpcServer)
service.Register(grpcServer, service.NewServices(rds, db, appLogger, cfg, videoService, agentRuntime, notificationPublisher)) service.Register(grpcServer, service.NewServices(rds, db, appLogger, cfg, videoService, agentRuntime, notificationPublisher))
if module.mqttPublisher != nil { if module.mqttPublisher != nil {
module.mqttPublisher.Start(ctx) module.mqttPublisher.Start(moduleCtx)
} }
go jobService.StartInflightReclaimLoop(moduleCtx, 30*time.Second, 100)
return module, nil 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) GRPCAddress() string { return ":" + m.cfg.Server.GRPCPort }
func (m *GRPCModule) ServeGRPC(listener net.Listener) error { return m.grpcServer.Serve(listener) } func (m *GRPCModule) ServeGRPC(listener net.Listener) error { return m.grpcServer.Serve(listener) }
func (m *GRPCModule) Shutdown() { func (m *GRPCModule) Shutdown() {
if m.cancel != nil {
m.cancel()
}
if m.grpcServer != nil { if m.grpcServer != nil {
m.grpcServer.GracefulStop() m.grpcServer.GracefulStop()
} }

View File

@@ -53,12 +53,14 @@ func (s *Server) StreamJobs(_ *proto.StreamOptions, stream grpcpkg.ServerStreami
} }
s.trackJobAssignment(agentID, job.ID) s.trackJobAssignment(agentID, job.ID)
if err := s.jobService.AssignJob(ctx, job.ID, agentID); err != nil { 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) s.untrackJobAssignment(agentID, job.ID)
continue continue
} }
var config map[string]any var config map[string]any
if err := json.Unmarshal([]byte(*job.Config), &config); err != nil { if job.Config == nil || json.Unmarshal([]byte(*job.Config), &config) != nil {
_ = s.jobService.UpdateJobStatus(ctx, job.ID, dto.JobStatusFailure) _ = s.jobService.HandleDispatchFailure(ctx, job.ID, "invalid_config", false)
s.untrackJobAssignment(agentID, job.ID) s.untrackJobAssignment(agentID, job.ID)
continue 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{}}) 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 { 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) s.untrackJobAssignment(agentID, job.ID)
return err return err
} }
@@ -101,8 +103,10 @@ func (s *Server) SubmitStatus(stream grpcpkg.ClientStreamingServer[proto.StatusU
} }
switch update.Type { switch update.Type {
case 0, 1: case 0, 1:
_ = s.jobService.RenewJobLease(ctx, update.StepUuid)
_ = s.jobService.ProcessLog(ctx, update.StepUuid, update.Data) _ = s.jobService.ProcessLog(ctx, update.StepUuid, update.Data)
case 4: case 4:
_ = s.jobService.RenewJobLease(ctx, update.StepUuid)
var progress float64 var progress float64
fmt.Sscanf(string(update.Data), "%f", &progress) fmt.Sscanf(string(update.Data), "%f", &progress)
_ = s.jobService.UpdateJobProgress(ctx, update.StepUuid, 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 { if err := s.jobService.UpdateJobStatus(ctx, req.Id, dto.JobStatusRunning); err != nil {
return nil, status.Error(codes.Internal, "failed to update job status") return nil, status.Error(codes.Internal, "failed to update job status")
} }
_ = s.jobService.RenewJobLease(ctx, req.Id)
return &proto.Empty{}, nil return &proto.Empty{}, nil
} }
@@ -138,11 +143,13 @@ func (s *Server) Done(ctx context.Context, req *proto.DoneRequest) (*proto.Empty
if !ok { if !ok {
return nil, status.Error(codes.Unauthenticated, "invalid session") return nil, status.Error(codes.Unauthenticated, "invalid session")
} }
jobStatus := dto.JobStatusSuccess var err error
if req.State != nil && req.State.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") return nil, status.Error(codes.Internal, "failed to update job status")
} }
s.untrackJobAssignment(agentID, req.Id) 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 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 return &proto.Empty{}, nil
} }

View File

@@ -25,7 +25,9 @@ func NewNotificationPublisher(client pahomqtt.Client, appLogger logger.Logger) s
type serviceNotificationNoop struct{} 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 { func (p *notificationPublisher) PublishNotificationCreated(_ context.Context, notification *model.Notification) error {
if p == nil || notification == nil { if p == nil || notification == nil {
@@ -41,4 +43,3 @@ func (p *notificationPublisher) PublishNotificationCreated(_ context.Context, no
func (p *notificationPublisher) notificationTopic(userID string) string { func (p *notificationPublisher) notificationTopic(userID string) string {
return fmt.Sprintf("%s/notifications/%s", p.prefix, userID) return fmt.Sprintf("%s/notifications/%s", p.prefix, userID)
} }

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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)
}
})
}
}

View File

@@ -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) 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 CancelJob(ctx context.Context, id string) error
RetryJob(ctx context.Context, id string) (*model.Job, 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 { 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) 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) { func buildJobPayload(videoID, userID, videoURL, format string) ([]byte, error) {
return json.Marshal(map[string]any{ return json.Marshal(map[string]any{
"video_id": videoID, "video_id": videoID,

View File

@@ -49,6 +49,10 @@ service Admin {
rpc CreateAdminJob(CreateAdminJobRequest) returns (CreateAdminJobResponse); rpc CreateAdminJob(CreateAdminJobRequest) returns (CreateAdminJobResponse);
rpc CancelAdminJob(CancelAdminJobRequest) returns (CancelAdminJobResponse); rpc CancelAdminJob(CancelAdminJobRequest) returns (CancelAdminJobResponse);
rpc RetryAdminJob(RetryAdminJobRequest) returns (RetryAdminJobResponse); 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 ListAdminAgents(ListAdminAgentsRequest) returns (ListAdminAgentsResponse);
rpc RestartAdminAgent(RestartAdminAgentRequest) returns (AdminAgentCommandResponse); rpc RestartAdminAgent(RestartAdminAgentRequest) returns (AdminAgentCommandResponse);
rpc UpdateAdminAgent(UpdateAdminAgentRequest) returns (AdminAgentCommandResponse); rpc UpdateAdminAgent(UpdateAdminAgentRequest) returns (AdminAgentCommandResponse);
@@ -533,6 +537,43 @@ message RetryAdminJobResponse {
AdminJob job = 1; 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 ListAdminAgentsRequest {}
message ListAdminAgentsResponse { message ListAdminAgentsResponse {

View File

@@ -392,3 +392,10 @@ message AdminAgent {
google.protobuf.Timestamp created_at = 11; google.protobuf.Timestamp created_at = 11;
google.protobuf.Timestamp updated_at = 12; google.protobuf.Timestamp updated_at = 12;
} }
message AdminDlqEntry {
AdminJob job = 1;
google.protobuf.Timestamp failure_time = 2;
string reason = 3;
int32 retry_count = 4;
}

View File

@@ -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;
}

376
script/create_database.sql Normal file
View File

@@ -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$
;