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
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -installsuffix cgo -ldflags="-s -w" -o main ./cmd/grpc
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -installsuffix cgo -ldflags="-s -w" -o server ./cmd/server
FROM scratch
COPY --from=builder /app/main /main
COPY --from=builder /app/server /server
EXPOSE 8080
ENTRYPOINT ["/main"]
EXPOSE 9000
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
appLogger := logger.NewLogger(cfg.Server.Mode)
module, err := grpc.NewGRPCModule(context.Background(), cfg, db, rdb, appLogger)
appCtx, appCancel := context.WithCancel(context.Background())
defer appCancel()
module, err := grpc.NewGRPCModule(appCtx, cfg, db, rdb, appLogger)
if err != nil {
log.Fatalf("Failed to setup gRPC runtime module: %v", err)
}

View File

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

View File

@@ -1,5 +1,4 @@
server:
port: "8080"
grpc_port: "9000"
mode: "debug" # debug or release
@@ -11,28 +10,15 @@ redis:
password: ""
db: 0
jwt:
secret: "your_super_secret_jwt_key"
google:
client_id: "your_google_client_id"
client_secret: "your_google_client_secret"
redirect_url: "http://localhost:8080/auth/google/callback"
state_ttl_minutes: 10
frontend:
base_url: "http://localhost:5173"
google_auth_finalize_path: "/auth/google/finalize"
internal:
marker: "your_shared_internal_auth_marker"
cors:
allow_origins:
- "http://localhost:5173"
- "http://localhost:8080"
- "http://localhost:8081"
email:
from: "no-reply@picpic.com"
@@ -43,8 +29,11 @@ aws:
secret_key: "your_secret_key"
render:
agent_secret: "your_render_agent_secret"
enable_metrics: true
enable_tracing: false
otlp_endpoint: ""
service_name: "stream-api-render"
# Agent runtime uses environment variables rather than this YAML file.
# Required: APP_INTERNAL_MARKER
# Optional: AGENT_SERVER (default localhost:9000), FORCE_NEW_ID, AGENT_IMAGE, HOST_DOCKER_SOCK

View File

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

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/credentials v1.19.7
github.com/aws/aws-sdk-go-v2/service/s3 v1.95.1
github.com/docker/docker v26.1.5+incompatible
github.com/eclipse/paho.mqtt.golang v1.5.1
github.com/google/uuid v1.6.0
github.com/lib/pq v1.11.2
@@ -26,10 +27,33 @@ require (
)
require (
github.com/Microsoft/go-winio v0.4.21 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/docker/go-connections v0.6.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/term v0.5.2 // indirect
github.com/morikuni/aec v1.1.0 // indirect
github.com/ncruces/go-strftime v1.0.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 // indirect
go.opentelemetry.io/otel v1.42.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.42.0 // indirect
go.opentelemetry.io/otel/metric v1.42.0 // indirect
go.opentelemetry.io/otel/trace v1.42.0 // indirect
golang.org/x/time v0.15.0 // indirect
gotest.tools/v3 v3.5.2 // indirect
modernc.org/libc v1.70.0 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
@@ -67,7 +91,6 @@ require (
github.com/jinzhu/now v1.1.5 // indirect
github.com/mattn/go-sqlite3 v1.14.22 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/sagikazarmark/locafero v0.11.0 // indirect
github.com/sony/gobreaker v1.0.0
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
@@ -78,12 +101,12 @@ require (
go.uber.org/multierr v1.10.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/mod v0.33.0 // indirect
golang.org/x/net v0.50.0 // indirect
golang.org/x/net v0.51.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/text v0.34.0 // indirect
golang.org/x/tools v0.42.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect
gorm.io/datatypes v1.2.4
gorm.io/driver/mysql v1.5.7 // indirect
gorm.io/hints v1.1.0 // indirect

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=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Microsoft/go-winio v0.4.21 h1:+6mVbXh4wPzUrl1COX9A+ZCvEpYsOBZ6/+kwDnvLyro=
github.com/Microsoft/go-winio v0.4.21/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84=
github.com/aws/aws-sdk-go-v2 v1.41.1 h1:ABlyEARCDLN034NhxlRUSZr4l71mh+T5KAeGh6cerhU=
github.com/aws/aws-sdk-go-v2 v1.41.1/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 h1:489krEF9xIGkOaaX3CE/Be2uWjiXrkCH6gUX+bZA/BU=
@@ -44,21 +48,36 @@ github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/docker v26.1.5+incompatible h1:NEAxTwEjxV6VbBMBoGG3zPqbiJosIApZjxlbrG9q3/g=
github.com/docker/docker v26.1.5+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/eclipse/paho.mqtt.golang v1.5.1 h1:/VSOv3oDLlpqR2Epjn1Q7b2bSTplJIeV2ISgCl2W7nE=
github.com/eclipse/paho.mqtt.golang v1.5.1/go.mod h1:1/yJCneuyOoCOzKSsOTUc0AJfpsItBGWvYpBLimhArU=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
@@ -68,6 +87,8 @@ github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpv
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
@@ -82,6 +103,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
@@ -97,6 +120,8 @@ github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkr
github.com/jinzhu/now v1.1.2/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@@ -110,10 +135,22 @@ github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/microsoft/go-mssqldb v0.17.0 h1:Fto83dMZPnYv1Zwx5vHHxpNraeEaUlQ/hhHLgZiaenE=
github.com/microsoft/go-mssqldb v0.17.0/go.mod h1:OkoNGhGEs8EZqchVTtochlXruEhEOaO4S0d2sB5aeGQ=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
github.com/morikuni/aec v1.1.0 h1:vBBl0pUnvi/Je71dsRrhMBtreIqNMYErSAbEeb8jrXQ=
github.com/morikuni/aec v1.1.0/go.mod h1:xDRgiq/iw5l+zkao76YTKzKttOp2cwPEne25HDkJnBw=
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/redis/go-redis/v9 v9.17.2 h1:P2EGsA4qVIM3Pp+aPocCJ7DguDHhqrXNhVcEp4ViluI=
@@ -124,6 +161,9 @@ github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0t
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/sony/gobreaker v1.0.0 h1:feX5fGGXSl3dYd4aHZItw+FpHLvvoaqkawKjVNiFMNQ=
github.com/sony/gobreaker v1.0.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
@@ -137,24 +177,35 @@ github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3A
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg=
go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho=
go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 h1:THuZiwpQZuHPul65w4WcwEnkX2QIuMT+UFoOrygtoJw=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0/go.mod h1:J2pvYM5NGHofZ2/Ru6zw/TNWnEQp5crgyDeSrYpXkAw=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.42.0 h1:uLXP+3mghfMf7XmV4PkGfFhFKuNWoCvvx5wP/wOXo0o=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.42.0/go.mod h1:v0Tj04armyT59mnURNUJf7RCKcKzq+lgJs6QSjHjaTc=
go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4=
go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI=
go.opentelemetry.io/otel/sdk v1.42.0 h1:LyC8+jqk6UJwdrI/8VydAq/hvkFKNHZVIWuslJXYsDo=
go.opentelemetry.io/otel/sdk v1.42.0/go.mod h1:rGHCAxd9DAph0joO4W6OPwxjNTYWghRWmkHuGbayMts=
go.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9RKCAZ3YGuA=
go.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc=
go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY=
go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc=
go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
@@ -163,27 +214,58 @@ go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 h1:JLQynH/LBHfCTSbDWl+py8C+Rg/k1OVH3xfcaiANuF0=
google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:kSJwQxqmFXeo79zOmbrALdflXQeAYcUbgS7PbpMknCY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU=
google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
@@ -216,6 +298,8 @@ gorm.io/hints v1.1.0 h1:Lp4z3rxREufSdxn4qmkK3TLDltrM10FLTHiuqwDPvXw=
gorm.io/hints v1.1.0/go.mod h1:lKQ0JjySsPBj3uslFzY3JhYDtqEwzm+G1hv8rWujB6Y=
gorm.io/plugin/dbresolver v1.6.2 h1:F4b85TenghUeITqe3+epPSUtHH7RIk3fXr5l83DF8Pc=
gorm.io/plugin/dbresolver v1.6.2/go.mod h1:tctw63jdrOezFR9HmrKnPkmig3m5Edem9fdxk9bQSzM=
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.32.0 h1:hjG66bI/kqIPX1b2yT6fr/jt+QedtP2fqojG2VrFuVw=

View File

@@ -3,7 +3,9 @@ package redis
import (
"context"
"encoding/json"
"errors"
"fmt"
"strings"
"time"
goredis "github.com/redis/go-redis/v9"
@@ -12,14 +14,24 @@ import (
)
const (
JobQueueKey = "render:jobs:queue"
JobQueueKey = "render:jobs:queue:v2"
JobInflightKey = "render:jobs:inflight"
JobInflightMetaKey = "render:jobs:inflight:meta"
JobSequenceKey = "render:jobs:queue:seq"
LogChannel = "render:jobs:logs"
ResourceChannel = "render:agents:resources"
JobUpdateChannel = "render:jobs:updates"
defaultQueuePoll = time.Second
defaultInflightTTL = 15 * time.Minute
)
type RedisAdapter struct{ client *goredis.Client }
type inflightMeta struct {
ReadyScore float64 `json:"ready_score"`
ClaimedAt int64 `json:"claimed_at"`
}
func NewAdapter(addr, password string, db int) (*RedisAdapter, error) {
client := goredis.NewClient(&goredis.Options{Addr: addr, Password: password, DB: db})
if err := client.Ping(context.Background()).Err(); err != nil {
@@ -31,13 +43,26 @@ func NewAdapter(addr, password string, db int) (*RedisAdapter, error) {
func (r *RedisAdapter) Client() *goredis.Client { return r.client }
func (r *RedisAdapter) Enqueue(ctx context.Context, job *model.Job) error {
data, err := json.Marshal(job)
if job == nil || strings.TrimSpace(job.ID) == "" {
return errors.New("job id is required")
}
priority := int64(0)
if job.Priority != nil {
priority = *job.Priority
}
seq, err := r.client.Incr(ctx, JobSequenceKey).Result()
if err != nil {
return err
}
timestamp := time.Now().UnixNano()
score := float64(-(int64(*job.Priority) * 1000000000) - timestamp)
return r.client.ZAdd(ctx, JobQueueKey, goredis.Z{Score: score, Member: data}).Err()
score := float64((-priority * 1_000_000_000_000) + seq)
jobID := strings.TrimSpace(job.ID)
if err := r.client.HDel(ctx, JobInflightMetaKey, jobID).Err(); err != nil {
return err
}
if err := r.client.ZRem(ctx, JobInflightKey, jobID).Err(); err != nil {
return err
}
return r.client.ZAdd(ctx, JobQueueKey, goredis.Z{Score: score, Member: jobID}).Err()
}
func (r *RedisAdapter) Dequeue(ctx context.Context) (*model.Job, error) {
@@ -56,25 +81,55 @@ func (r *RedisAdapter) Dequeue(ctx context.Context) (*model.Job, error) {
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-time.After(time.Second):
case <-time.After(defaultQueuePoll):
continue
}
}
var raw []byte
switch member := res[0].Member.(type) {
case string:
raw = []byte(member)
case []byte:
raw = member
default:
return nil, fmt.Errorf("unexpected redis queue payload type %T", member)
}
var job model.Job
if err := json.Unmarshal(raw, &job); err != nil {
jobID := fmt.Sprintf("%v", res[0].Member)
meta, err := json.Marshal(inflightMeta{ReadyScore: res[0].Score, ClaimedAt: time.Now().Unix()})
if err != nil {
return nil, err
}
return &job, nil
leaseScore := float64(time.Now().Add(defaultInflightTTL).Unix())
pipe := r.client.TxPipeline()
pipe.ZAdd(ctx, JobInflightKey, goredis.Z{Score: leaseScore, Member: jobID})
pipe.HSet(ctx, JobInflightMetaKey, jobID, meta)
if _, err := pipe.Exec(ctx); err != nil {
return nil, err
}
return &model.Job{ID: jobID}, nil
}
}
func (r *RedisAdapter) Ack(ctx context.Context, jobID string) error {
jobID = strings.TrimSpace(jobID)
if jobID == "" {
return nil
}
pipe := r.client.TxPipeline()
pipe.ZRem(ctx, JobQueueKey, jobID)
pipe.ZRem(ctx, JobInflightKey, jobID)
pipe.HDel(ctx, JobInflightMetaKey, jobID)
_, err := pipe.Exec(ctx)
return err
}
func (r *RedisAdapter) ListExpiredInflight(ctx context.Context, now time.Time, limit int64) ([]string, error) {
if limit <= 0 {
limit = 100
}
return r.client.ZRangeByScore(ctx, JobInflightKey, &goredis.ZRangeBy{Min: "-inf", Max: fmt.Sprintf("%d", now.Unix()), Offset: 0, Count: limit}).Result()
}
func (r *RedisAdapter) TouchInflight(ctx context.Context, jobID string, ttl time.Duration) error {
jobID = strings.TrimSpace(jobID)
if jobID == "" {
return nil
}
if ttl <= 0 {
ttl = defaultInflightTTL
}
return r.client.ZAddXX(ctx, JobInflightKey, goredis.Z{Score: float64(time.Now().Add(ttl).Unix()), Member: jobID}).Err()
}
func (r *RedisAdapter) Publish(ctx context.Context, jobID string, logLine string, progress float64) error {
@@ -173,6 +228,7 @@ func (r *RedisAdapter) SubscribeJobUpdates(ctx context.Context) (<-chan string,
}()
return ch, nil
}
func (c *RedisAdapter) Set(ctx context.Context, key string, value interface{}, expiration time.Duration) error {
return c.client.Set(ctx, key, value, expiration).Err()
}

View File

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

View File

@@ -4803,6 +4803,398 @@ func (x *RetryAdminJobResponse) GetJob() *AdminJob {
return nil
}
type ListAdminDlqJobsRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Offset int32 `protobuf:"varint,1,opt,name=offset,proto3" json:"offset,omitempty"`
Limit int32 `protobuf:"varint,2,opt,name=limit,proto3" json:"limit,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ListAdminDlqJobsRequest) Reset() {
*x = ListAdminDlqJobsRequest{}
mi := &file_app_v1_admin_proto_msgTypes[79]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ListAdminDlqJobsRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ListAdminDlqJobsRequest) ProtoMessage() {}
func (x *ListAdminDlqJobsRequest) ProtoReflect() protoreflect.Message {
mi := &file_app_v1_admin_proto_msgTypes[79]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ListAdminDlqJobsRequest.ProtoReflect.Descriptor instead.
func (*ListAdminDlqJobsRequest) Descriptor() ([]byte, []int) {
return file_app_v1_admin_proto_rawDescGZIP(), []int{79}
}
func (x *ListAdminDlqJobsRequest) GetOffset() int32 {
if x != nil {
return x.Offset
}
return 0
}
func (x *ListAdminDlqJobsRequest) GetLimit() int32 {
if x != nil {
return x.Limit
}
return 0
}
type ListAdminDlqJobsResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Items []*AdminDlqEntry `protobuf:"bytes,1,rep,name=items,proto3" json:"items,omitempty"`
Total int64 `protobuf:"varint,2,opt,name=total,proto3" json:"total,omitempty"`
Offset int32 `protobuf:"varint,3,opt,name=offset,proto3" json:"offset,omitempty"`
Limit int32 `protobuf:"varint,4,opt,name=limit,proto3" json:"limit,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ListAdminDlqJobsResponse) Reset() {
*x = ListAdminDlqJobsResponse{}
mi := &file_app_v1_admin_proto_msgTypes[80]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ListAdminDlqJobsResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ListAdminDlqJobsResponse) ProtoMessage() {}
func (x *ListAdminDlqJobsResponse) ProtoReflect() protoreflect.Message {
mi := &file_app_v1_admin_proto_msgTypes[80]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ListAdminDlqJobsResponse.ProtoReflect.Descriptor instead.
func (*ListAdminDlqJobsResponse) Descriptor() ([]byte, []int) {
return file_app_v1_admin_proto_rawDescGZIP(), []int{80}
}
func (x *ListAdminDlqJobsResponse) GetItems() []*AdminDlqEntry {
if x != nil {
return x.Items
}
return nil
}
func (x *ListAdminDlqJobsResponse) GetTotal() int64 {
if x != nil {
return x.Total
}
return 0
}
func (x *ListAdminDlqJobsResponse) GetOffset() int32 {
if x != nil {
return x.Offset
}
return 0
}
func (x *ListAdminDlqJobsResponse) GetLimit() int32 {
if x != nil {
return x.Limit
}
return 0
}
type GetAdminDlqJobRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *GetAdminDlqJobRequest) Reset() {
*x = GetAdminDlqJobRequest{}
mi := &file_app_v1_admin_proto_msgTypes[81]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *GetAdminDlqJobRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GetAdminDlqJobRequest) ProtoMessage() {}
func (x *GetAdminDlqJobRequest) ProtoReflect() protoreflect.Message {
mi := &file_app_v1_admin_proto_msgTypes[81]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use GetAdminDlqJobRequest.ProtoReflect.Descriptor instead.
func (*GetAdminDlqJobRequest) Descriptor() ([]byte, []int) {
return file_app_v1_admin_proto_rawDescGZIP(), []int{81}
}
func (x *GetAdminDlqJobRequest) GetId() string {
if x != nil {
return x.Id
}
return ""
}
type GetAdminDlqJobResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Item *AdminDlqEntry `protobuf:"bytes,1,opt,name=item,proto3" json:"item,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *GetAdminDlqJobResponse) Reset() {
*x = GetAdminDlqJobResponse{}
mi := &file_app_v1_admin_proto_msgTypes[82]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *GetAdminDlqJobResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GetAdminDlqJobResponse) ProtoMessage() {}
func (x *GetAdminDlqJobResponse) ProtoReflect() protoreflect.Message {
mi := &file_app_v1_admin_proto_msgTypes[82]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use GetAdminDlqJobResponse.ProtoReflect.Descriptor instead.
func (*GetAdminDlqJobResponse) Descriptor() ([]byte, []int) {
return file_app_v1_admin_proto_rawDescGZIP(), []int{82}
}
func (x *GetAdminDlqJobResponse) GetItem() *AdminDlqEntry {
if x != nil {
return x.Item
}
return nil
}
type RetryAdminDlqJobRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *RetryAdminDlqJobRequest) Reset() {
*x = RetryAdminDlqJobRequest{}
mi := &file_app_v1_admin_proto_msgTypes[83]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *RetryAdminDlqJobRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*RetryAdminDlqJobRequest) ProtoMessage() {}
func (x *RetryAdminDlqJobRequest) ProtoReflect() protoreflect.Message {
mi := &file_app_v1_admin_proto_msgTypes[83]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use RetryAdminDlqJobRequest.ProtoReflect.Descriptor instead.
func (*RetryAdminDlqJobRequest) Descriptor() ([]byte, []int) {
return file_app_v1_admin_proto_rawDescGZIP(), []int{83}
}
func (x *RetryAdminDlqJobRequest) GetId() string {
if x != nil {
return x.Id
}
return ""
}
type RetryAdminDlqJobResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Job *AdminJob `protobuf:"bytes,1,opt,name=job,proto3" json:"job,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *RetryAdminDlqJobResponse) Reset() {
*x = RetryAdminDlqJobResponse{}
mi := &file_app_v1_admin_proto_msgTypes[84]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *RetryAdminDlqJobResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*RetryAdminDlqJobResponse) ProtoMessage() {}
func (x *RetryAdminDlqJobResponse) ProtoReflect() protoreflect.Message {
mi := &file_app_v1_admin_proto_msgTypes[84]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use RetryAdminDlqJobResponse.ProtoReflect.Descriptor instead.
func (*RetryAdminDlqJobResponse) Descriptor() ([]byte, []int) {
return file_app_v1_admin_proto_rawDescGZIP(), []int{84}
}
func (x *RetryAdminDlqJobResponse) GetJob() *AdminJob {
if x != nil {
return x.Job
}
return nil
}
type RemoveAdminDlqJobRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *RemoveAdminDlqJobRequest) Reset() {
*x = RemoveAdminDlqJobRequest{}
mi := &file_app_v1_admin_proto_msgTypes[85]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *RemoveAdminDlqJobRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*RemoveAdminDlqJobRequest) ProtoMessage() {}
func (x *RemoveAdminDlqJobRequest) ProtoReflect() protoreflect.Message {
mi := &file_app_v1_admin_proto_msgTypes[85]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use RemoveAdminDlqJobRequest.ProtoReflect.Descriptor instead.
func (*RemoveAdminDlqJobRequest) Descriptor() ([]byte, []int) {
return file_app_v1_admin_proto_rawDescGZIP(), []int{85}
}
func (x *RemoveAdminDlqJobRequest) GetId() string {
if x != nil {
return x.Id
}
return ""
}
type RemoveAdminDlqJobResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Status string `protobuf:"bytes,1,opt,name=status,proto3" json:"status,omitempty"`
JobId string `protobuf:"bytes,2,opt,name=job_id,json=jobId,proto3" json:"job_id,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *RemoveAdminDlqJobResponse) Reset() {
*x = RemoveAdminDlqJobResponse{}
mi := &file_app_v1_admin_proto_msgTypes[86]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *RemoveAdminDlqJobResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*RemoveAdminDlqJobResponse) ProtoMessage() {}
func (x *RemoveAdminDlqJobResponse) ProtoReflect() protoreflect.Message {
mi := &file_app_v1_admin_proto_msgTypes[86]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use RemoveAdminDlqJobResponse.ProtoReflect.Descriptor instead.
func (*RemoveAdminDlqJobResponse) Descriptor() ([]byte, []int) {
return file_app_v1_admin_proto_rawDescGZIP(), []int{86}
}
func (x *RemoveAdminDlqJobResponse) GetStatus() string {
if x != nil {
return x.Status
}
return ""
}
func (x *RemoveAdminDlqJobResponse) GetJobId() string {
if x != nil {
return x.JobId
}
return ""
}
type ListAdminAgentsRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
@@ -4811,7 +5203,7 @@ type ListAdminAgentsRequest struct {
func (x *ListAdminAgentsRequest) Reset() {
*x = ListAdminAgentsRequest{}
mi := &file_app_v1_admin_proto_msgTypes[79]
mi := &file_app_v1_admin_proto_msgTypes[87]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -4823,7 +5215,7 @@ func (x *ListAdminAgentsRequest) String() string {
func (*ListAdminAgentsRequest) ProtoMessage() {}
func (x *ListAdminAgentsRequest) ProtoReflect() protoreflect.Message {
mi := &file_app_v1_admin_proto_msgTypes[79]
mi := &file_app_v1_admin_proto_msgTypes[87]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -4836,7 +5228,7 @@ func (x *ListAdminAgentsRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use ListAdminAgentsRequest.ProtoReflect.Descriptor instead.
func (*ListAdminAgentsRequest) Descriptor() ([]byte, []int) {
return file_app_v1_admin_proto_rawDescGZIP(), []int{79}
return file_app_v1_admin_proto_rawDescGZIP(), []int{87}
}
type ListAdminAgentsResponse struct {
@@ -4848,7 +5240,7 @@ type ListAdminAgentsResponse struct {
func (x *ListAdminAgentsResponse) Reset() {
*x = ListAdminAgentsResponse{}
mi := &file_app_v1_admin_proto_msgTypes[80]
mi := &file_app_v1_admin_proto_msgTypes[88]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -4860,7 +5252,7 @@ func (x *ListAdminAgentsResponse) String() string {
func (*ListAdminAgentsResponse) ProtoMessage() {}
func (x *ListAdminAgentsResponse) ProtoReflect() protoreflect.Message {
mi := &file_app_v1_admin_proto_msgTypes[80]
mi := &file_app_v1_admin_proto_msgTypes[88]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -4873,7 +5265,7 @@ func (x *ListAdminAgentsResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use ListAdminAgentsResponse.ProtoReflect.Descriptor instead.
func (*ListAdminAgentsResponse) Descriptor() ([]byte, []int) {
return file_app_v1_admin_proto_rawDescGZIP(), []int{80}
return file_app_v1_admin_proto_rawDescGZIP(), []int{88}
}
func (x *ListAdminAgentsResponse) GetAgents() []*AdminAgent {
@@ -4892,7 +5284,7 @@ type RestartAdminAgentRequest struct {
func (x *RestartAdminAgentRequest) Reset() {
*x = RestartAdminAgentRequest{}
mi := &file_app_v1_admin_proto_msgTypes[81]
mi := &file_app_v1_admin_proto_msgTypes[89]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -4904,7 +5296,7 @@ func (x *RestartAdminAgentRequest) String() string {
func (*RestartAdminAgentRequest) ProtoMessage() {}
func (x *RestartAdminAgentRequest) ProtoReflect() protoreflect.Message {
mi := &file_app_v1_admin_proto_msgTypes[81]
mi := &file_app_v1_admin_proto_msgTypes[89]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -4917,7 +5309,7 @@ func (x *RestartAdminAgentRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use RestartAdminAgentRequest.ProtoReflect.Descriptor instead.
func (*RestartAdminAgentRequest) Descriptor() ([]byte, []int) {
return file_app_v1_admin_proto_rawDescGZIP(), []int{81}
return file_app_v1_admin_proto_rawDescGZIP(), []int{89}
}
func (x *RestartAdminAgentRequest) GetId() string {
@@ -4936,7 +5328,7 @@ type UpdateAdminAgentRequest struct {
func (x *UpdateAdminAgentRequest) Reset() {
*x = UpdateAdminAgentRequest{}
mi := &file_app_v1_admin_proto_msgTypes[82]
mi := &file_app_v1_admin_proto_msgTypes[90]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -4948,7 +5340,7 @@ func (x *UpdateAdminAgentRequest) String() string {
func (*UpdateAdminAgentRequest) ProtoMessage() {}
func (x *UpdateAdminAgentRequest) ProtoReflect() protoreflect.Message {
mi := &file_app_v1_admin_proto_msgTypes[82]
mi := &file_app_v1_admin_proto_msgTypes[90]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -4961,7 +5353,7 @@ func (x *UpdateAdminAgentRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use UpdateAdminAgentRequest.ProtoReflect.Descriptor instead.
func (*UpdateAdminAgentRequest) Descriptor() ([]byte, []int) {
return file_app_v1_admin_proto_rawDescGZIP(), []int{82}
return file_app_v1_admin_proto_rawDescGZIP(), []int{90}
}
func (x *UpdateAdminAgentRequest) GetId() string {
@@ -4980,7 +5372,7 @@ type AdminAgentCommandResponse struct {
func (x *AdminAgentCommandResponse) Reset() {
*x = AdminAgentCommandResponse{}
mi := &file_app_v1_admin_proto_msgTypes[83]
mi := &file_app_v1_admin_proto_msgTypes[91]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -4992,7 +5384,7 @@ func (x *AdminAgentCommandResponse) String() string {
func (*AdminAgentCommandResponse) ProtoMessage() {}
func (x *AdminAgentCommandResponse) ProtoReflect() protoreflect.Message {
mi := &file_app_v1_admin_proto_msgTypes[83]
mi := &file_app_v1_admin_proto_msgTypes[91]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -5005,7 +5397,7 @@ func (x *AdminAgentCommandResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use AdminAgentCommandResponse.ProtoReflect.Descriptor instead.
func (*AdminAgentCommandResponse) Descriptor() ([]byte, []int) {
return file_app_v1_admin_proto_rawDescGZIP(), []int{83}
return file_app_v1_admin_proto_rawDescGZIP(), []int{91}
}
func (x *AdminAgentCommandResponse) GetStatus() string {
@@ -5448,7 +5840,28 @@ const file_app_v1_admin_proto_rawDesc = "" +
"\x14RetryAdminJobRequest\x12\x0e\n" +
"\x02id\x18\x01 \x01(\tR\x02id\"B\n" +
"\x15RetryAdminJobResponse\x12)\n" +
"\x03job\x18\x01 \x01(\v2\x17.stream.app.v1.AdminJobR\x03job\"\x18\n" +
"\x03job\x18\x01 \x01(\v2\x17.stream.app.v1.AdminJobR\x03job\"G\n" +
"\x17ListAdminDlqJobsRequest\x12\x16\n" +
"\x06offset\x18\x01 \x01(\x05R\x06offset\x12\x14\n" +
"\x05limit\x18\x02 \x01(\x05R\x05limit\"\x92\x01\n" +
"\x18ListAdminDlqJobsResponse\x122\n" +
"\x05items\x18\x01 \x03(\v2\x1c.stream.app.v1.AdminDlqEntryR\x05items\x12\x14\n" +
"\x05total\x18\x02 \x01(\x03R\x05total\x12\x16\n" +
"\x06offset\x18\x03 \x01(\x05R\x06offset\x12\x14\n" +
"\x05limit\x18\x04 \x01(\x05R\x05limit\"'\n" +
"\x15GetAdminDlqJobRequest\x12\x0e\n" +
"\x02id\x18\x01 \x01(\tR\x02id\"J\n" +
"\x16GetAdminDlqJobResponse\x120\n" +
"\x04item\x18\x01 \x01(\v2\x1c.stream.app.v1.AdminDlqEntryR\x04item\")\n" +
"\x17RetryAdminDlqJobRequest\x12\x0e\n" +
"\x02id\x18\x01 \x01(\tR\x02id\"E\n" +
"\x18RetryAdminDlqJobResponse\x12)\n" +
"\x03job\x18\x01 \x01(\v2\x17.stream.app.v1.AdminJobR\x03job\"*\n" +
"\x18RemoveAdminDlqJobRequest\x12\x0e\n" +
"\x02id\x18\x01 \x01(\tR\x02id\"J\n" +
"\x19RemoveAdminDlqJobResponse\x12\x16\n" +
"\x06status\x18\x01 \x01(\tR\x06status\x12\x15\n" +
"\x06job_id\x18\x02 \x01(\tR\x05jobId\"\x18\n" +
"\x16ListAdminAgentsRequest\"L\n" +
"\x17ListAdminAgentsResponse\x121\n" +
"\x06agents\x18\x01 \x03(\v2\x19.stream.app.v1.AdminAgentR\x06agents\"*\n" +
@@ -5457,7 +5870,7 @@ const file_app_v1_admin_proto_rawDesc = "" +
"\x17UpdateAdminAgentRequest\x12\x0e\n" +
"\x02id\x18\x01 \x01(\tR\x02id\"3\n" +
"\x19AdminAgentCommandResponse\x12\x16\n" +
"\x06status\x18\x01 \x01(\tR\x06status2\x9d$\n" +
"\x06status\x18\x01 \x01(\tR\x06status2\xae'\n" +
"\x05Admin\x12f\n" +
"\x11GetAdminDashboard\x12'.stream.app.v1.GetAdminDashboardRequest\x1a(.stream.app.v1.GetAdminDashboardResponse\x12]\n" +
"\x0eListAdminUsers\x12$.stream.app.v1.ListAdminUsersRequest\x1a%.stream.app.v1.ListAdminUsersResponse\x12W\n" +
@@ -5500,7 +5913,11 @@ const file_app_v1_admin_proto_rawDesc = "" +
"\x0fGetAdminJobLogs\x12%.stream.app.v1.GetAdminJobLogsRequest\x1a&.stream.app.v1.GetAdminJobLogsResponse\x12]\n" +
"\x0eCreateAdminJob\x12$.stream.app.v1.CreateAdminJobRequest\x1a%.stream.app.v1.CreateAdminJobResponse\x12]\n" +
"\x0eCancelAdminJob\x12$.stream.app.v1.CancelAdminJobRequest\x1a%.stream.app.v1.CancelAdminJobResponse\x12Z\n" +
"\rRetryAdminJob\x12#.stream.app.v1.RetryAdminJobRequest\x1a$.stream.app.v1.RetryAdminJobResponse\x12`\n" +
"\rRetryAdminJob\x12#.stream.app.v1.RetryAdminJobRequest\x1a$.stream.app.v1.RetryAdminJobResponse\x12c\n" +
"\x10ListAdminDlqJobs\x12&.stream.app.v1.ListAdminDlqJobsRequest\x1a'.stream.app.v1.ListAdminDlqJobsResponse\x12]\n" +
"\x0eGetAdminDlqJob\x12$.stream.app.v1.GetAdminDlqJobRequest\x1a%.stream.app.v1.GetAdminDlqJobResponse\x12c\n" +
"\x10RetryAdminDlqJob\x12&.stream.app.v1.RetryAdminDlqJobRequest\x1a'.stream.app.v1.RetryAdminDlqJobResponse\x12f\n" +
"\x11RemoveAdminDlqJob\x12'.stream.app.v1.RemoveAdminDlqJobRequest\x1a(.stream.app.v1.RemoveAdminDlqJobResponse\x12`\n" +
"\x0fListAdminAgents\x12%.stream.app.v1.ListAdminAgentsRequest\x1a&.stream.app.v1.ListAdminAgentsResponse\x12f\n" +
"\x11RestartAdminAgent\x12'.stream.app.v1.RestartAdminAgentRequest\x1a(.stream.app.v1.AdminAgentCommandResponse\x12d\n" +
"\x10UpdateAdminAgent\x12&.stream.app.v1.UpdateAdminAgentRequest\x1a(.stream.app.v1.AdminAgentCommandResponseB,Z*stream.api/internal/gen/proto/app/v1;appv1b\x06proto3"
@@ -5517,7 +5934,7 @@ func file_app_v1_admin_proto_rawDescGZIP() []byte {
return file_app_v1_admin_proto_rawDescData
}
var file_app_v1_admin_proto_msgTypes = make([]protoimpl.MessageInfo, 85)
var file_app_v1_admin_proto_msgTypes = make([]protoimpl.MessageInfo, 93)
var file_app_v1_admin_proto_goTypes = []any{
(*GetAdminDashboardRequest)(nil), // 0: stream.app.v1.GetAdminDashboardRequest
(*GetAdminDashboardResponse)(nil), // 1: stream.app.v1.GetAdminDashboardResponse
@@ -5598,158 +6015,178 @@ var file_app_v1_admin_proto_goTypes = []any{
(*CancelAdminJobResponse)(nil), // 76: stream.app.v1.CancelAdminJobResponse
(*RetryAdminJobRequest)(nil), // 77: stream.app.v1.RetryAdminJobRequest
(*RetryAdminJobResponse)(nil), // 78: stream.app.v1.RetryAdminJobResponse
(*ListAdminAgentsRequest)(nil), // 79: stream.app.v1.ListAdminAgentsRequest
(*ListAdminAgentsResponse)(nil), // 80: stream.app.v1.ListAdminAgentsResponse
(*RestartAdminAgentRequest)(nil), // 81: stream.app.v1.RestartAdminAgentRequest
(*UpdateAdminAgentRequest)(nil), // 82: stream.app.v1.UpdateAdminAgentRequest
(*AdminAgentCommandResponse)(nil), // 83: stream.app.v1.AdminAgentCommandResponse
nil, // 84: stream.app.v1.CreateAdminJobRequest.EnvEntry
(*AdminDashboard)(nil), // 85: stream.app.v1.AdminDashboard
(*AdminUser)(nil), // 86: stream.app.v1.AdminUser
(*AdminUserDetail)(nil), // 87: stream.app.v1.AdminUserDetail
(*AdminVideo)(nil), // 88: stream.app.v1.AdminVideo
(*AdminPayment)(nil), // 89: stream.app.v1.AdminPayment
(*PlanSubscription)(nil), // 90: stream.app.v1.PlanSubscription
(*AdminPlan)(nil), // 91: stream.app.v1.AdminPlan
(*AdminAdTemplate)(nil), // 92: stream.app.v1.AdminAdTemplate
(*AdminPopupAd)(nil), // 93: stream.app.v1.AdminPopupAd
(*AdminPlayerConfig)(nil), // 94: stream.app.v1.AdminPlayerConfig
(*AdminJob)(nil), // 95: stream.app.v1.AdminJob
(*AdminAgent)(nil), // 96: stream.app.v1.AdminAgent
(*MessageResponse)(nil), // 97: stream.app.v1.MessageResponse
(*ListAdminDlqJobsRequest)(nil), // 79: stream.app.v1.ListAdminDlqJobsRequest
(*ListAdminDlqJobsResponse)(nil), // 80: stream.app.v1.ListAdminDlqJobsResponse
(*GetAdminDlqJobRequest)(nil), // 81: stream.app.v1.GetAdminDlqJobRequest
(*GetAdminDlqJobResponse)(nil), // 82: stream.app.v1.GetAdminDlqJobResponse
(*RetryAdminDlqJobRequest)(nil), // 83: stream.app.v1.RetryAdminDlqJobRequest
(*RetryAdminDlqJobResponse)(nil), // 84: stream.app.v1.RetryAdminDlqJobResponse
(*RemoveAdminDlqJobRequest)(nil), // 85: stream.app.v1.RemoveAdminDlqJobRequest
(*RemoveAdminDlqJobResponse)(nil), // 86: stream.app.v1.RemoveAdminDlqJobResponse
(*ListAdminAgentsRequest)(nil), // 87: stream.app.v1.ListAdminAgentsRequest
(*ListAdminAgentsResponse)(nil), // 88: stream.app.v1.ListAdminAgentsResponse
(*RestartAdminAgentRequest)(nil), // 89: stream.app.v1.RestartAdminAgentRequest
(*UpdateAdminAgentRequest)(nil), // 90: stream.app.v1.UpdateAdminAgentRequest
(*AdminAgentCommandResponse)(nil), // 91: stream.app.v1.AdminAgentCommandResponse
nil, // 92: stream.app.v1.CreateAdminJobRequest.EnvEntry
(*AdminDashboard)(nil), // 93: stream.app.v1.AdminDashboard
(*AdminUser)(nil), // 94: stream.app.v1.AdminUser
(*AdminUserDetail)(nil), // 95: stream.app.v1.AdminUserDetail
(*AdminVideo)(nil), // 96: stream.app.v1.AdminVideo
(*AdminPayment)(nil), // 97: stream.app.v1.AdminPayment
(*PlanSubscription)(nil), // 98: stream.app.v1.PlanSubscription
(*AdminPlan)(nil), // 99: stream.app.v1.AdminPlan
(*AdminAdTemplate)(nil), // 100: stream.app.v1.AdminAdTemplate
(*AdminPopupAd)(nil), // 101: stream.app.v1.AdminPopupAd
(*AdminPlayerConfig)(nil), // 102: stream.app.v1.AdminPlayerConfig
(*AdminJob)(nil), // 103: stream.app.v1.AdminJob
(*AdminDlqEntry)(nil), // 104: stream.app.v1.AdminDlqEntry
(*AdminAgent)(nil), // 105: stream.app.v1.AdminAgent
(*MessageResponse)(nil), // 106: stream.app.v1.MessageResponse
}
var file_app_v1_admin_proto_depIdxs = []int32{
85, // 0: stream.app.v1.GetAdminDashboardResponse.dashboard:type_name -> stream.app.v1.AdminDashboard
86, // 1: stream.app.v1.ListAdminUsersResponse.users:type_name -> stream.app.v1.AdminUser
87, // 2: stream.app.v1.GetAdminUserResponse.user:type_name -> stream.app.v1.AdminUserDetail
86, // 3: stream.app.v1.CreateAdminUserResponse.user:type_name -> stream.app.v1.AdminUser
86, // 4: stream.app.v1.UpdateAdminUserResponse.user:type_name -> stream.app.v1.AdminUser
87, // 5: stream.app.v1.UpdateAdminUserReferralSettingsResponse.user:type_name -> stream.app.v1.AdminUserDetail
88, // 6: stream.app.v1.ListAdminVideosResponse.videos:type_name -> stream.app.v1.AdminVideo
88, // 7: stream.app.v1.GetAdminVideoResponse.video:type_name -> stream.app.v1.AdminVideo
88, // 8: stream.app.v1.CreateAdminVideoResponse.video:type_name -> stream.app.v1.AdminVideo
88, // 9: stream.app.v1.UpdateAdminVideoResponse.video:type_name -> stream.app.v1.AdminVideo
89, // 10: stream.app.v1.ListAdminPaymentsResponse.payments:type_name -> stream.app.v1.AdminPayment
89, // 11: stream.app.v1.GetAdminPaymentResponse.payment:type_name -> stream.app.v1.AdminPayment
89, // 12: stream.app.v1.CreateAdminPaymentResponse.payment:type_name -> stream.app.v1.AdminPayment
90, // 13: stream.app.v1.CreateAdminPaymentResponse.subscription:type_name -> stream.app.v1.PlanSubscription
89, // 14: stream.app.v1.UpdateAdminPaymentResponse.payment:type_name -> stream.app.v1.AdminPayment
91, // 15: stream.app.v1.ListAdminPlansResponse.plans:type_name -> stream.app.v1.AdminPlan
91, // 16: stream.app.v1.CreateAdminPlanResponse.plan:type_name -> stream.app.v1.AdminPlan
91, // 17: stream.app.v1.UpdateAdminPlanResponse.plan:type_name -> stream.app.v1.AdminPlan
92, // 18: stream.app.v1.ListAdminAdTemplatesResponse.templates:type_name -> stream.app.v1.AdminAdTemplate
92, // 19: stream.app.v1.GetAdminAdTemplateResponse.template:type_name -> stream.app.v1.AdminAdTemplate
92, // 20: stream.app.v1.CreateAdminAdTemplateResponse.template:type_name -> stream.app.v1.AdminAdTemplate
92, // 21: stream.app.v1.UpdateAdminAdTemplateResponse.template:type_name -> stream.app.v1.AdminAdTemplate
93, // 22: stream.app.v1.ListAdminPopupAdsResponse.items:type_name -> stream.app.v1.AdminPopupAd
93, // 23: stream.app.v1.GetAdminPopupAdResponse.item:type_name -> stream.app.v1.AdminPopupAd
93, // 24: stream.app.v1.CreateAdminPopupAdResponse.item:type_name -> stream.app.v1.AdminPopupAd
93, // 25: stream.app.v1.UpdateAdminPopupAdResponse.item:type_name -> stream.app.v1.AdminPopupAd
94, // 26: stream.app.v1.ListAdminPlayerConfigsResponse.configs:type_name -> stream.app.v1.AdminPlayerConfig
94, // 27: stream.app.v1.GetAdminPlayerConfigResponse.config:type_name -> stream.app.v1.AdminPlayerConfig
94, // 28: stream.app.v1.CreateAdminPlayerConfigResponse.config:type_name -> stream.app.v1.AdminPlayerConfig
94, // 29: stream.app.v1.UpdateAdminPlayerConfigResponse.config:type_name -> stream.app.v1.AdminPlayerConfig
95, // 30: stream.app.v1.ListAdminJobsResponse.jobs:type_name -> stream.app.v1.AdminJob
95, // 31: stream.app.v1.GetAdminJobResponse.job:type_name -> stream.app.v1.AdminJob
84, // 32: stream.app.v1.CreateAdminJobRequest.env:type_name -> stream.app.v1.CreateAdminJobRequest.EnvEntry
95, // 33: stream.app.v1.CreateAdminJobResponse.job:type_name -> stream.app.v1.AdminJob
95, // 34: stream.app.v1.RetryAdminJobResponse.job:type_name -> stream.app.v1.AdminJob
96, // 35: stream.app.v1.ListAdminAgentsResponse.agents:type_name -> stream.app.v1.AdminAgent
0, // 36: stream.app.v1.Admin.GetAdminDashboard:input_type -> stream.app.v1.GetAdminDashboardRequest
2, // 37: stream.app.v1.Admin.ListAdminUsers:input_type -> stream.app.v1.ListAdminUsersRequest
4, // 38: stream.app.v1.Admin.GetAdminUser:input_type -> stream.app.v1.GetAdminUserRequest
6, // 39: stream.app.v1.Admin.CreateAdminUser:input_type -> stream.app.v1.CreateAdminUserRequest
8, // 40: stream.app.v1.Admin.UpdateAdminUser:input_type -> stream.app.v1.UpdateAdminUserRequest
10, // 41: stream.app.v1.Admin.UpdateAdminUserReferralSettings:input_type -> stream.app.v1.UpdateAdminUserReferralSettingsRequest
12, // 42: stream.app.v1.Admin.UpdateAdminUserRole:input_type -> stream.app.v1.UpdateAdminUserRoleRequest
14, // 43: stream.app.v1.Admin.DeleteAdminUser:input_type -> stream.app.v1.DeleteAdminUserRequest
15, // 44: stream.app.v1.Admin.ListAdminVideos:input_type -> stream.app.v1.ListAdminVideosRequest
17, // 45: stream.app.v1.Admin.GetAdminVideo:input_type -> stream.app.v1.GetAdminVideoRequest
19, // 46: stream.app.v1.Admin.CreateAdminVideo:input_type -> stream.app.v1.CreateAdminVideoRequest
21, // 47: stream.app.v1.Admin.UpdateAdminVideo:input_type -> stream.app.v1.UpdateAdminVideoRequest
23, // 48: stream.app.v1.Admin.DeleteAdminVideo:input_type -> stream.app.v1.DeleteAdminVideoRequest
24, // 49: stream.app.v1.Admin.ListAdminPayments:input_type -> stream.app.v1.ListAdminPaymentsRequest
26, // 50: stream.app.v1.Admin.GetAdminPayment:input_type -> stream.app.v1.GetAdminPaymentRequest
28, // 51: stream.app.v1.Admin.CreateAdminPayment:input_type -> stream.app.v1.CreateAdminPaymentRequest
30, // 52: stream.app.v1.Admin.UpdateAdminPayment:input_type -> stream.app.v1.UpdateAdminPaymentRequest
32, // 53: stream.app.v1.Admin.ListAdminPlans:input_type -> stream.app.v1.ListAdminPlansRequest
34, // 54: stream.app.v1.Admin.CreateAdminPlan:input_type -> stream.app.v1.CreateAdminPlanRequest
36, // 55: stream.app.v1.Admin.UpdateAdminPlan:input_type -> stream.app.v1.UpdateAdminPlanRequest
38, // 56: stream.app.v1.Admin.DeleteAdminPlan:input_type -> stream.app.v1.DeleteAdminPlanRequest
40, // 57: stream.app.v1.Admin.ListAdminAdTemplates:input_type -> stream.app.v1.ListAdminAdTemplatesRequest
42, // 58: stream.app.v1.Admin.GetAdminAdTemplate:input_type -> stream.app.v1.GetAdminAdTemplateRequest
44, // 59: stream.app.v1.Admin.CreateAdminAdTemplate:input_type -> stream.app.v1.CreateAdminAdTemplateRequest
46, // 60: stream.app.v1.Admin.UpdateAdminAdTemplate:input_type -> stream.app.v1.UpdateAdminAdTemplateRequest
48, // 61: stream.app.v1.Admin.DeleteAdminAdTemplate:input_type -> stream.app.v1.DeleteAdminAdTemplateRequest
49, // 62: stream.app.v1.Admin.ListAdminPopupAds:input_type -> stream.app.v1.ListAdminPopupAdsRequest
51, // 63: stream.app.v1.Admin.GetAdminPopupAd:input_type -> stream.app.v1.GetAdminPopupAdRequest
53, // 64: stream.app.v1.Admin.CreateAdminPopupAd:input_type -> stream.app.v1.CreateAdminPopupAdRequest
55, // 65: stream.app.v1.Admin.UpdateAdminPopupAd:input_type -> stream.app.v1.UpdateAdminPopupAdRequest
57, // 66: stream.app.v1.Admin.DeleteAdminPopupAd:input_type -> stream.app.v1.DeleteAdminPopupAdRequest
58, // 67: stream.app.v1.Admin.ListAdminPlayerConfigs:input_type -> stream.app.v1.ListAdminPlayerConfigsRequest
60, // 68: stream.app.v1.Admin.GetAdminPlayerConfig:input_type -> stream.app.v1.GetAdminPlayerConfigRequest
62, // 69: stream.app.v1.Admin.CreateAdminPlayerConfig:input_type -> stream.app.v1.CreateAdminPlayerConfigRequest
64, // 70: stream.app.v1.Admin.UpdateAdminPlayerConfig:input_type -> stream.app.v1.UpdateAdminPlayerConfigRequest
66, // 71: stream.app.v1.Admin.DeleteAdminPlayerConfig:input_type -> stream.app.v1.DeleteAdminPlayerConfigRequest
67, // 72: stream.app.v1.Admin.ListAdminJobs:input_type -> stream.app.v1.ListAdminJobsRequest
69, // 73: stream.app.v1.Admin.GetAdminJob:input_type -> stream.app.v1.GetAdminJobRequest
71, // 74: stream.app.v1.Admin.GetAdminJobLogs:input_type -> stream.app.v1.GetAdminJobLogsRequest
73, // 75: stream.app.v1.Admin.CreateAdminJob:input_type -> stream.app.v1.CreateAdminJobRequest
75, // 76: stream.app.v1.Admin.CancelAdminJob:input_type -> stream.app.v1.CancelAdminJobRequest
77, // 77: stream.app.v1.Admin.RetryAdminJob:input_type -> stream.app.v1.RetryAdminJobRequest
79, // 78: stream.app.v1.Admin.ListAdminAgents:input_type -> stream.app.v1.ListAdminAgentsRequest
81, // 79: stream.app.v1.Admin.RestartAdminAgent:input_type -> stream.app.v1.RestartAdminAgentRequest
82, // 80: stream.app.v1.Admin.UpdateAdminAgent:input_type -> stream.app.v1.UpdateAdminAgentRequest
1, // 81: stream.app.v1.Admin.GetAdminDashboard:output_type -> stream.app.v1.GetAdminDashboardResponse
3, // 82: stream.app.v1.Admin.ListAdminUsers:output_type -> stream.app.v1.ListAdminUsersResponse
5, // 83: stream.app.v1.Admin.GetAdminUser:output_type -> stream.app.v1.GetAdminUserResponse
7, // 84: stream.app.v1.Admin.CreateAdminUser:output_type -> stream.app.v1.CreateAdminUserResponse
9, // 85: stream.app.v1.Admin.UpdateAdminUser:output_type -> stream.app.v1.UpdateAdminUserResponse
11, // 86: stream.app.v1.Admin.UpdateAdminUserReferralSettings:output_type -> stream.app.v1.UpdateAdminUserReferralSettingsResponse
13, // 87: stream.app.v1.Admin.UpdateAdminUserRole:output_type -> stream.app.v1.UpdateAdminUserRoleResponse
97, // 88: stream.app.v1.Admin.DeleteAdminUser:output_type -> stream.app.v1.MessageResponse
16, // 89: stream.app.v1.Admin.ListAdminVideos:output_type -> stream.app.v1.ListAdminVideosResponse
18, // 90: stream.app.v1.Admin.GetAdminVideo:output_type -> stream.app.v1.GetAdminVideoResponse
20, // 91: stream.app.v1.Admin.CreateAdminVideo:output_type -> stream.app.v1.CreateAdminVideoResponse
22, // 92: stream.app.v1.Admin.UpdateAdminVideo:output_type -> stream.app.v1.UpdateAdminVideoResponse
97, // 93: stream.app.v1.Admin.DeleteAdminVideo:output_type -> stream.app.v1.MessageResponse
25, // 94: stream.app.v1.Admin.ListAdminPayments:output_type -> stream.app.v1.ListAdminPaymentsResponse
27, // 95: stream.app.v1.Admin.GetAdminPayment:output_type -> stream.app.v1.GetAdminPaymentResponse
29, // 96: stream.app.v1.Admin.CreateAdminPayment:output_type -> stream.app.v1.CreateAdminPaymentResponse
31, // 97: stream.app.v1.Admin.UpdateAdminPayment:output_type -> stream.app.v1.UpdateAdminPaymentResponse
33, // 98: stream.app.v1.Admin.ListAdminPlans:output_type -> stream.app.v1.ListAdminPlansResponse
35, // 99: stream.app.v1.Admin.CreateAdminPlan:output_type -> stream.app.v1.CreateAdminPlanResponse
37, // 100: stream.app.v1.Admin.UpdateAdminPlan:output_type -> stream.app.v1.UpdateAdminPlanResponse
39, // 101: stream.app.v1.Admin.DeleteAdminPlan:output_type -> stream.app.v1.DeleteAdminPlanResponse
41, // 102: stream.app.v1.Admin.ListAdminAdTemplates:output_type -> stream.app.v1.ListAdminAdTemplatesResponse
43, // 103: stream.app.v1.Admin.GetAdminAdTemplate:output_type -> stream.app.v1.GetAdminAdTemplateResponse
45, // 104: stream.app.v1.Admin.CreateAdminAdTemplate:output_type -> stream.app.v1.CreateAdminAdTemplateResponse
47, // 105: stream.app.v1.Admin.UpdateAdminAdTemplate:output_type -> stream.app.v1.UpdateAdminAdTemplateResponse
97, // 106: stream.app.v1.Admin.DeleteAdminAdTemplate:output_type -> stream.app.v1.MessageResponse
50, // 107: stream.app.v1.Admin.ListAdminPopupAds:output_type -> stream.app.v1.ListAdminPopupAdsResponse
52, // 108: stream.app.v1.Admin.GetAdminPopupAd:output_type -> stream.app.v1.GetAdminPopupAdResponse
54, // 109: stream.app.v1.Admin.CreateAdminPopupAd:output_type -> stream.app.v1.CreateAdminPopupAdResponse
56, // 110: stream.app.v1.Admin.UpdateAdminPopupAd:output_type -> stream.app.v1.UpdateAdminPopupAdResponse
97, // 111: stream.app.v1.Admin.DeleteAdminPopupAd:output_type -> stream.app.v1.MessageResponse
59, // 112: stream.app.v1.Admin.ListAdminPlayerConfigs:output_type -> stream.app.v1.ListAdminPlayerConfigsResponse
61, // 113: stream.app.v1.Admin.GetAdminPlayerConfig:output_type -> stream.app.v1.GetAdminPlayerConfigResponse
63, // 114: stream.app.v1.Admin.CreateAdminPlayerConfig:output_type -> stream.app.v1.CreateAdminPlayerConfigResponse
65, // 115: stream.app.v1.Admin.UpdateAdminPlayerConfig:output_type -> stream.app.v1.UpdateAdminPlayerConfigResponse
97, // 116: stream.app.v1.Admin.DeleteAdminPlayerConfig:output_type -> stream.app.v1.MessageResponse
68, // 117: stream.app.v1.Admin.ListAdminJobs:output_type -> stream.app.v1.ListAdminJobsResponse
70, // 118: stream.app.v1.Admin.GetAdminJob:output_type -> stream.app.v1.GetAdminJobResponse
72, // 119: stream.app.v1.Admin.GetAdminJobLogs:output_type -> stream.app.v1.GetAdminJobLogsResponse
74, // 120: stream.app.v1.Admin.CreateAdminJob:output_type -> stream.app.v1.CreateAdminJobResponse
76, // 121: stream.app.v1.Admin.CancelAdminJob:output_type -> stream.app.v1.CancelAdminJobResponse
78, // 122: stream.app.v1.Admin.RetryAdminJob:output_type -> stream.app.v1.RetryAdminJobResponse
80, // 123: stream.app.v1.Admin.ListAdminAgents:output_type -> stream.app.v1.ListAdminAgentsResponse
83, // 124: stream.app.v1.Admin.RestartAdminAgent:output_type -> stream.app.v1.AdminAgentCommandResponse
83, // 125: stream.app.v1.Admin.UpdateAdminAgent:output_type -> stream.app.v1.AdminAgentCommandResponse
81, // [81:126] is the sub-list for method output_type
36, // [36:81] is the sub-list for method input_type
36, // [36:36] is the sub-list for extension type_name
36, // [36:36] is the sub-list for extension extendee
0, // [0:36] is the sub-list for field type_name
93, // 0: stream.app.v1.GetAdminDashboardResponse.dashboard:type_name -> stream.app.v1.AdminDashboard
94, // 1: stream.app.v1.ListAdminUsersResponse.users:type_name -> stream.app.v1.AdminUser
95, // 2: stream.app.v1.GetAdminUserResponse.user:type_name -> stream.app.v1.AdminUserDetail
94, // 3: stream.app.v1.CreateAdminUserResponse.user:type_name -> stream.app.v1.AdminUser
94, // 4: stream.app.v1.UpdateAdminUserResponse.user:type_name -> stream.app.v1.AdminUser
95, // 5: stream.app.v1.UpdateAdminUserReferralSettingsResponse.user:type_name -> stream.app.v1.AdminUserDetail
96, // 6: stream.app.v1.ListAdminVideosResponse.videos:type_name -> stream.app.v1.AdminVideo
96, // 7: stream.app.v1.GetAdminVideoResponse.video:type_name -> stream.app.v1.AdminVideo
96, // 8: stream.app.v1.CreateAdminVideoResponse.video:type_name -> stream.app.v1.AdminVideo
96, // 9: stream.app.v1.UpdateAdminVideoResponse.video:type_name -> stream.app.v1.AdminVideo
97, // 10: stream.app.v1.ListAdminPaymentsResponse.payments:type_name -> stream.app.v1.AdminPayment
97, // 11: stream.app.v1.GetAdminPaymentResponse.payment:type_name -> stream.app.v1.AdminPayment
97, // 12: stream.app.v1.CreateAdminPaymentResponse.payment:type_name -> stream.app.v1.AdminPayment
98, // 13: stream.app.v1.CreateAdminPaymentResponse.subscription:type_name -> stream.app.v1.PlanSubscription
97, // 14: stream.app.v1.UpdateAdminPaymentResponse.payment:type_name -> stream.app.v1.AdminPayment
99, // 15: stream.app.v1.ListAdminPlansResponse.plans:type_name -> stream.app.v1.AdminPlan
99, // 16: stream.app.v1.CreateAdminPlanResponse.plan:type_name -> stream.app.v1.AdminPlan
99, // 17: stream.app.v1.UpdateAdminPlanResponse.plan:type_name -> stream.app.v1.AdminPlan
100, // 18: stream.app.v1.ListAdminAdTemplatesResponse.templates:type_name -> stream.app.v1.AdminAdTemplate
100, // 19: stream.app.v1.GetAdminAdTemplateResponse.template:type_name -> stream.app.v1.AdminAdTemplate
100, // 20: stream.app.v1.CreateAdminAdTemplateResponse.template:type_name -> stream.app.v1.AdminAdTemplate
100, // 21: stream.app.v1.UpdateAdminAdTemplateResponse.template:type_name -> stream.app.v1.AdminAdTemplate
101, // 22: stream.app.v1.ListAdminPopupAdsResponse.items:type_name -> stream.app.v1.AdminPopupAd
101, // 23: stream.app.v1.GetAdminPopupAdResponse.item:type_name -> stream.app.v1.AdminPopupAd
101, // 24: stream.app.v1.CreateAdminPopupAdResponse.item:type_name -> stream.app.v1.AdminPopupAd
101, // 25: stream.app.v1.UpdateAdminPopupAdResponse.item:type_name -> stream.app.v1.AdminPopupAd
102, // 26: stream.app.v1.ListAdminPlayerConfigsResponse.configs:type_name -> stream.app.v1.AdminPlayerConfig
102, // 27: stream.app.v1.GetAdminPlayerConfigResponse.config:type_name -> stream.app.v1.AdminPlayerConfig
102, // 28: stream.app.v1.CreateAdminPlayerConfigResponse.config:type_name -> stream.app.v1.AdminPlayerConfig
102, // 29: stream.app.v1.UpdateAdminPlayerConfigResponse.config:type_name -> stream.app.v1.AdminPlayerConfig
103, // 30: stream.app.v1.ListAdminJobsResponse.jobs:type_name -> stream.app.v1.AdminJob
103, // 31: stream.app.v1.GetAdminJobResponse.job:type_name -> stream.app.v1.AdminJob
92, // 32: stream.app.v1.CreateAdminJobRequest.env:type_name -> stream.app.v1.CreateAdminJobRequest.EnvEntry
103, // 33: stream.app.v1.CreateAdminJobResponse.job:type_name -> stream.app.v1.AdminJob
103, // 34: stream.app.v1.RetryAdminJobResponse.job:type_name -> stream.app.v1.AdminJob
104, // 35: stream.app.v1.ListAdminDlqJobsResponse.items:type_name -> stream.app.v1.AdminDlqEntry
104, // 36: stream.app.v1.GetAdminDlqJobResponse.item:type_name -> stream.app.v1.AdminDlqEntry
103, // 37: stream.app.v1.RetryAdminDlqJobResponse.job:type_name -> stream.app.v1.AdminJob
105, // 38: stream.app.v1.ListAdminAgentsResponse.agents:type_name -> stream.app.v1.AdminAgent
0, // 39: stream.app.v1.Admin.GetAdminDashboard:input_type -> stream.app.v1.GetAdminDashboardRequest
2, // 40: stream.app.v1.Admin.ListAdminUsers:input_type -> stream.app.v1.ListAdminUsersRequest
4, // 41: stream.app.v1.Admin.GetAdminUser:input_type -> stream.app.v1.GetAdminUserRequest
6, // 42: stream.app.v1.Admin.CreateAdminUser:input_type -> stream.app.v1.CreateAdminUserRequest
8, // 43: stream.app.v1.Admin.UpdateAdminUser:input_type -> stream.app.v1.UpdateAdminUserRequest
10, // 44: stream.app.v1.Admin.UpdateAdminUserReferralSettings:input_type -> stream.app.v1.UpdateAdminUserReferralSettingsRequest
12, // 45: stream.app.v1.Admin.UpdateAdminUserRole:input_type -> stream.app.v1.UpdateAdminUserRoleRequest
14, // 46: stream.app.v1.Admin.DeleteAdminUser:input_type -> stream.app.v1.DeleteAdminUserRequest
15, // 47: stream.app.v1.Admin.ListAdminVideos:input_type -> stream.app.v1.ListAdminVideosRequest
17, // 48: stream.app.v1.Admin.GetAdminVideo:input_type -> stream.app.v1.GetAdminVideoRequest
19, // 49: stream.app.v1.Admin.CreateAdminVideo:input_type -> stream.app.v1.CreateAdminVideoRequest
21, // 50: stream.app.v1.Admin.UpdateAdminVideo:input_type -> stream.app.v1.UpdateAdminVideoRequest
23, // 51: stream.app.v1.Admin.DeleteAdminVideo:input_type -> stream.app.v1.DeleteAdminVideoRequest
24, // 52: stream.app.v1.Admin.ListAdminPayments:input_type -> stream.app.v1.ListAdminPaymentsRequest
26, // 53: stream.app.v1.Admin.GetAdminPayment:input_type -> stream.app.v1.GetAdminPaymentRequest
28, // 54: stream.app.v1.Admin.CreateAdminPayment:input_type -> stream.app.v1.CreateAdminPaymentRequest
30, // 55: stream.app.v1.Admin.UpdateAdminPayment:input_type -> stream.app.v1.UpdateAdminPaymentRequest
32, // 56: stream.app.v1.Admin.ListAdminPlans:input_type -> stream.app.v1.ListAdminPlansRequest
34, // 57: stream.app.v1.Admin.CreateAdminPlan:input_type -> stream.app.v1.CreateAdminPlanRequest
36, // 58: stream.app.v1.Admin.UpdateAdminPlan:input_type -> stream.app.v1.UpdateAdminPlanRequest
38, // 59: stream.app.v1.Admin.DeleteAdminPlan:input_type -> stream.app.v1.DeleteAdminPlanRequest
40, // 60: stream.app.v1.Admin.ListAdminAdTemplates:input_type -> stream.app.v1.ListAdminAdTemplatesRequest
42, // 61: stream.app.v1.Admin.GetAdminAdTemplate:input_type -> stream.app.v1.GetAdminAdTemplateRequest
44, // 62: stream.app.v1.Admin.CreateAdminAdTemplate:input_type -> stream.app.v1.CreateAdminAdTemplateRequest
46, // 63: stream.app.v1.Admin.UpdateAdminAdTemplate:input_type -> stream.app.v1.UpdateAdminAdTemplateRequest
48, // 64: stream.app.v1.Admin.DeleteAdminAdTemplate:input_type -> stream.app.v1.DeleteAdminAdTemplateRequest
49, // 65: stream.app.v1.Admin.ListAdminPopupAds:input_type -> stream.app.v1.ListAdminPopupAdsRequest
51, // 66: stream.app.v1.Admin.GetAdminPopupAd:input_type -> stream.app.v1.GetAdminPopupAdRequest
53, // 67: stream.app.v1.Admin.CreateAdminPopupAd:input_type -> stream.app.v1.CreateAdminPopupAdRequest
55, // 68: stream.app.v1.Admin.UpdateAdminPopupAd:input_type -> stream.app.v1.UpdateAdminPopupAdRequest
57, // 69: stream.app.v1.Admin.DeleteAdminPopupAd:input_type -> stream.app.v1.DeleteAdminPopupAdRequest
58, // 70: stream.app.v1.Admin.ListAdminPlayerConfigs:input_type -> stream.app.v1.ListAdminPlayerConfigsRequest
60, // 71: stream.app.v1.Admin.GetAdminPlayerConfig:input_type -> stream.app.v1.GetAdminPlayerConfigRequest
62, // 72: stream.app.v1.Admin.CreateAdminPlayerConfig:input_type -> stream.app.v1.CreateAdminPlayerConfigRequest
64, // 73: stream.app.v1.Admin.UpdateAdminPlayerConfig:input_type -> stream.app.v1.UpdateAdminPlayerConfigRequest
66, // 74: stream.app.v1.Admin.DeleteAdminPlayerConfig:input_type -> stream.app.v1.DeleteAdminPlayerConfigRequest
67, // 75: stream.app.v1.Admin.ListAdminJobs:input_type -> stream.app.v1.ListAdminJobsRequest
69, // 76: stream.app.v1.Admin.GetAdminJob:input_type -> stream.app.v1.GetAdminJobRequest
71, // 77: stream.app.v1.Admin.GetAdminJobLogs:input_type -> stream.app.v1.GetAdminJobLogsRequest
73, // 78: stream.app.v1.Admin.CreateAdminJob:input_type -> stream.app.v1.CreateAdminJobRequest
75, // 79: stream.app.v1.Admin.CancelAdminJob:input_type -> stream.app.v1.CancelAdminJobRequest
77, // 80: stream.app.v1.Admin.RetryAdminJob:input_type -> stream.app.v1.RetryAdminJobRequest
79, // 81: stream.app.v1.Admin.ListAdminDlqJobs:input_type -> stream.app.v1.ListAdminDlqJobsRequest
81, // 82: stream.app.v1.Admin.GetAdminDlqJob:input_type -> stream.app.v1.GetAdminDlqJobRequest
83, // 83: stream.app.v1.Admin.RetryAdminDlqJob:input_type -> stream.app.v1.RetryAdminDlqJobRequest
85, // 84: stream.app.v1.Admin.RemoveAdminDlqJob:input_type -> stream.app.v1.RemoveAdminDlqJobRequest
87, // 85: stream.app.v1.Admin.ListAdminAgents:input_type -> stream.app.v1.ListAdminAgentsRequest
89, // 86: stream.app.v1.Admin.RestartAdminAgent:input_type -> stream.app.v1.RestartAdminAgentRequest
90, // 87: stream.app.v1.Admin.UpdateAdminAgent:input_type -> stream.app.v1.UpdateAdminAgentRequest
1, // 88: stream.app.v1.Admin.GetAdminDashboard:output_type -> stream.app.v1.GetAdminDashboardResponse
3, // 89: stream.app.v1.Admin.ListAdminUsers:output_type -> stream.app.v1.ListAdminUsersResponse
5, // 90: stream.app.v1.Admin.GetAdminUser:output_type -> stream.app.v1.GetAdminUserResponse
7, // 91: stream.app.v1.Admin.CreateAdminUser:output_type -> stream.app.v1.CreateAdminUserResponse
9, // 92: stream.app.v1.Admin.UpdateAdminUser:output_type -> stream.app.v1.UpdateAdminUserResponse
11, // 93: stream.app.v1.Admin.UpdateAdminUserReferralSettings:output_type -> stream.app.v1.UpdateAdminUserReferralSettingsResponse
13, // 94: stream.app.v1.Admin.UpdateAdminUserRole:output_type -> stream.app.v1.UpdateAdminUserRoleResponse
106, // 95: stream.app.v1.Admin.DeleteAdminUser:output_type -> stream.app.v1.MessageResponse
16, // 96: stream.app.v1.Admin.ListAdminVideos:output_type -> stream.app.v1.ListAdminVideosResponse
18, // 97: stream.app.v1.Admin.GetAdminVideo:output_type -> stream.app.v1.GetAdminVideoResponse
20, // 98: stream.app.v1.Admin.CreateAdminVideo:output_type -> stream.app.v1.CreateAdminVideoResponse
22, // 99: stream.app.v1.Admin.UpdateAdminVideo:output_type -> stream.app.v1.UpdateAdminVideoResponse
106, // 100: stream.app.v1.Admin.DeleteAdminVideo:output_type -> stream.app.v1.MessageResponse
25, // 101: stream.app.v1.Admin.ListAdminPayments:output_type -> stream.app.v1.ListAdminPaymentsResponse
27, // 102: stream.app.v1.Admin.GetAdminPayment:output_type -> stream.app.v1.GetAdminPaymentResponse
29, // 103: stream.app.v1.Admin.CreateAdminPayment:output_type -> stream.app.v1.CreateAdminPaymentResponse
31, // 104: stream.app.v1.Admin.UpdateAdminPayment:output_type -> stream.app.v1.UpdateAdminPaymentResponse
33, // 105: stream.app.v1.Admin.ListAdminPlans:output_type -> stream.app.v1.ListAdminPlansResponse
35, // 106: stream.app.v1.Admin.CreateAdminPlan:output_type -> stream.app.v1.CreateAdminPlanResponse
37, // 107: stream.app.v1.Admin.UpdateAdminPlan:output_type -> stream.app.v1.UpdateAdminPlanResponse
39, // 108: stream.app.v1.Admin.DeleteAdminPlan:output_type -> stream.app.v1.DeleteAdminPlanResponse
41, // 109: stream.app.v1.Admin.ListAdminAdTemplates:output_type -> stream.app.v1.ListAdminAdTemplatesResponse
43, // 110: stream.app.v1.Admin.GetAdminAdTemplate:output_type -> stream.app.v1.GetAdminAdTemplateResponse
45, // 111: stream.app.v1.Admin.CreateAdminAdTemplate:output_type -> stream.app.v1.CreateAdminAdTemplateResponse
47, // 112: stream.app.v1.Admin.UpdateAdminAdTemplate:output_type -> stream.app.v1.UpdateAdminAdTemplateResponse
106, // 113: stream.app.v1.Admin.DeleteAdminAdTemplate:output_type -> stream.app.v1.MessageResponse
50, // 114: stream.app.v1.Admin.ListAdminPopupAds:output_type -> stream.app.v1.ListAdminPopupAdsResponse
52, // 115: stream.app.v1.Admin.GetAdminPopupAd:output_type -> stream.app.v1.GetAdminPopupAdResponse
54, // 116: stream.app.v1.Admin.CreateAdminPopupAd:output_type -> stream.app.v1.CreateAdminPopupAdResponse
56, // 117: stream.app.v1.Admin.UpdateAdminPopupAd:output_type -> stream.app.v1.UpdateAdminPopupAdResponse
106, // 118: stream.app.v1.Admin.DeleteAdminPopupAd:output_type -> stream.app.v1.MessageResponse
59, // 119: stream.app.v1.Admin.ListAdminPlayerConfigs:output_type -> stream.app.v1.ListAdminPlayerConfigsResponse
61, // 120: stream.app.v1.Admin.GetAdminPlayerConfig:output_type -> stream.app.v1.GetAdminPlayerConfigResponse
63, // 121: stream.app.v1.Admin.CreateAdminPlayerConfig:output_type -> stream.app.v1.CreateAdminPlayerConfigResponse
65, // 122: stream.app.v1.Admin.UpdateAdminPlayerConfig:output_type -> stream.app.v1.UpdateAdminPlayerConfigResponse
106, // 123: stream.app.v1.Admin.DeleteAdminPlayerConfig:output_type -> stream.app.v1.MessageResponse
68, // 124: stream.app.v1.Admin.ListAdminJobs:output_type -> stream.app.v1.ListAdminJobsResponse
70, // 125: stream.app.v1.Admin.GetAdminJob:output_type -> stream.app.v1.GetAdminJobResponse
72, // 126: stream.app.v1.Admin.GetAdminJobLogs:output_type -> stream.app.v1.GetAdminJobLogsResponse
74, // 127: stream.app.v1.Admin.CreateAdminJob:output_type -> stream.app.v1.CreateAdminJobResponse
76, // 128: stream.app.v1.Admin.CancelAdminJob:output_type -> stream.app.v1.CancelAdminJobResponse
78, // 129: stream.app.v1.Admin.RetryAdminJob:output_type -> stream.app.v1.RetryAdminJobResponse
80, // 130: stream.app.v1.Admin.ListAdminDlqJobs:output_type -> stream.app.v1.ListAdminDlqJobsResponse
82, // 131: stream.app.v1.Admin.GetAdminDlqJob:output_type -> stream.app.v1.GetAdminDlqJobResponse
84, // 132: stream.app.v1.Admin.RetryAdminDlqJob:output_type -> stream.app.v1.RetryAdminDlqJobResponse
86, // 133: stream.app.v1.Admin.RemoveAdminDlqJob:output_type -> stream.app.v1.RemoveAdminDlqJobResponse
88, // 134: stream.app.v1.Admin.ListAdminAgents:output_type -> stream.app.v1.ListAdminAgentsResponse
91, // 135: stream.app.v1.Admin.RestartAdminAgent:output_type -> stream.app.v1.AdminAgentCommandResponse
91, // 136: stream.app.v1.Admin.UpdateAdminAgent:output_type -> stream.app.v1.AdminAgentCommandResponse
88, // [88:137] is the sub-list for method output_type
39, // [39:88] is the sub-list for method input_type
39, // [39:39] is the sub-list for extension type_name
39, // [39:39] is the sub-list for extension extendee
0, // [0:39] is the sub-list for field type_name
}
func init() { file_app_v1_admin_proto_init() }
@@ -5787,7 +6224,7 @@ func file_app_v1_admin_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_app_v1_admin_proto_rawDesc), len(file_app_v1_admin_proto_rawDesc)),
NumEnums: 0,
NumMessages: 85,
NumMessages: 93,
NumExtensions: 0,
NumServices: 1,
},

View File

@@ -61,6 +61,10 @@ const (
Admin_CreateAdminJob_FullMethodName = "/stream.app.v1.Admin/CreateAdminJob"
Admin_CancelAdminJob_FullMethodName = "/stream.app.v1.Admin/CancelAdminJob"
Admin_RetryAdminJob_FullMethodName = "/stream.app.v1.Admin/RetryAdminJob"
Admin_ListAdminDlqJobs_FullMethodName = "/stream.app.v1.Admin/ListAdminDlqJobs"
Admin_GetAdminDlqJob_FullMethodName = "/stream.app.v1.Admin/GetAdminDlqJob"
Admin_RetryAdminDlqJob_FullMethodName = "/stream.app.v1.Admin/RetryAdminDlqJob"
Admin_RemoveAdminDlqJob_FullMethodName = "/stream.app.v1.Admin/RemoveAdminDlqJob"
Admin_ListAdminAgents_FullMethodName = "/stream.app.v1.Admin/ListAdminAgents"
Admin_RestartAdminAgent_FullMethodName = "/stream.app.v1.Admin/RestartAdminAgent"
Admin_UpdateAdminAgent_FullMethodName = "/stream.app.v1.Admin/UpdateAdminAgent"
@@ -112,6 +116,10 @@ type AdminClient interface {
CreateAdminJob(ctx context.Context, in *CreateAdminJobRequest, opts ...grpc.CallOption) (*CreateAdminJobResponse, error)
CancelAdminJob(ctx context.Context, in *CancelAdminJobRequest, opts ...grpc.CallOption) (*CancelAdminJobResponse, error)
RetryAdminJob(ctx context.Context, in *RetryAdminJobRequest, opts ...grpc.CallOption) (*RetryAdminJobResponse, error)
ListAdminDlqJobs(ctx context.Context, in *ListAdminDlqJobsRequest, opts ...grpc.CallOption) (*ListAdminDlqJobsResponse, error)
GetAdminDlqJob(ctx context.Context, in *GetAdminDlqJobRequest, opts ...grpc.CallOption) (*GetAdminDlqJobResponse, error)
RetryAdminDlqJob(ctx context.Context, in *RetryAdminDlqJobRequest, opts ...grpc.CallOption) (*RetryAdminDlqJobResponse, error)
RemoveAdminDlqJob(ctx context.Context, in *RemoveAdminDlqJobRequest, opts ...grpc.CallOption) (*RemoveAdminDlqJobResponse, error)
ListAdminAgents(ctx context.Context, in *ListAdminAgentsRequest, opts ...grpc.CallOption) (*ListAdminAgentsResponse, error)
RestartAdminAgent(ctx context.Context, in *RestartAdminAgentRequest, opts ...grpc.CallOption) (*AdminAgentCommandResponse, error)
UpdateAdminAgent(ctx context.Context, in *UpdateAdminAgentRequest, opts ...grpc.CallOption) (*AdminAgentCommandResponse, error)
@@ -545,6 +553,46 @@ func (c *adminClient) RetryAdminJob(ctx context.Context, in *RetryAdminJobReques
return out, nil
}
func (c *adminClient) ListAdminDlqJobs(ctx context.Context, in *ListAdminDlqJobsRequest, opts ...grpc.CallOption) (*ListAdminDlqJobsResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ListAdminDlqJobsResponse)
err := c.cc.Invoke(ctx, Admin_ListAdminDlqJobs_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *adminClient) GetAdminDlqJob(ctx context.Context, in *GetAdminDlqJobRequest, opts ...grpc.CallOption) (*GetAdminDlqJobResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(GetAdminDlqJobResponse)
err := c.cc.Invoke(ctx, Admin_GetAdminDlqJob_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *adminClient) RetryAdminDlqJob(ctx context.Context, in *RetryAdminDlqJobRequest, opts ...grpc.CallOption) (*RetryAdminDlqJobResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(RetryAdminDlqJobResponse)
err := c.cc.Invoke(ctx, Admin_RetryAdminDlqJob_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *adminClient) RemoveAdminDlqJob(ctx context.Context, in *RemoveAdminDlqJobRequest, opts ...grpc.CallOption) (*RemoveAdminDlqJobResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(RemoveAdminDlqJobResponse)
err := c.cc.Invoke(ctx, Admin_RemoveAdminDlqJob_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *adminClient) ListAdminAgents(ctx context.Context, in *ListAdminAgentsRequest, opts ...grpc.CallOption) (*ListAdminAgentsResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ListAdminAgentsResponse)
@@ -621,6 +669,10 @@ type AdminServer interface {
CreateAdminJob(context.Context, *CreateAdminJobRequest) (*CreateAdminJobResponse, error)
CancelAdminJob(context.Context, *CancelAdminJobRequest) (*CancelAdminJobResponse, error)
RetryAdminJob(context.Context, *RetryAdminJobRequest) (*RetryAdminJobResponse, error)
ListAdminDlqJobs(context.Context, *ListAdminDlqJobsRequest) (*ListAdminDlqJobsResponse, error)
GetAdminDlqJob(context.Context, *GetAdminDlqJobRequest) (*GetAdminDlqJobResponse, error)
RetryAdminDlqJob(context.Context, *RetryAdminDlqJobRequest) (*RetryAdminDlqJobResponse, error)
RemoveAdminDlqJob(context.Context, *RemoveAdminDlqJobRequest) (*RemoveAdminDlqJobResponse, error)
ListAdminAgents(context.Context, *ListAdminAgentsRequest) (*ListAdminAgentsResponse, error)
RestartAdminAgent(context.Context, *RestartAdminAgentRequest) (*AdminAgentCommandResponse, error)
UpdateAdminAgent(context.Context, *UpdateAdminAgentRequest) (*AdminAgentCommandResponse, error)
@@ -760,6 +812,18 @@ func (UnimplementedAdminServer) CancelAdminJob(context.Context, *CancelAdminJobR
func (UnimplementedAdminServer) RetryAdminJob(context.Context, *RetryAdminJobRequest) (*RetryAdminJobResponse, error) {
return nil, status.Error(codes.Unimplemented, "method RetryAdminJob not implemented")
}
func (UnimplementedAdminServer) ListAdminDlqJobs(context.Context, *ListAdminDlqJobsRequest) (*ListAdminDlqJobsResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListAdminDlqJobs not implemented")
}
func (UnimplementedAdminServer) GetAdminDlqJob(context.Context, *GetAdminDlqJobRequest) (*GetAdminDlqJobResponse, error) {
return nil, status.Error(codes.Unimplemented, "method GetAdminDlqJob not implemented")
}
func (UnimplementedAdminServer) RetryAdminDlqJob(context.Context, *RetryAdminDlqJobRequest) (*RetryAdminDlqJobResponse, error) {
return nil, status.Error(codes.Unimplemented, "method RetryAdminDlqJob not implemented")
}
func (UnimplementedAdminServer) RemoveAdminDlqJob(context.Context, *RemoveAdminDlqJobRequest) (*RemoveAdminDlqJobResponse, error) {
return nil, status.Error(codes.Unimplemented, "method RemoveAdminDlqJob not implemented")
}
func (UnimplementedAdminServer) ListAdminAgents(context.Context, *ListAdminAgentsRequest) (*ListAdminAgentsResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListAdminAgents not implemented")
}
@@ -1546,6 +1610,78 @@ func _Admin_RetryAdminJob_Handler(srv interface{}, ctx context.Context, dec func
return interceptor(ctx, in, info, handler)
}
func _Admin_ListAdminDlqJobs_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListAdminDlqJobsRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(AdminServer).ListAdminDlqJobs(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Admin_ListAdminDlqJobs_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(AdminServer).ListAdminDlqJobs(ctx, req.(*ListAdminDlqJobsRequest))
}
return interceptor(ctx, in, info, handler)
}
func _Admin_GetAdminDlqJob_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetAdminDlqJobRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(AdminServer).GetAdminDlqJob(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Admin_GetAdminDlqJob_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(AdminServer).GetAdminDlqJob(ctx, req.(*GetAdminDlqJobRequest))
}
return interceptor(ctx, in, info, handler)
}
func _Admin_RetryAdminDlqJob_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(RetryAdminDlqJobRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(AdminServer).RetryAdminDlqJob(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Admin_RetryAdminDlqJob_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(AdminServer).RetryAdminDlqJob(ctx, req.(*RetryAdminDlqJobRequest))
}
return interceptor(ctx, in, info, handler)
}
func _Admin_RemoveAdminDlqJob_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(RemoveAdminDlqJobRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(AdminServer).RemoveAdminDlqJob(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Admin_RemoveAdminDlqJob_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(AdminServer).RemoveAdminDlqJob(ctx, req.(*RemoveAdminDlqJobRequest))
}
return interceptor(ctx, in, info, handler)
}
func _Admin_ListAdminAgents_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListAdminAgentsRequest)
if err := dec(in); err != nil {
@@ -1775,6 +1911,22 @@ var Admin_ServiceDesc = grpc.ServiceDesc{
MethodName: "RetryAdminJob",
Handler: _Admin_RetryAdminJob_Handler,
},
{
MethodName: "ListAdminDlqJobs",
Handler: _Admin_ListAdminDlqJobs_Handler,
},
{
MethodName: "GetAdminDlqJob",
Handler: _Admin_GetAdminDlqJob_Handler,
},
{
MethodName: "RetryAdminDlqJob",
Handler: _Admin_RetryAdminDlqJob_Handler,
},
{
MethodName: "RemoveAdminDlqJob",
Handler: _Admin_RemoveAdminDlqJob_Handler,
},
{
MethodName: "ListAdminAgents",
Handler: _Admin_ListAdminAgents_Handler,

View File

@@ -3442,6 +3442,74 @@ func (x *AdminAgent) GetUpdatedAt() *timestamppb.Timestamp {
return nil
}
type AdminDlqEntry struct {
state protoimpl.MessageState `protogen:"open.v1"`
Job *AdminJob `protobuf:"bytes,1,opt,name=job,proto3" json:"job,omitempty"`
FailureTime *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=failure_time,json=failureTime,proto3" json:"failure_time,omitempty"`
Reason string `protobuf:"bytes,3,opt,name=reason,proto3" json:"reason,omitempty"`
RetryCount int32 `protobuf:"varint,4,opt,name=retry_count,json=retryCount,proto3" json:"retry_count,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *AdminDlqEntry) Reset() {
*x = AdminDlqEntry{}
mi := &file_app_v1_common_proto_msgTypes[27]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *AdminDlqEntry) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*AdminDlqEntry) ProtoMessage() {}
func (x *AdminDlqEntry) ProtoReflect() protoreflect.Message {
mi := &file_app_v1_common_proto_msgTypes[27]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use AdminDlqEntry.ProtoReflect.Descriptor instead.
func (*AdminDlqEntry) Descriptor() ([]byte, []int) {
return file_app_v1_common_proto_rawDescGZIP(), []int{27}
}
func (x *AdminDlqEntry) GetJob() *AdminJob {
if x != nil {
return x.Job
}
return nil
}
func (x *AdminDlqEntry) GetFailureTime() *timestamppb.Timestamp {
if x != nil {
return x.FailureTime
}
return nil
}
func (x *AdminDlqEntry) GetReason() string {
if x != nil {
return x.Reason
}
return ""
}
func (x *AdminDlqEntry) GetRetryCount() int32 {
if x != nil {
return x.RetryCount
}
return 0
}
var File_app_v1_common_proto protoreflect.FileDescriptor
const file_app_v1_common_proto_rawDesc = "" +
@@ -3954,7 +4022,13 @@ const file_app_v1_common_proto_rawDesc = "" +
"\n" +
"created_at\x18\v \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\x129\n" +
"\n" +
"updated_at\x18\f \x01(\v2\x1a.google.protobuf.TimestampR\tupdatedAtB,Z*stream.api/internal/gen/proto/app/v1;appv1b\x06proto3"
"updated_at\x18\f \x01(\v2\x1a.google.protobuf.TimestampR\tupdatedAt\"\xb2\x01\n" +
"\rAdminDlqEntry\x12)\n" +
"\x03job\x18\x01 \x01(\v2\x17.stream.app.v1.AdminJobR\x03job\x12=\n" +
"\ffailure_time\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\vfailureTime\x12\x16\n" +
"\x06reason\x18\x03 \x01(\tR\x06reason\x12\x1f\n" +
"\vretry_count\x18\x04 \x01(\x05R\n" +
"retryCountB,Z*stream.api/internal/gen/proto/app/v1;appv1b\x06proto3"
var (
file_app_v1_common_proto_rawDescOnce sync.Once
@@ -3968,7 +4042,7 @@ func file_app_v1_common_proto_rawDescGZIP() []byte {
return file_app_v1_common_proto_rawDescData
}
var file_app_v1_common_proto_msgTypes = make([]protoimpl.MessageInfo, 27)
var file_app_v1_common_proto_msgTypes = make([]protoimpl.MessageInfo, 28)
var file_app_v1_common_proto_goTypes = []any{
(*MessageResponse)(nil), // 0: stream.app.v1.MessageResponse
(*User)(nil), // 1: stream.app.v1.User
@@ -3997,61 +4071,64 @@ var file_app_v1_common_proto_goTypes = []any{
(*AdminPopupAd)(nil), // 24: stream.app.v1.AdminPopupAd
(*AdminJob)(nil), // 25: stream.app.v1.AdminJob
(*AdminAgent)(nil), // 26: stream.app.v1.AdminAgent
(*timestamppb.Timestamp)(nil), // 27: google.protobuf.Timestamp
(*AdminDlqEntry)(nil), // 27: stream.app.v1.AdminDlqEntry
(*timestamppb.Timestamp)(nil), // 28: google.protobuf.Timestamp
}
var file_app_v1_common_proto_depIdxs = []int32{
27, // 0: stream.app.v1.User.plan_started_at:type_name -> google.protobuf.Timestamp
27, // 1: stream.app.v1.User.plan_expires_at:type_name -> google.protobuf.Timestamp
27, // 2: stream.app.v1.User.created_at:type_name -> google.protobuf.Timestamp
27, // 3: stream.app.v1.User.updated_at:type_name -> google.protobuf.Timestamp
27, // 4: stream.app.v1.Notification.created_at:type_name -> google.protobuf.Timestamp
27, // 5: stream.app.v1.Domain.created_at:type_name -> google.protobuf.Timestamp
27, // 6: stream.app.v1.Domain.updated_at:type_name -> google.protobuf.Timestamp
27, // 7: stream.app.v1.AdTemplate.created_at:type_name -> google.protobuf.Timestamp
27, // 8: stream.app.v1.AdTemplate.updated_at:type_name -> google.protobuf.Timestamp
27, // 9: stream.app.v1.PopupAd.created_at:type_name -> google.protobuf.Timestamp
27, // 10: stream.app.v1.PopupAd.updated_at:type_name -> google.protobuf.Timestamp
27, // 11: stream.app.v1.PlayerConfig.created_at:type_name -> google.protobuf.Timestamp
27, // 12: stream.app.v1.PlayerConfig.updated_at:type_name -> google.protobuf.Timestamp
27, // 13: stream.app.v1.AdminPlayerConfig.created_at:type_name -> google.protobuf.Timestamp
27, // 14: stream.app.v1.AdminPlayerConfig.updated_at:type_name -> google.protobuf.Timestamp
27, // 15: stream.app.v1.Payment.created_at:type_name -> google.protobuf.Timestamp
27, // 16: stream.app.v1.Payment.updated_at:type_name -> google.protobuf.Timestamp
27, // 17: stream.app.v1.PlanSubscription.started_at:type_name -> google.protobuf.Timestamp
27, // 18: stream.app.v1.PlanSubscription.expires_at:type_name -> google.protobuf.Timestamp
27, // 19: stream.app.v1.PlanSubscription.created_at:type_name -> google.protobuf.Timestamp
27, // 20: stream.app.v1.PlanSubscription.updated_at:type_name -> google.protobuf.Timestamp
27, // 21: stream.app.v1.WalletTransaction.created_at:type_name -> google.protobuf.Timestamp
27, // 22: stream.app.v1.WalletTransaction.updated_at:type_name -> google.protobuf.Timestamp
27, // 23: stream.app.v1.PaymentHistoryItem.expires_at:type_name -> google.protobuf.Timestamp
27, // 24: stream.app.v1.PaymentHistoryItem.created_at:type_name -> google.protobuf.Timestamp
27, // 25: stream.app.v1.Video.created_at:type_name -> google.protobuf.Timestamp
27, // 26: stream.app.v1.Video.updated_at:type_name -> google.protobuf.Timestamp
27, // 27: stream.app.v1.AdminUser.created_at:type_name -> google.protobuf.Timestamp
27, // 28: stream.app.v1.AdminUser.updated_at:type_name -> google.protobuf.Timestamp
28, // 0: stream.app.v1.User.plan_started_at:type_name -> google.protobuf.Timestamp
28, // 1: stream.app.v1.User.plan_expires_at:type_name -> google.protobuf.Timestamp
28, // 2: stream.app.v1.User.created_at:type_name -> google.protobuf.Timestamp
28, // 3: stream.app.v1.User.updated_at:type_name -> google.protobuf.Timestamp
28, // 4: stream.app.v1.Notification.created_at:type_name -> google.protobuf.Timestamp
28, // 5: stream.app.v1.Domain.created_at:type_name -> google.protobuf.Timestamp
28, // 6: stream.app.v1.Domain.updated_at:type_name -> google.protobuf.Timestamp
28, // 7: stream.app.v1.AdTemplate.created_at:type_name -> google.protobuf.Timestamp
28, // 8: stream.app.v1.AdTemplate.updated_at:type_name -> google.protobuf.Timestamp
28, // 9: stream.app.v1.PopupAd.created_at:type_name -> google.protobuf.Timestamp
28, // 10: stream.app.v1.PopupAd.updated_at:type_name -> google.protobuf.Timestamp
28, // 11: stream.app.v1.PlayerConfig.created_at:type_name -> google.protobuf.Timestamp
28, // 12: stream.app.v1.PlayerConfig.updated_at:type_name -> google.protobuf.Timestamp
28, // 13: stream.app.v1.AdminPlayerConfig.created_at:type_name -> google.protobuf.Timestamp
28, // 14: stream.app.v1.AdminPlayerConfig.updated_at:type_name -> google.protobuf.Timestamp
28, // 15: stream.app.v1.Payment.created_at:type_name -> google.protobuf.Timestamp
28, // 16: stream.app.v1.Payment.updated_at:type_name -> google.protobuf.Timestamp
28, // 17: stream.app.v1.PlanSubscription.started_at:type_name -> google.protobuf.Timestamp
28, // 18: stream.app.v1.PlanSubscription.expires_at:type_name -> google.protobuf.Timestamp
28, // 19: stream.app.v1.PlanSubscription.created_at:type_name -> google.protobuf.Timestamp
28, // 20: stream.app.v1.PlanSubscription.updated_at:type_name -> google.protobuf.Timestamp
28, // 21: stream.app.v1.WalletTransaction.created_at:type_name -> google.protobuf.Timestamp
28, // 22: stream.app.v1.WalletTransaction.updated_at:type_name -> google.protobuf.Timestamp
28, // 23: stream.app.v1.PaymentHistoryItem.expires_at:type_name -> google.protobuf.Timestamp
28, // 24: stream.app.v1.PaymentHistoryItem.created_at:type_name -> google.protobuf.Timestamp
28, // 25: stream.app.v1.Video.created_at:type_name -> google.protobuf.Timestamp
28, // 26: stream.app.v1.Video.updated_at:type_name -> google.protobuf.Timestamp
28, // 27: stream.app.v1.AdminUser.created_at:type_name -> google.protobuf.Timestamp
28, // 28: stream.app.v1.AdminUser.updated_at:type_name -> google.protobuf.Timestamp
17, // 29: stream.app.v1.AdminUserReferralInfo.referrer:type_name -> stream.app.v1.ReferralUserSummary
27, // 30: stream.app.v1.AdminUserReferralInfo.reward_granted_at:type_name -> google.protobuf.Timestamp
28, // 30: stream.app.v1.AdminUserReferralInfo.reward_granted_at:type_name -> google.protobuf.Timestamp
16, // 31: stream.app.v1.AdminUserDetail.user:type_name -> stream.app.v1.AdminUser
11, // 32: stream.app.v1.AdminUserDetail.subscription:type_name -> stream.app.v1.PlanSubscription
18, // 33: stream.app.v1.AdminUserDetail.referral:type_name -> stream.app.v1.AdminUserReferralInfo
27, // 34: stream.app.v1.AdminVideo.created_at:type_name -> google.protobuf.Timestamp
27, // 35: stream.app.v1.AdminVideo.updated_at:type_name -> google.protobuf.Timestamp
27, // 36: stream.app.v1.AdminPayment.created_at:type_name -> google.protobuf.Timestamp
27, // 37: stream.app.v1.AdminPayment.updated_at:type_name -> google.protobuf.Timestamp
27, // 38: stream.app.v1.AdminAdTemplate.created_at:type_name -> google.protobuf.Timestamp
27, // 39: stream.app.v1.AdminAdTemplate.updated_at:type_name -> google.protobuf.Timestamp
27, // 40: stream.app.v1.AdminPopupAd.created_at:type_name -> google.protobuf.Timestamp
27, // 41: stream.app.v1.AdminPopupAd.updated_at:type_name -> google.protobuf.Timestamp
27, // 42: stream.app.v1.AdminJob.created_at:type_name -> google.protobuf.Timestamp
27, // 43: stream.app.v1.AdminJob.updated_at:type_name -> google.protobuf.Timestamp
27, // 44: stream.app.v1.AdminAgent.last_heartbeat:type_name -> google.protobuf.Timestamp
27, // 45: stream.app.v1.AdminAgent.created_at:type_name -> google.protobuf.Timestamp
27, // 46: stream.app.v1.AdminAgent.updated_at:type_name -> google.protobuf.Timestamp
47, // [47:47] is the sub-list for method output_type
47, // [47:47] is the sub-list for method input_type
47, // [47:47] is the sub-list for extension type_name
47, // [47:47] is the sub-list for extension extendee
0, // [0:47] is the sub-list for field type_name
28, // 34: stream.app.v1.AdminVideo.created_at:type_name -> google.protobuf.Timestamp
28, // 35: stream.app.v1.AdminVideo.updated_at:type_name -> google.protobuf.Timestamp
28, // 36: stream.app.v1.AdminPayment.created_at:type_name -> google.protobuf.Timestamp
28, // 37: stream.app.v1.AdminPayment.updated_at:type_name -> google.protobuf.Timestamp
28, // 38: stream.app.v1.AdminAdTemplate.created_at:type_name -> google.protobuf.Timestamp
28, // 39: stream.app.v1.AdminAdTemplate.updated_at:type_name -> google.protobuf.Timestamp
28, // 40: stream.app.v1.AdminPopupAd.created_at:type_name -> google.protobuf.Timestamp
28, // 41: stream.app.v1.AdminPopupAd.updated_at:type_name -> google.protobuf.Timestamp
28, // 42: stream.app.v1.AdminJob.created_at:type_name -> google.protobuf.Timestamp
28, // 43: stream.app.v1.AdminJob.updated_at:type_name -> google.protobuf.Timestamp
28, // 44: stream.app.v1.AdminAgent.last_heartbeat:type_name -> google.protobuf.Timestamp
28, // 45: stream.app.v1.AdminAgent.created_at:type_name -> google.protobuf.Timestamp
28, // 46: stream.app.v1.AdminAgent.updated_at:type_name -> google.protobuf.Timestamp
25, // 47: stream.app.v1.AdminDlqEntry.job:type_name -> stream.app.v1.AdminJob
28, // 48: stream.app.v1.AdminDlqEntry.failure_time:type_name -> google.protobuf.Timestamp
49, // [49:49] is the sub-list for method output_type
49, // [49:49] is the sub-list for method input_type
49, // [49:49] is the sub-list for extension type_name
49, // [49:49] is the sub-list for extension extendee
0, // [0:49] is the sub-list for field type_name
}
func init() { file_app_v1_common_proto_init() }
@@ -4085,7 +4162,7 @@ func file_app_v1_common_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_app_v1_common_proto_rawDesc), len(file_app_v1_common_proto_rawDesc)),
NumEnums: 0,
NumMessages: 27,
NumMessages: 28,
NumExtensions: 0,
NumServices: 0,
},

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

View File

@@ -44,3 +44,10 @@ type JobConfigEnvelope struct {
VideoID string `json:"video_id,omitempty"`
TimeLimit int64 `json:"time_limit,omitempty"`
}
type DLQEntry struct {
Job *model.Job `json:"job,omitempty"`
FailureTime int64 `json:"failure_time_unix,omitempty"`
Reason string `json:"reason,omitempty"`
RetryCount int64 `json:"retry_count,omitempty"`
}

View File

@@ -100,3 +100,82 @@ func (r *jobRepository) GetLatestByVideoID(ctx context.Context, videoID string)
}
return &job, nil
}
func (r *jobRepository) AssignPendingJob(ctx context.Context, jobID string, agentID int64, now time.Time) (bool, error) {
result, err := query.Job.WithContext(ctx).
Where(query.Job.ID.Eq(strings.TrimSpace(jobID)), query.Job.Status.Eq("pending"), query.Job.Cancelled.Is(false)).
Updates(map[string]any{"status": "running", "agent_id": agentID, "updated_at": now})
if err != nil {
return false, err
}
return result.RowsAffected > 0, nil
}
func (r *jobRepository) MarkJobStatusIfCurrent(ctx context.Context, jobID string, fromStatuses []string, toStatus string, now time.Time, clearAgent bool) (bool, error) {
jobID = strings.TrimSpace(jobID)
updates := map[string]any{"status": toStatus, "updated_at": now}
if clearAgent {
updates["agent_id"] = nil
}
q := query.Job.WithContext(ctx).Where(query.Job.ID.Eq(jobID), query.Job.Status.In(fromStatuses...))
result, err := q.Updates(updates)
if err != nil {
return false, err
}
return result.RowsAffected > 0, nil
}
func (r *jobRepository) CancelJobIfActive(ctx context.Context, jobID string, now time.Time) (bool, error) {
result, err := query.Job.WithContext(ctx).
Where(query.Job.ID.Eq(strings.TrimSpace(jobID)), query.Job.Status.In("pending", "running")).
Updates(map[string]any{"status": "cancelled", "cancelled": true, "updated_at": now})
if err != nil {
return false, err
}
return result.RowsAffected > 0, nil
}
func (r *jobRepository) RequeueJob(ctx context.Context, jobID string, retryCount int64, logs *string, now time.Time) (bool, error) {
updates := map[string]any{"status": "pending", "cancelled": false, "progress": 0, "agent_id": nil, "retry_count": retryCount, "updated_at": now}
if logs != nil {
updates["logs"] = *logs
}
result, err := query.Job.WithContext(ctx).
Where(query.Job.ID.Eq(strings.TrimSpace(jobID)), query.Job.Status.In("running", "pending", "failure", "cancelled")).
Updates(updates)
if err != nil {
return false, err
}
return result.RowsAffected > 0, nil
}
func (r *jobRepository) MoveJobToFailure(ctx context.Context, jobID string, logs *string, now time.Time) (bool, error) {
updates := map[string]any{"status": "failure", "agent_id": nil, "updated_at": now}
if logs != nil {
updates["logs"] = *logs
}
result, err := query.Job.WithContext(ctx).
Where(query.Job.ID.Eq(strings.TrimSpace(jobID)), query.Job.Status.In("running", "pending")).
Updates(updates)
if err != nil {
return false, err
}
return result.RowsAffected > 0, nil
}
func (r *jobRepository) UpdateProgressAndLogsIfRunning(ctx context.Context, jobID string, progress *float64, logs *string, now time.Time) (bool, error) {
updates := map[string]any{"updated_at": now}
if progress != nil {
updates["progress"] = *progress
}
if logs != nil {
updates["logs"] = *logs
}
result, err := query.Job.WithContext(ctx).
Where(query.Job.ID.Eq(strings.TrimSpace(jobID)), query.Job.Status.Eq("running")).
Updates(updates)
if err != nil {
return false, err
}
return result.RowsAffected > 0, nil
}

View File

@@ -1,15 +1,17 @@
package service
import (
"context"
"fmt"
"testing"
"time"
"github.com/google/uuid"
"google.golang.org/grpc/codes"
"gorm.io/gorm"
redisadapter "stream.api/internal/adapters/redis"
appv1 "stream.api/internal/api/proto/app/v1"
"stream.api/internal/database/model"
runtimeservices "stream.api/internal/service/runtime/services"
renderworkflow "stream.api/internal/workflow/render"
)
@@ -18,7 +20,7 @@ func TestListAdminJobsCursorPagination(t *testing.T) {
ensureTestJobsTable(t, db)
services := newTestAppServices(t, db)
services.videoWorkflowService = renderworkflow.New(db, runtimeservices.NewJobService(db, nil, nil))
services.videoWorkflowService = renderworkflow.New(db, NewJobService(db, nil, nil, nil))
admin := seedTestUser(t, db, model.User{ID: uuid.NewString(), Email: "admin@example.com", Role: ptrString("ADMIN")})
baseTime := time.Date(2026, 3, 22, 10, 0, 0, 0, time.UTC)
@@ -67,7 +69,7 @@ func TestListAdminJobsInvalidCursor(t *testing.T) {
ensureTestJobsTable(t, db)
services := newTestAppServices(t, db)
services.videoWorkflowService = renderworkflow.New(db, runtimeservices.NewJobService(db, nil, nil))
services.videoWorkflowService = renderworkflow.New(db, NewJobService(db, nil, nil, nil))
admin := seedTestUser(t, db, model.User{ID: uuid.NewString(), Email: "admin@example.com", Role: ptrString("ADMIN")})
conn, cleanup := newTestGRPCServer(t, services)
@@ -86,7 +88,7 @@ func TestListAdminJobsCursorRejectsAgentMismatch(t *testing.T) {
ensureTestJobsTable(t, db)
services := newTestAppServices(t, db)
services.videoWorkflowService = renderworkflow.New(db, runtimeservices.NewJobService(db, nil, nil))
services.videoWorkflowService = renderworkflow.New(db, NewJobService(db, nil, nil, nil))
admin := seedTestUser(t, db, model.User{ID: uuid.NewString(), Email: "admin@example.com", Role: ptrString("ADMIN")})
baseTime := time.Date(2026, 3, 22, 11, 0, 0, 0, time.UTC)
@@ -192,4 +194,212 @@ func assertAdminJobIDs(t *testing.T, jobs []*appv1.AdminJob, want []string) {
}
}
func ptrTime(v time.Time) *time.Time { return &v }
type fakeDLQ struct {
entries map[string]*redisadapter.DLQEntry
order []string
listErr error
getErr error
removeErr error
retryErr error
}
func newFakeDLQ(entries ...*redisadapter.DLQEntry) *fakeDLQ {
f := &fakeDLQ{entries: map[string]*redisadapter.DLQEntry{}, order: []string{}}
for _, entry := range entries {
if entry == nil || entry.Job == nil {
continue
}
f.entries[entry.Job.ID] = entry
f.order = append(f.order, entry.Job.ID)
}
return f
}
func (f *fakeDLQ) Add(_ context.Context, job *model.Job, reason string) error {
if f.entries == nil {
f.entries = map[string]*redisadapter.DLQEntry{}
}
entry := &redisadapter.DLQEntry{Job: job, FailureTime: time.Now().UTC(), Reason: reason}
f.entries[job.ID] = entry
f.order = append(f.order, job.ID)
return nil
}
func (f *fakeDLQ) Get(_ context.Context, jobID string) (*redisadapter.DLQEntry, error) {
if f.getErr != nil {
return nil, f.getErr
}
entry, ok := f.entries[jobID]
if !ok {
return nil, fmt.Errorf("job not found in DLQ")
}
return entry, nil
}
func (f *fakeDLQ) List(_ context.Context, offset, limit int64) ([]*redisadapter.DLQEntry, error) {
if f.listErr != nil {
return nil, f.listErr
}
if offset < 0 {
offset = 0
}
if limit <= 0 {
limit = int64(len(f.order))
}
items := make([]*redisadapter.DLQEntry, 0)
for i := offset; i < int64(len(f.order)) && int64(len(items)) < limit; i++ {
id := f.order[i]
if entry, ok := f.entries[id]; ok {
items = append(items, entry)
}
}
return items, nil
}
func (f *fakeDLQ) Count(_ context.Context) (int64, error) {
return int64(len(f.entries)), nil
}
func (f *fakeDLQ) Remove(_ context.Context, jobID string) error {
if f.removeErr != nil {
return f.removeErr
}
if _, ok := f.entries[jobID]; !ok {
return fmt.Errorf("job not found in DLQ")
}
delete(f.entries, jobID)
filtered := make([]string, 0, len(f.order))
for _, id := range f.order {
if id != jobID {
filtered = append(filtered, id)
}
}
f.order = filtered
return nil
}
func (f *fakeDLQ) Retry(ctx context.Context, jobID string) (*model.Job, error) {
if f.retryErr != nil {
return nil, f.retryErr
}
entry, err := f.Get(ctx, jobID)
if err != nil {
return nil, err
}
if err := f.Remove(ctx, jobID); err != nil {
return nil, err
}
return entry.Job, nil
}
type fakeQueue struct {
enqueueErr error
}
func (f *fakeQueue) Enqueue(_ context.Context, _ *model.Job) error { return f.enqueueErr }
func (f *fakeQueue) Dequeue(_ context.Context) (*model.Job, error) { return nil, nil }
func TestAdminDlqJobs(t *testing.T) {
t.Run("list happy path", func(t *testing.T) {
db := newTestDB(t)
ensureTestJobsTable(t, db)
admin := seedTestUser(t, db, model.User{ID: uuid.NewString(), Email: "admin@example.com", Role: ptrString("ADMIN")})
job1 := seedTestJob(t, db, model.Job{ID: "job-dlq-1", CreatedAt: ptrTime(time.Now().Add(-2 * time.Hour).UTC()), UpdatedAt: ptrTime(time.Now().Add(-2 * time.Hour).UTC())})
job2 := seedTestJob(t, db, model.Job{ID: "job-dlq-2", CreatedAt: ptrTime(time.Now().Add(-time.Hour).UTC()), UpdatedAt: ptrTime(time.Now().Add(-time.Hour).UTC())})
services := newTestAppServices(t, db)
services.videoWorkflowService = renderworkflow.New(db, NewJobService(db, &fakeQueue{}, nil, newFakeDLQ(
&redisadapter.DLQEntry{Job: &job1, FailureTime: time.Now().Add(-30 * time.Minute).UTC(), Reason: "lease_expired", RetryCount: 2},
&redisadapter.DLQEntry{Job: &job2, FailureTime: time.Now().Add(-10 * time.Minute).UTC(), Reason: "invalid_config", RetryCount: 3},
)))
conn, cleanup := newTestGRPCServer(t, services)
defer cleanup()
client := newAdminClient(conn)
resp, err := client.ListAdminDlqJobs(testActorOutgoingContext(admin.ID, "ADMIN"), &appv1.ListAdminDlqJobsRequest{Offset: 0, Limit: 10})
if err != nil {
t.Fatalf("ListAdminDlqJobs error = %v", err)
}
if resp.GetTotal() != 2 {
t.Fatalf("total = %d, want 2", resp.GetTotal())
}
if len(resp.GetItems()) != 2 {
t.Fatalf("items len = %d, want 2", len(resp.GetItems()))
}
if resp.GetItems()[0].GetJob().GetId() != "job-dlq-1" {
t.Fatalf("first job id = %q", resp.GetItems()[0].GetJob().GetId())
}
if resp.GetItems()[0].GetReason() != "lease_expired" {
t.Fatalf("first reason = %q", resp.GetItems()[0].GetReason())
}
})
t.Run("get not found", func(t *testing.T) {
db := newTestDB(t)
ensureTestJobsTable(t, db)
admin := seedTestUser(t, db, model.User{ID: uuid.NewString(), Email: "admin@example.com", Role: ptrString("ADMIN")})
services := newTestAppServices(t, db)
services.videoWorkflowService = renderworkflow.New(db, NewJobService(db, &fakeQueue{}, nil, newFakeDLQ()))
conn, cleanup := newTestGRPCServer(t, services)
defer cleanup()
client := newAdminClient(conn)
_, err := client.GetAdminDlqJob(testActorOutgoingContext(admin.ID, "ADMIN"), &appv1.GetAdminDlqJobRequest{Id: "missing"})
assertGRPCCode(t, err, codes.NotFound)
})
t.Run("retry happy path", func(t *testing.T) {
db := newTestDB(t)
ensureTestJobsTable(t, db)
admin := seedTestUser(t, db, model.User{ID: uuid.NewString(), Email: "admin@example.com", Role: ptrString("ADMIN")})
job := seedTestJob(t, db, model.Job{ID: "job-dlq-retry", Status: ptrString("failure"), CreatedAt: ptrTime(time.Now().Add(-time.Hour).UTC()), UpdatedAt: ptrTime(time.Now().Add(-time.Hour).UTC())})
services := newTestAppServices(t, db)
services.videoWorkflowService = renderworkflow.New(db, NewJobService(db, &fakeQueue{}, nil, newFakeDLQ(
&redisadapter.DLQEntry{Job: &job, FailureTime: time.Now().UTC(), Reason: "lease_expired", RetryCount: 1},
)))
conn, cleanup := newTestGRPCServer(t, services)
defer cleanup()
client := newAdminClient(conn)
resp, err := client.RetryAdminDlqJob(testActorOutgoingContext(admin.ID, "ADMIN"), &appv1.RetryAdminDlqJobRequest{Id: job.ID})
if err != nil {
t.Fatalf("RetryAdminDlqJob error = %v", err)
}
if resp.GetJob().GetId() != job.ID {
t.Fatalf("job id = %q, want %q", resp.GetJob().GetId(), job.ID)
}
if resp.GetJob().GetStatus() != "pending" {
t.Fatalf("job status = %q, want pending", resp.GetJob().GetStatus())
}
})
t.Run("remove happy path", func(t *testing.T) {
db := newTestDB(t)
ensureTestJobsTable(t, db)
admin := seedTestUser(t, db, model.User{ID: uuid.NewString(), Email: "admin@example.com", Role: ptrString("ADMIN")})
job := seedTestJob(t, db, model.Job{ID: "job-dlq-remove", CreatedAt: ptrTime(time.Now().Add(-time.Hour).UTC()), UpdatedAt: ptrTime(time.Now().Add(-time.Hour).UTC())})
services := newTestAppServices(t, db)
services.videoWorkflowService = renderworkflow.New(db, NewJobService(db, &fakeQueue{}, nil, newFakeDLQ(
&redisadapter.DLQEntry{Job: &job, FailureTime: time.Now().UTC(), Reason: "invalid_config", RetryCount: 3},
)))
conn, cleanup := newTestGRPCServer(t, services)
defer cleanup()
client := newAdminClient(conn)
resp, err := client.RemoveAdminDlqJob(testActorOutgoingContext(admin.ID, "ADMIN"), &appv1.RemoveAdminDlqJobRequest{Id: job.ID})
if err != nil {
t.Fatalf("RemoveAdminDlqJob error = %v", err)
}
if resp.GetStatus() != "removed" {
t.Fatalf("status = %q, want removed", resp.GetStatus())
}
})
t.Run("list permission denied", func(t *testing.T) {
db := newTestDB(t)
ensureTestJobsTable(t, db)
user := seedTestUser(t, db, model.User{ID: uuid.NewString(), Email: "user@example.com", Role: ptrString("USER")})
services := newTestAppServices(t, db)
services.videoWorkflowService = renderworkflow.New(db, NewJobService(db, &fakeQueue{}, nil, newFakeDLQ()))
conn, cleanup := newTestGRPCServer(t, services)
defer cleanup()
client := newAdminClient(conn)
_, err := client.ListAdminDlqJobs(testActorOutgoingContext(user.ID, "USER"), &appv1.ListAdminDlqJobsRequest{})
assertGRPCCode(t, err, codes.PermissionDenied)
})
}

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

View File

@@ -65,31 +65,61 @@ func buildAdminJob(job *model.Job) *appv1.AdminJob {
if job == nil {
return nil
}
agentID := strconv.FormatInt(*job.AgentID, 10)
var agentID *string
if job.AgentID != nil {
value := strconv.FormatInt(*job.AgentID, 10)
agentID = &value
}
return &appv1.AdminJob{
Id: job.ID,
Status: string(*job.Status),
Priority: int32(*job.Priority),
UserId: *job.UserID,
Status: stringValue(job.Status),
Priority: int32(int64Value(job.Priority)),
UserId: stringValue(job.UserID),
Name: job.ID,
TimeLimit: *job.TimeLimit,
InputUrl: *job.InputURL,
OutputUrl: *job.OutputURL,
TotalDuration: *job.TotalDuration,
CurrentTime: *job.CurrentTime,
Progress: *job.Progress,
AgentId: &agentID,
Logs: *job.Logs,
Config: *job.Config,
Cancelled: *job.Cancelled,
RetryCount: int32(*job.RetryCount),
MaxRetries: int32(*job.MaxRetries),
CreatedAt: timestamppb.New(*job.CreatedAt),
UpdatedAt: timestamppb.New(*job.UpdatedAt),
VideoId: stringPointerOrNil(*job.VideoID),
TimeLimit: int64Value(job.TimeLimit),
InputUrl: stringValue(job.InputURL),
OutputUrl: stringValue(job.OutputURL),
TotalDuration: int64Value(job.TotalDuration),
CurrentTime: int64Value(job.CurrentTime),
Progress: float64Value(job.Progress),
AgentId: agentID,
Logs: stringValue(job.Logs),
Config: stringValue(job.Config),
Cancelled: boolValue(job.Cancelled),
RetryCount: int32(int64Value(job.RetryCount)),
MaxRetries: int32(int64Value(job.MaxRetries)),
CreatedAt: timeToProto(job.CreatedAt),
UpdatedAt: timeToProto(job.UpdatedAt),
VideoId: job.VideoID,
}
}
func buildAdminDlqEntry(entry *dto.DLQEntry) *appv1.AdminDlqEntry {
if entry == nil {
return nil
}
return &appv1.AdminDlqEntry{
Job: buildAdminJob(entry.Job),
FailureTime: timestamppb.New(time.Unix(entry.FailureTime, 0).UTC()),
Reason: entry.Reason,
RetryCount: int32(entry.RetryCount),
}
}
func int64Value(value *int64) int64 {
if value == nil {
return 0
}
return *value
}
func float64Value(value *float64) float64 {
if value == nil {
return 0
}
return *value
}
func buildAdminAgent(agent *dto.AgentWithStats) *appv1.AdminAgent {
if agent == nil || agent.Agent == nil {
return nil
@@ -532,7 +562,12 @@ func (s *appServices) buildAdminPopupAd(ctx context.Context, item *model.PopupAd
Label: item.Label,
Value: item.Value,
IsActive: boolValue(item.IsActive),
MaxTriggersPerSession: func() int32 { if item.MaxTriggersPerSession != nil { return *item.MaxTriggersPerSession }; return 0 }(),
MaxTriggersPerSession: func() int32 {
if item.MaxTriggersPerSession != nil {
return *item.MaxTriggersPerSession
}
return 0
}(),
CreatedAt: timeToProto(item.CreatedAt),
UpdatedAt: timeToProto(item.UpdatedAt),
}

View File

@@ -45,7 +45,12 @@ func toProtoPopupAd(item *model.PopupAd) *appv1.PopupAd {
Label: item.Label,
Value: item.Value,
IsActive: boolValue(item.IsActive),
MaxTriggersPerSession: func() int32 { if item.MaxTriggersPerSession != nil { return *item.MaxTriggersPerSession }; return 0 }(),
MaxTriggersPerSession: func() int32 {
if item.MaxTriggersPerSession != nil {
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)
CancelJob(ctx context.Context, id string) error
RetryJob(ctx context.Context, id string) (*model.Job, error)
ListDLQ(ctx context.Context, offset, limit int) ([]*dto.DLQEntry, int64, error)
GetDLQ(ctx context.Context, id string) (*dto.DLQEntry, error)
RetryDLQ(ctx context.Context, id string) (*model.Job, error)
RemoveDLQ(ctx context.Context, id string) error
}
type VideoRepository interface {
@@ -199,6 +203,12 @@ type JobRepository interface {
ListByOffset(ctx context.Context, agentID string, offset int, limit int) ([]*model.Job, int64, error)
Save(ctx context.Context, job *model.Job) error
UpdateVideoStatus(ctx context.Context, videoID string, statusValue string, processingStatus string) error
AssignPendingJob(ctx context.Context, jobID string, agentID int64, now time.Time) (bool, error)
MarkJobStatusIfCurrent(ctx context.Context, jobID string, fromStatuses []string, toStatus string, now time.Time, clearAgent bool) (bool, error)
CancelJobIfActive(ctx context.Context, jobID string, now time.Time) (bool, error)
RequeueJob(ctx context.Context, jobID string, retryCount int64, logs *string, now time.Time) (bool, error)
MoveJobToFailure(ctx context.Context, jobID string, logs *string, now time.Time) (bool, error)
UpdateProgressAndLogsIfRunning(ctx context.Context, jobID string, progress *float64, logs *string, now time.Time) (bool, error)
}
type AgentRuntime interface {

View File

@@ -8,9 +8,9 @@ import (
"github.com/google/uuid"
"google.golang.org/grpc/metadata"
_ "modernc.org/sqlite"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
_ "modernc.org/sqlite"
appv1 "stream.api/internal/api/proto/app/v1"
"stream.api/internal/database/model"
"stream.api/internal/database/query"

View File

@@ -65,12 +65,7 @@ func (s *appServices) buildReferralShareLink(username *string) *string {
return nil
}
path := "/ref/" + url.PathEscape(trimmed)
base := strings.TrimRight(strings.TrimSpace(s.frontendBaseURL), "/")
if base == "" {
return &path
}
link := base + path
return &link
}
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.RegisterPaymentsServer(server, services.PaymentsServer)
appv1.RegisterVideosServer(server, services.VideosServer)
appv1.RegisterVideoMetadataServer(server, services.VideoMetadataServer)
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
}
func (s *appServices) ListAdminDlqJobs(ctx context.Context, req *appv1.ListAdminDlqJobsRequest) (*appv1.ListAdminDlqJobsResponse, error) {
if _, err := s.requireAdmin(ctx); err != nil {
return nil, err
}
if s.videoWorkflowService == nil {
return nil, status.Error(codes.Unavailable, "Job service is unavailable")
}
offset := int(req.GetOffset())
limit := int(req.GetLimit())
items, total, err := s.videoWorkflowService.ListDLQ(ctx, offset, limit)
if err != nil {
return nil, status.Error(codes.Internal, "Failed to list DLQ jobs")
}
entries := make([]*appv1.AdminDlqEntry, 0, len(items))
for _, item := range items {
entries = append(entries, buildAdminDlqEntry(item))
}
return &appv1.ListAdminDlqJobsResponse{Items: entries, Total: total, Offset: int32(offset), Limit: int32(limit)}, nil
}
func (s *appServices) GetAdminDlqJob(ctx context.Context, req *appv1.GetAdminDlqJobRequest) (*appv1.GetAdminDlqJobResponse, error) {
if _, err := s.requireAdmin(ctx); err != nil {
return nil, err
}
if s.videoWorkflowService == nil {
return nil, status.Error(codes.Unavailable, "Job service is unavailable")
}
id := strings.TrimSpace(req.GetId())
if id == "" {
return nil, status.Error(codes.NotFound, "Job not found in DLQ")
}
item, err := s.videoWorkflowService.GetDLQ(ctx, id)
if err != nil {
if strings.Contains(strings.ToLower(err.Error()), "not found") {
return nil, status.Error(codes.NotFound, "Job not found in DLQ")
}
return nil, status.Error(codes.Internal, "Failed to load DLQ job")
}
return &appv1.GetAdminDlqJobResponse{Item: buildAdminDlqEntry(item)}, nil
}
func (s *appServices) RetryAdminDlqJob(ctx context.Context, req *appv1.RetryAdminDlqJobRequest) (*appv1.RetryAdminDlqJobResponse, error) {
if _, err := s.requireAdmin(ctx); err != nil {
return nil, err
}
if s.videoWorkflowService == nil {
return nil, status.Error(codes.Unavailable, "Job service is unavailable")
}
id := strings.TrimSpace(req.GetId())
if id == "" {
return nil, status.Error(codes.NotFound, "Job not found in DLQ")
}
job, err := s.videoWorkflowService.RetryDLQ(ctx, id)
if err != nil {
if strings.Contains(strings.ToLower(err.Error()), "not found") {
return nil, status.Error(codes.NotFound, "Job not found in DLQ")
}
return nil, status.Error(codes.FailedPrecondition, err.Error())
}
return &appv1.RetryAdminDlqJobResponse{Job: buildAdminJob(job)}, nil
}
func (s *appServices) RemoveAdminDlqJob(ctx context.Context, req *appv1.RemoveAdminDlqJobRequest) (*appv1.RemoveAdminDlqJobResponse, error) {
if _, err := s.requireAdmin(ctx); err != nil {
return nil, err
}
if s.videoWorkflowService == nil {
return nil, status.Error(codes.Unavailable, "Job service is unavailable")
}
id := strings.TrimSpace(req.GetId())
if id == "" {
return nil, status.Error(codes.NotFound, "Job not found in DLQ")
}
if err := s.videoWorkflowService.RemoveDLQ(ctx, id); err != nil {
if strings.Contains(strings.ToLower(err.Error()), "not found") {
return nil, status.Error(codes.NotFound, "Job not found in DLQ")
}
return nil, status.Error(codes.Internal, "Failed to remove DLQ job")
}
return &appv1.RemoveAdminDlqJobResponse{Status: "removed", JobId: id}, nil
}
func (s *appServices) ListAdminAgents(ctx context.Context, _ *appv1.ListAdminAgentsRequest) (*appv1.ListAdminAgentsResponse, error) {
if _, err := s.requireAdmin(ctx); err != nil {
return nil, err

View File

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

View File

@@ -12,9 +12,11 @@ import (
"time"
"gorm.io/gorm"
redisadapter "stream.api/internal/adapters/redis"
"stream.api/internal/database/model"
"stream.api/internal/dto"
"stream.api/internal/repository"
"stream.api/pkg/logger"
)
type JobQueue interface {
@@ -33,20 +35,36 @@ type LogPubSub interface {
SubscribeJobUpdates(ctx context.Context) (<-chan string, error)
}
type DeadLetterQueue interface {
Add(ctx context.Context, job *model.Job, reason string) error
Get(ctx context.Context, jobID string) (*redisadapter.DLQEntry, error)
List(ctx context.Context, offset, limit int64) ([]*redisadapter.DLQEntry, error)
Count(ctx context.Context) (int64, error)
Remove(ctx context.Context, jobID string) error
Retry(ctx context.Context, jobID string) (*model.Job, error)
}
type JobService struct {
queue JobQueue
pubsub LogPubSub
dlq DeadLetterQueue
jobRepository JobRepository
logger logger.Logger
}
func NewJobService(db *gorm.DB, queue JobQueue, pubsub LogPubSub) *JobService {
func NewJobService(db *gorm.DB, queue JobQueue, pubsub LogPubSub, dlq DeadLetterQueue) *JobService {
return &JobService{
queue: queue,
pubsub: pubsub,
dlq: dlq,
jobRepository: repository.NewJobRepository(db),
}
}
func (s *JobService) SetLogger(l logger.Logger) {
s.logger = l
}
var ErrInvalidJobCursor = errors.New("invalid job cursor")
func strPtr(v string) *string { return &v }
@@ -165,11 +183,16 @@ func (s *JobService) CreateJob(ctx context.Context, userID string, videoID strin
if err := syncVideoStatus(ctx, s.jobRepository, videoID, dto.JobStatusPending); err != nil {
return nil, err
}
// dtoJob := todtoJob(job)
if s.queue == nil {
return nil, errors.New("job queue is unavailable")
}
if err := s.removeFromQueue(ctx, job.ID); err != nil {
return nil, err
}
if err := s.queue.Enqueue(ctx, job); err != nil {
return nil, err
}
_ = s.pubsub.PublishJobUpdate(ctx, job.ID, status, videoID)
_ = s.publishJobUpdate(ctx, job.ID, status, videoID)
return job, nil
}
@@ -233,18 +256,55 @@ func (s *JobService) GetJob(ctx context.Context, id string) (*model.Job, error)
}
func (s *JobService) GetNextJob(ctx context.Context) (*model.Job, error) {
return s.queue.Dequeue(ctx)
if s.queue == nil {
return nil, errors.New("job queue is unavailable")
}
for {
job, err := s.queue.Dequeue(ctx)
if err != nil || job == nil {
return job, err
}
fresh, err := s.jobRepository.GetByID(ctx, job.ID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
continue
}
return nil, err
}
if !s.canDispatchJob(fresh) {
continue
}
return fresh, nil
}
}
func (s *JobService) SubscribeSystemResources(ctx context.Context) (<-chan dto.SystemResource, error) {
if s.pubsub == nil {
return nil, errors.New("job pubsub is unavailable")
}
return s.pubsub.SubscribeResources(ctx)
}
func (s *JobService) SubscribeJobLogs(ctx context.Context, jobID string) (<-chan dto.LogEntry, error) {
if s.pubsub == nil {
return nil, errors.New("job pubsub is unavailable")
}
return s.pubsub.Subscribe(ctx, jobID)
}
func (s *JobService) SubscribeCancel(ctx context.Context, agentID string) (<-chan string, error) {
if s.pubsub == nil {
return nil, errors.New("job pubsub is unavailable")
}
return s.pubsub.SubscribeCancel(ctx, agentID)
}
func (s *JobService) SubscribeJobUpdates(ctx context.Context) (<-chan string, error) {
if s.pubsub == nil {
return nil, errors.New("job pubsub is unavailable")
}
return s.pubsub.SubscribeJobUpdates(ctx)
}
@@ -253,17 +313,38 @@ func (s *JobService) UpdateJobStatus(ctx context.Context, jobID string, status d
if err != nil {
return err
}
currentStatus := s.jobStatus(job)
if (currentStatus == dto.JobStatusCancelled || currentStatus == dto.JobStatusSuccess) && currentStatus != status {
return nil
}
if currentStatus == status {
return nil
}
now := time.Now()
job.Status = strPtr(string(status))
job.UpdatedAt = &now
if err := s.jobRepository.Save(ctx, job); err != nil {
updated := false
switch status {
case dto.JobStatusRunning:
updated, err = s.jobRepository.MarkJobStatusIfCurrent(ctx, jobID, []string{string(dto.JobStatusPending)}, string(status), now, false)
case dto.JobStatusSuccess:
updated, err = s.jobRepository.MarkJobStatusIfCurrent(ctx, jobID, []string{string(dto.JobStatusRunning)}, string(status), now, true)
if err == nil && updated {
err = s.removeFromQueue(ctx, jobID)
}
default:
updated, err = s.jobRepository.MarkJobStatusIfCurrent(ctx, jobID, []string{string(currentStatus)}, string(status), now, status == dto.JobStatusFailure || status == dto.JobStatusCancelled)
}
if err != nil {
return err
}
if !updated {
return nil
}
cfg := parseJobConfig(job.Config)
if err := syncVideoStatus(ctx, s.jobRepository, cfg.VideoID, status); err != nil {
return err
}
return s.pubsub.PublishJobUpdate(ctx, jobID, string(status), cfg.VideoID)
return s.publishJobUpdate(ctx, jobID, string(status), cfg.VideoID)
}
func (s *JobService) AssignJob(ctx context.Context, jobID string, agentID string) error {
@@ -271,23 +352,26 @@ func (s *JobService) AssignJob(ctx context.Context, jobID string, agentID string
if err != nil {
return err
}
if !s.canDispatchJob(job) {
return fmt.Errorf("job %s is not dispatchable", jobID)
}
agentNumeric, err := strconv.ParseInt(agentID, 10, 64)
if err != nil {
return err
}
now := time.Now()
status := string(dto.JobStatusRunning)
job.AgentID = &agentNumeric
job.Status = &status
job.UpdatedAt = &now
if err := s.jobRepository.Save(ctx, job); err != nil {
updated, err := s.jobRepository.AssignPendingJob(ctx, jobID, agentNumeric, time.Now())
if err != nil {
return err
}
if !updated {
return fmt.Errorf("job %s is not dispatchable", jobID)
}
cfg := parseJobConfig(job.Config)
if err := syncVideoStatus(ctx, s.jobRepository, cfg.VideoID, dto.JobStatusRunning); err != nil {
return err
}
return s.pubsub.PublishJobUpdate(ctx, jobID, status, cfg.VideoID)
return s.publishJobUpdate(ctx, jobID, string(dto.JobStatusRunning), cfg.VideoID)
}
func (s *JobService) CancelJob(ctx context.Context, jobID string) error {
@@ -295,31 +379,25 @@ func (s *JobService) CancelJob(ctx context.Context, jobID string) error {
if err != nil {
return fmt.Errorf("job not found: %w", err)
}
currentStatus := ""
if job.Status != nil {
currentStatus = *job.Status
}
if currentStatus != string(dto.JobStatusPending) && currentStatus != string(dto.JobStatusRunning) {
return fmt.Errorf("cannot cancel job with status %s", currentStatus)
}
cancelled := true
status := string(dto.JobStatusCancelled)
now := time.Now()
job.Cancelled = &cancelled
job.Status = &status
job.UpdatedAt = &now
if err := s.jobRepository.Save(ctx, job); err != nil {
updated, err := s.jobRepository.CancelJobIfActive(ctx, jobID, time.Now())
if err != nil {
return err
}
if !updated {
return fmt.Errorf("cannot cancel job with status %s", s.jobStatus(job))
}
cfg := parseJobConfig(job.Config)
if err := syncVideoStatus(ctx, s.jobRepository, cfg.VideoID, dto.JobStatusCancelled); err != nil {
return err
}
_ = s.pubsub.PublishJobUpdate(ctx, jobID, status, cfg.VideoID)
if job.AgentID != nil {
_ = s.publishJobUpdate(ctx, jobID, string(dto.JobStatusCancelled), cfg.VideoID)
if err := s.removeFromQueue(ctx, job.ID); err != nil {
return err
}
if job.AgentID != nil && s.pubsub != nil {
_ = s.pubsub.PublishCancel(ctx, strconv.FormatInt(*job.AgentID, 10), job.ID)
}
return s.pubsub.Publish(ctx, jobID, "[SYSTEM] Job cancelled by admin", -1)
return s.publishLog(ctx, jobID, "[SYSTEM] Job cancelled by admin", -1)
}
func (s *JobService) RetryJob(ctx context.Context, jobID string) (*model.Job, error) {
@@ -327,47 +405,17 @@ func (s *JobService) RetryJob(ctx context.Context, jobID string) (*model.Job, er
if err != nil {
return nil, fmt.Errorf("job not found: %w", err)
}
currentStatus := ""
if job.Status != nil {
currentStatus = *job.Status
}
if currentStatus != string(dto.JobStatusFailure) && currentStatus != string(dto.JobStatusCancelled) {
currentStatus := s.jobStatus(job)
if currentStatus != dto.JobStatusFailure && currentStatus != dto.JobStatusCancelled {
return nil, fmt.Errorf("cannot retry job with status %s", currentStatus)
}
currentRetry := int64(0)
if job.RetryCount != nil {
currentRetry = *job.RetryCount
}
maxRetries := int64(3)
if job.MaxRetries != nil {
maxRetries = *job.MaxRetries
}
if currentRetry >= maxRetries {
return nil, fmt.Errorf("max retries (%d) exceeded", maxRetries)
}
pending := string(dto.JobStatusPending)
cancelled := false
progress := 0.0
now := time.Now()
job.Status = &pending
job.Cancelled = &cancelled
job.RetryCount = int64Ptr(currentRetry + 1)
job.Progress = &progress
job.AgentID = nil
job.UpdatedAt = &now
if err := s.jobRepository.Save(ctx, job); err != nil {
if err := s.requeueJob(ctx, job, false); err != nil {
return nil, err
}
cfg := parseJobConfig(job.Config)
if err := syncVideoStatus(ctx, s.jobRepository, cfg.VideoID, dto.JobStatusPending); err != nil {
return nil, err
if s.dlq != nil {
_ = s.dlq.Remove(ctx, jobID)
}
// dtoJob := todtoJob(job)
if err := s.queue.Enqueue(ctx, job); err != nil {
return nil, err
}
_ = s.pubsub.PublishJobUpdate(ctx, jobID, pending, cfg.VideoID)
return job, nil
return s.jobRepository.GetByID(ctx, jobID)
}
func (s *JobService) UpdateJobProgress(ctx context.Context, jobID string, progress float64) error {
@@ -381,7 +429,7 @@ func (s *JobService) UpdateJobProgress(ctx context.Context, jobID string, progre
if err := s.jobRepository.Save(ctx, job); err != nil {
return err
}
return s.pubsub.Publish(ctx, jobID, "", progress)
return s.publishLog(ctx, jobID, "", progress)
}
func (s *JobService) ProcessLog(ctx context.Context, jobID string, logData []byte) error {
@@ -420,7 +468,7 @@ func (s *JobService) ProcessLog(ctx context.Context, jobID string, logData []byt
if err := s.jobRepository.Save(ctx, job); err != nil {
return err
}
return s.pubsub.Publish(ctx, jobID, line, progress)
return s.publishLog(ctx, jobID, line, progress)
}
func syncVideoStatus(ctx context.Context, jobRepository JobRepository, videoID string, status dto.JobStatus) error {
@@ -444,5 +492,345 @@ func syncVideoStatus(ctx context.Context, jobRepository JobRepository, videoID s
}
func (s *JobService) PublishSystemResources(ctx context.Context, agentID string, data []byte) error {
if s.pubsub == nil {
return errors.New("job pubsub is unavailable")
}
return s.pubsub.PublishResource(ctx, agentID, data)
}
func (s *JobService) StartInflightReclaimLoop(ctx context.Context, interval time.Duration, batchSize int64) {
if interval <= 0 {
interval = 30 * time.Second
}
if s.logger != nil {
s.logger.Info("started inflight reclaim loop", "interval", interval.String(), "batch_size", batchSize)
}
ticker := time.NewTicker(interval)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
if s.logger != nil {
s.logger.Info("stopped inflight reclaim loop")
}
return
case <-ticker.C:
_ = s.reclaimExpiredOnce(ctx, batchSize)
}
}
}
func (s *JobService) reclaimExpiredOnce(ctx context.Context, batchSize int64) error {
type expirable interface {
ListExpiredInflight(ctx context.Context, now time.Time, limit int64) ([]string, error)
}
queue, ok := s.queue.(expirable)
if !ok {
return nil
}
startedAt := time.Now()
jobIDs, err := queue.ListExpiredInflight(ctx, time.Now(), batchSize)
if err != nil {
if s.logger != nil {
s.logger.Error("failed to list expired inflight jobs", "error", err)
}
return err
}
for _, jobID := range jobIDs {
_ = s.handleExpiredInflightJob(ctx, jobID)
}
if s.logger != nil && len(jobIDs) > 0 {
s.logger.Info("completed inflight reclaim batch", "expired_count", len(jobIDs), "duration_ms", time.Since(startedAt).Milliseconds())
}
return nil
}
func (s *JobService) handleExpiredInflightJob(ctx context.Context, jobID string) error {
job, err := s.jobRepository.GetByID(ctx, jobID)
if err != nil {
if s.logger != nil {
s.logger.Warn("failed to load expired inflight job", "job_id", jobID, "error", err)
}
return err
}
status := s.jobStatus(job)
if s.logger != nil {
s.logger.Warn("processing expired inflight job", "job_id", jobID, "current_status", status)
}
switch status {
case dto.JobStatusRunning:
return s.HandleJobFailure(ctx, jobID, "lease_expired")
case dto.JobStatusPending, dto.JobStatusSuccess, dto.JobStatusFailure, dto.JobStatusCancelled:
return s.removeFromQueue(ctx, jobID)
default:
return s.removeFromQueue(ctx, jobID)
}
}
func (s *JobService) RenewJobLease(ctx context.Context, jobID string) error {
type touchable interface {
TouchInflight(ctx context.Context, jobID string, ttl time.Duration) error
}
queue, ok := s.queue.(touchable)
if !ok {
return nil
}
return queue.TouchInflight(ctx, jobID, 15*time.Minute)
}
func (s *JobService) HandleDispatchFailure(ctx context.Context, jobID string, reason string, retryable bool) error {
job, err := s.jobRepository.GetByID(ctx, jobID)
if err != nil {
return err
}
if !retryable {
return s.moveJobToDLQ(ctx, job, reason)
}
return s.requeueOrDLQ(ctx, job, reason)
}
func (s *JobService) HandleJobFailure(ctx context.Context, jobID string, reason string) error {
job, err := s.jobRepository.GetByID(ctx, jobID)
if err != nil {
return err
}
return s.requeueOrDLQ(ctx, job, reason)
}
func (s *JobService) HandleAgentDisconnect(ctx context.Context, jobID string) error {
job, err := s.jobRepository.GetByID(ctx, jobID)
if err != nil {
return err
}
return s.requeueOrDLQ(ctx, job, "agent_unregistered")
}
func (s *JobService) requeueOrDLQ(ctx context.Context, job *model.Job, reason string) error {
if job == nil {
return nil
}
willRetry := s.canAutoRetry(job)
if s.logger != nil {
s.logger.Warn("evaluating retry vs dlq", "job_id", job.ID, "reason", reason, "retry_count", s.retryCount(job), "max_retries", s.maxRetries(job), "will_retry", willRetry)
}
if !willRetry {
return s.moveJobToDLQ(ctx, job, reason)
}
return s.requeueJob(ctx, job, true)
}
func (s *JobService) requeueJob(ctx context.Context, job *model.Job, incrementRetry bool) error {
if job == nil {
return nil
}
if s.queue == nil {
return errors.New("job queue is unavailable")
}
pending := string(dto.JobStatusPending)
cancelled := false
progress := 0.0
now := time.Now()
job.Status = &pending
job.Cancelled = &cancelled
job.Progress = &progress
job.AgentID = nil
job.UpdatedAt = &now
if incrementRetry {
job.RetryCount = int64Ptr(s.retryCount(job) + 1)
job.Logs = appendSystemLog(job.Logs, fmt.Sprintf("[SYSTEM] Auto-retry scheduled at %s", now.UTC().Format(time.RFC3339)))
}
if err := s.jobRepository.Save(ctx, job); err != nil {
return err
}
cfg := parseJobConfig(job.Config)
if err := syncVideoStatus(ctx, s.jobRepository, cfg.VideoID, dto.JobStatusPending); err != nil {
return err
}
if err := s.queue.Enqueue(ctx, job); err != nil {
return err
}
if s.logger != nil {
s.logger.Info("requeued job", "job_id", job.ID, "retry_count", s.retryCount(job), "max_retries", s.maxRetries(job), "action", "requeue")
}
return s.publishJobUpdate(ctx, job.ID, pending, cfg.VideoID)
}
func (s *JobService) moveJobToDLQ(ctx context.Context, job *model.Job, reason string) error {
if job == nil {
return nil
}
failure := string(dto.JobStatusFailure)
now := time.Now()
job.Status = &failure
job.AgentID = nil
job.UpdatedAt = &now
job.Logs = appendSystemLog(job.Logs, fmt.Sprintf("[SYSTEM] Sent to DLQ: %s", strings.TrimSpace(reason)))
if err := s.jobRepository.Save(ctx, job); err != nil {
return err
}
cfg := parseJobConfig(job.Config)
if err := syncVideoStatus(ctx, s.jobRepository, cfg.VideoID, dto.JobStatusFailure); err != nil {
return err
}
if err := s.removeFromQueue(ctx, job.ID); err != nil {
return err
}
if s.dlq != nil {
if err := s.dlq.Add(ctx, job, strings.TrimSpace(reason)); err != nil {
return err
}
if s.logger != nil {
dlqCount, _ := s.dlq.Count(ctx)
s.logger.Warn("moved job to dlq", "job_id", job.ID, "reason", strings.TrimSpace(reason), "retry_count", s.retryCount(job), "max_retries", s.maxRetries(job), "dlq_size", dlqCount)
}
}
return s.publishJobUpdate(ctx, job.ID, failure, cfg.VideoID)
}
func (s *JobService) canDispatchJob(job *model.Job) bool {
if job == nil {
return false
}
if job.Cancelled != nil && *job.Cancelled {
return false
}
return s.jobStatus(job) == dto.JobStatusPending
}
func (s *JobService) canAutoRetry(job *model.Job) bool {
return s.retryCount(job) < s.maxRetries(job)
}
func (s *JobService) retryCount(job *model.Job) int64 {
if job == nil || job.RetryCount == nil {
return 0
}
return *job.RetryCount
}
func (s *JobService) maxRetries(job *model.Job) int64 {
if job == nil || job.MaxRetries == nil || *job.MaxRetries <= 0 {
return 3
}
return *job.MaxRetries
}
func (s *JobService) jobStatus(job *model.Job) dto.JobStatus {
if job == nil || job.Status == nil {
return dto.JobStatusPending
}
return dto.JobStatus(strings.TrimSpace(*job.Status))
}
func (s *JobService) publishJobUpdate(ctx context.Context, jobID string, status string, videoID string) error {
if s.pubsub == nil {
return nil
}
return s.pubsub.PublishJobUpdate(ctx, jobID, status, videoID)
}
func (s *JobService) publishLog(ctx context.Context, jobID string, logLine string, progress float64) error {
if s.pubsub == nil {
return nil
}
return s.pubsub.Publish(ctx, jobID, logLine, progress)
}
func (s *JobService) ListDLQ(ctx context.Context, offset, limit int) ([]*dto.DLQEntry, int64, error) {
if s.dlq == nil {
return []*dto.DLQEntry{}, 0, nil
}
if offset < 0 {
offset = 0
}
if limit <= 0 {
limit = 20
}
entries, err := s.dlq.List(ctx, int64(offset), int64(limit))
if err != nil {
return nil, 0, err
}
count, err := s.dlq.Count(ctx)
if err != nil {
return nil, 0, err
}
items := make([]*dto.DLQEntry, 0, len(entries))
for _, entry := range entries {
items = append(items, &dto.DLQEntry{Job: entry.Job, FailureTime: entry.FailureTime.Unix(), Reason: entry.Reason, RetryCount: entry.RetryCount})
}
return items, count, nil
}
func (s *JobService) GetDLQ(ctx context.Context, id string) (*dto.DLQEntry, error) {
if s.dlq == nil {
return nil, fmt.Errorf("job not found in DLQ")
}
entry, err := s.dlq.Get(ctx, strings.TrimSpace(id))
if err != nil {
return nil, err
}
return &dto.DLQEntry{Job: entry.Job, FailureTime: entry.FailureTime.Unix(), Reason: entry.Reason, RetryCount: entry.RetryCount}, nil
}
func (s *JobService) RetryDLQ(ctx context.Context, id string) (*model.Job, error) {
if s.dlq == nil {
return nil, fmt.Errorf("job not found in DLQ")
}
job, err := s.dlq.Retry(ctx, strings.TrimSpace(id))
if err != nil {
return nil, err
}
if job == nil {
return nil, fmt.Errorf("job not found in DLQ")
}
if err := s.requeueJob(ctx, job, false); err != nil {
return nil, err
}
if s.logger != nil {
count, _ := s.dlq.Count(ctx)
s.logger.Info("retried job from dlq", "job_id", job.ID, "dlq_size", count)
}
return s.jobRepository.GetByID(ctx, job.ID)
}
func (s *JobService) RemoveDLQ(ctx context.Context, id string) error {
if s.dlq == nil {
return fmt.Errorf("job not found in DLQ")
}
jobID := strings.TrimSpace(id)
if err := s.dlq.Remove(ctx, jobID); err != nil {
return err
}
if s.logger != nil {
count, _ := s.dlq.Count(ctx)
s.logger.Info("removed job from dlq", "job_id", jobID, "dlq_size", count)
}
return nil
}
func (s *JobService) removeFromQueue(ctx context.Context, jobID string) error {
type ackable interface {
Ack(ctx context.Context, jobID string) error
}
if queue, ok := s.queue.(ackable); ok {
return queue.Ack(ctx, jobID)
}
return nil
}
func appendSystemLog(logs *string, line string) *string {
line = strings.TrimSpace(line)
if line == "" {
return logs
}
existing := ""
if logs != nil {
existing = *logs
}
if existing != "" && !strings.HasSuffix(existing, "\n") {
existing += "\n"
}
existing += line + "\n"
return &existing
}

View File

@@ -7,12 +7,12 @@ import (
"time"
"github.com/google/uuid"
_ "modernc.org/sqlite"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
_ "modernc.org/sqlite"
appv1 "stream.api/internal/api/proto/app/v1"
"stream.api/internal/database/model"
"stream.api/internal/database/query"

View File

@@ -12,6 +12,7 @@ import (
"google.golang.org/grpc/status"
"gorm.io/gorm"
appv1 "stream.api/internal/api/proto/app/v1"
"stream.api/internal/database/model"
)
func (s *videosAppService) GetUploadUrl(ctx context.Context, req *appv1.GetUploadUrlRequest) (*appv1.GetUploadUrlResponse, error) {
@@ -151,6 +152,94 @@ func (s *videosAppService) GetVideo(ctx context.Context, req *appv1.GetVideoRequ
}
return &appv1.GetVideoResponse{Video: payload}, nil
}
func (s *videosAppService) GetVideoMetadata(ctx context.Context, req *appv1.GetVideoMetadataRequest) (*appv1.GetVideoMetadataResponse, error) {
if _, err := s.authenticator.RequireTrustedMetadata(ctx); err != nil {
return nil, err
}
videoID := strings.TrimSpace(req.GetVideoId())
if videoID == "" {
return nil, status.Error(codes.NotFound, "Video not found")
}
if s.videoRepository == nil {
return nil, status.Error(codes.Internal, "Video repository is unavailable")
}
video, err := s.videoRepository.GetByID(ctx, videoID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, status.Error(codes.NotFound, "Video not found")
}
s.logger.Error("Failed to fetch video metadata source video", "error", err, "video_id", videoID)
return nil, status.Error(codes.Internal, "Failed to fetch video metadata")
}
videoPayload, err := s.buildVideo(ctx, video)
if err != nil {
s.logger.Error("Failed to build video metadata video payload", "error", err, "video_id", video.ID)
return nil, status.Error(codes.Internal, "Failed to fetch video metadata")
}
ownerID := strings.TrimSpace(video.UserID)
ownerUser, err := s.userRepository.GetByID(ctx, ownerID)
if err != nil {
s.logger.Error("Failed to load video owner for metadata", "error", err, "video_id", video.ID)
return nil, status.Error(codes.Internal, "Failed to fetch video metadata")
}
configOwnerID := ownerID
if ownerUser.PlanID == nil || strings.TrimSpace(*ownerUser.PlanID) == "" {
configOwnerID, err = s.resolveSystemConfigOwnerID(ctx)
if err != nil {
s.logger.Error("Failed to resolve system config owner", "error", err, "video_id", video.ID)
return nil, status.Error(codes.Internal, "Failed to fetch video metadata")
}
}
domains, err := s.domainRepository.ListByUser(ctx, ownerID)
if err != nil {
s.logger.Error("Failed to load video metadata domains", "error", err, "video_id", video.ID)
return nil, status.Error(codes.Internal, "Failed to fetch video metadata")
}
protoDomains := make([]*appv1.Domain, 0, len(domains))
for i := range domains {
item := domains[i]
protoDomains = append(protoDomains, toProtoDomain(&item))
}
playerConfig, err := s.resolveDefaultPlayerConfig(ctx, configOwnerID)
if err != nil {
s.logger.Error("Failed to load default player config for video metadata", "error", err, "video_id", video.ID)
return nil, status.Error(codes.Internal, "Failed to fetch video metadata")
}
if playerConfig == nil {
return nil, status.Error(codes.FailedPrecondition, "Default player config is required")
}
popupAd, err := s.resolveActivePopupAd(ctx, configOwnerID)
if err != nil {
s.logger.Error("Failed to load popup ad for video metadata", "error", err, "video_id", video.ID)
return nil, status.Error(codes.Internal, "Failed to fetch video metadata")
}
if popupAd == nil {
return nil, status.Error(codes.FailedPrecondition, "Active popup ad is required")
}
adTemplate, err := s.resolveEffectiveAdTemplate(ctx, video, ownerID, configOwnerID)
if err != nil {
s.logger.Error("Failed to load ad template for video metadata", "error", err, "video_id", video.ID)
return nil, status.Error(codes.Internal, "Failed to fetch video metadata")
}
if adTemplate == nil {
return nil, status.Error(codes.FailedPrecondition, "Ad template is required")
}
if len(protoDomains) == 0 {
return nil, status.Error(codes.FailedPrecondition, "At least one domain is required")
}
return &appv1.GetVideoMetadataResponse{
Video: videoPayload,
DefaultPlayerConfig: toProtoPlayerConfig(playerConfig),
AdTemplate: toProtoAdTemplate(adTemplate),
ActivePopupAd: toProtoPopupAd(popupAd),
Domains: protoDomains,
}, nil
}
func (s *videosAppService) UpdateVideo(ctx context.Context, req *appv1.UpdateVideoRequest) (*appv1.UpdateVideoResponse, error) {
result, err := s.authenticate(ctx)
if err != nil {
@@ -216,6 +305,59 @@ func (s *videosAppService) UpdateVideo(ctx context.Context, req *appv1.UpdateVid
}
return &appv1.UpdateVideoResponse{Video: payload}, nil
}
func (s *videosAppService) resolveSystemConfigOwnerID(ctx context.Context) (string, error) {
users, _, err := s.userRepository.ListForAdmin(ctx, "", "ADMIN", 1, 0)
if err != nil {
return "", err
}
if len(users) == 0 {
return "", fmt.Errorf("system config owner not found")
}
return users[0].ID, nil
}
func (s *videosAppService) resolveDefaultPlayerConfig(ctx context.Context, userID string) (*model.PlayerConfig, error) {
items, err := s.playerConfigRepo.ListByUser(ctx, userID)
if err != nil {
return nil, err
}
if len(items) == 0 {
return nil, nil
}
return &items[0], nil
}
func (s *videosAppService) resolveActivePopupAd(ctx context.Context, userID string) (*model.PopupAd, error) {
item, err := s.popupAdRepository.GetActiveByUser(ctx, userID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, err
}
return item, nil
}
func (s *videosAppService) resolveEffectiveAdTemplate(ctx context.Context, video *model.Video, ownerID string, configOwnerID string) (*model.AdTemplate, error) {
if video != nil && video.AdID != nil && strings.TrimSpace(*video.AdID) != "" {
item, err := s.adTemplateRepository.GetByIDAndUser(ctx, strings.TrimSpace(*video.AdID), ownerID)
if err == nil {
return item, nil
}
if !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, err
}
}
items, err := s.adTemplateRepository.ListByUser(ctx, configOwnerID)
if err != nil {
return nil, err
}
if len(items) == 0 {
return nil, nil
}
return &items[0], nil
}
func (s *videosAppService) DeleteVideo(ctx context.Context, req *appv1.DeleteVideoRequest) (*appv1.MessageResponse, error) {
result, err := s.authenticate(ctx)
if err != nil {

View File

@@ -7,7 +7,6 @@ import (
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
proto "stream.api/internal/api/proto/agent/v1"
"stream.api/internal/dto"
)
func (s *Server) RegisterAgent(ctx context.Context, req *proto.RegisterAgentRequest) (*proto.RegisterAgentResponse, error) {
@@ -39,7 +38,7 @@ func (s *Server) UnregisterAgent(ctx context.Context, _ *proto.Empty) (*proto.Em
return nil, status.Error(codes.Unauthenticated, "invalid session")
}
for _, jobID := range s.getAgentJobs(agentID) {
_ = s.jobService.UpdateJobStatus(ctx, jobID, dto.JobStatusFailure)
_ = s.jobService.HandleAgentDisconnect(ctx, jobID)
s.untrackJobAssignment(agentID, jobID)
}
s.sessions.Delete(token)

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) {
if s.agentSecret != "" && req.AgentToken != s.agentSecret {
return nil, status.Error(codes.Unauthenticated, "invalid agent secret")
return nil, status.Error(codes.Unauthenticated, "invalid internal marker")
}
agentID := req.AgentId
if len(agentID) > 6 && agentID[:6] == "agent-" {

View File

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

View File

@@ -53,12 +53,14 @@ func (s *Server) StreamJobs(_ *proto.StreamOptions, stream grpcpkg.ServerStreami
}
s.trackJobAssignment(agentID, job.ID)
if err := s.jobService.AssignJob(ctx, job.ID, agentID); err != nil {
_ = s.jobService.HandleDispatchFailure(ctx, job.ID, "assign_failed", true)
s.untrackJobAssignment(agentID, job.ID)
continue
}
var config map[string]any
if err := json.Unmarshal([]byte(*job.Config), &config); err != nil {
_ = s.jobService.UpdateJobStatus(ctx, job.ID, dto.JobStatusFailure)
if job.Config == nil || json.Unmarshal([]byte(*job.Config), &config) != nil {
_ = s.jobService.HandleDispatchFailure(ctx, job.ID, "invalid_config", false)
s.untrackJobAssignment(agentID, job.ID)
continue
}
@@ -80,7 +82,7 @@ func (s *Server) StreamJobs(_ *proto.StreamOptions, stream grpcpkg.ServerStreami
}
payload, _ := json.Marshal(map[string]any{"image": image, "commands": commands, "environment": map[string]string{}})
if err := stream.Send(&proto.Workflow{Id: job.ID, Timeout: 60 * 60 * 1000, Payload: payload}); err != nil {
_ = s.jobService.UpdateJobStatus(ctx, job.ID, dto.JobStatusPending)
_ = s.jobService.HandleDispatchFailure(ctx, job.ID, "stream_send_failed", true)
s.untrackJobAssignment(agentID, job.ID)
return err
}
@@ -101,8 +103,10 @@ func (s *Server) SubmitStatus(stream grpcpkg.ClientStreamingServer[proto.StatusU
}
switch update.Type {
case 0, 1:
_ = s.jobService.RenewJobLease(ctx, update.StepUuid)
_ = s.jobService.ProcessLog(ctx, update.StepUuid, update.Data)
case 4:
_ = s.jobService.RenewJobLease(ctx, update.StepUuid)
var progress float64
fmt.Sscanf(string(update.Data), "%f", &progress)
_ = s.jobService.UpdateJobProgress(ctx, update.StepUuid, progress)
@@ -126,6 +130,7 @@ func (s *Server) Init(ctx context.Context, req *proto.InitRequest) (*proto.Empty
if err := s.jobService.UpdateJobStatus(ctx, req.Id, dto.JobStatusRunning); err != nil {
return nil, status.Error(codes.Internal, "failed to update job status")
}
_ = s.jobService.RenewJobLease(ctx, req.Id)
return &proto.Empty{}, nil
}
@@ -138,11 +143,13 @@ func (s *Server) Done(ctx context.Context, req *proto.DoneRequest) (*proto.Empty
if !ok {
return nil, status.Error(codes.Unauthenticated, "invalid session")
}
jobStatus := dto.JobStatusSuccess
var err error
if req.State != nil && req.State.Error != "" {
jobStatus = dto.JobStatusFailure
err = s.jobService.HandleJobFailure(ctx, req.Id, req.State.Error)
} else {
err = s.jobService.UpdateJobStatus(ctx, req.Id, dto.JobStatusSuccess)
}
if err := s.jobService.UpdateJobStatus(ctx, req.Id, jobStatus); err != nil {
if err != nil {
return nil, status.Error(codes.Internal, "failed to update job status")
}
s.untrackJobAssignment(agentID, req.Id)
@@ -165,6 +172,12 @@ func (s *Server) Log(ctx context.Context, req *proto.LogRequest) (*proto.Empty,
return &proto.Empty{}, nil
}
func (s *Server) Extend(context.Context, *proto.ExtendRequest) (*proto.Empty, error) {
func (s *Server) Extend(ctx context.Context, req *proto.ExtendRequest) (*proto.Empty, error) {
if _, _, ok := s.getAgentIDFromContext(ctx); !ok {
return nil, status.Error(codes.Unauthenticated, "invalid session")
}
if req != nil && req.Id != "" {
_ = s.jobService.RenewJobLease(ctx, req.Id)
}
return &proto.Empty{}, nil
}

View File

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

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)
CancelJob(ctx context.Context, id string) error
RetryJob(ctx context.Context, id string) (*model.Job, error)
ListDLQ(ctx context.Context, offset, limit int) ([]*dto.DLQEntry, int64, error)
GetDLQ(ctx context.Context, id string) (*dto.DLQEntry, error)
RetryDLQ(ctx context.Context, id string) (*model.Job, error)
RemoveDLQ(ctx context.Context, id string) error
}
type Workflow struct {
@@ -187,6 +191,34 @@ func (w *Workflow) RetryJob(ctx context.Context, id string) (*model.Job, error)
return w.jobService.RetryJob(ctx, id)
}
func (w *Workflow) ListDLQ(ctx context.Context, offset, limit int) ([]*dto.DLQEntry, int64, error) {
if w == nil || w.jobService == nil {
return nil, 0, ErrJobServiceUnavailable
}
return w.jobService.ListDLQ(ctx, offset, limit)
}
func (w *Workflow) GetDLQ(ctx context.Context, id string) (*dto.DLQEntry, error) {
if w == nil || w.jobService == nil {
return nil, ErrJobServiceUnavailable
}
return w.jobService.GetDLQ(ctx, id)
}
func (w *Workflow) RetryDLQ(ctx context.Context, id string) (*model.Job, error) {
if w == nil || w.jobService == nil {
return nil, ErrJobServiceUnavailable
}
return w.jobService.RetryDLQ(ctx, id)
}
func (w *Workflow) RemoveDLQ(ctx context.Context, id string) error {
if w == nil || w.jobService == nil {
return ErrJobServiceUnavailable
}
return w.jobService.RemoveDLQ(ctx, id)
}
func buildJobPayload(videoID, userID, videoURL, format string) ([]byte, error) {
return json.Marshal(map[string]any{
"video_id": videoID,

View File

@@ -49,6 +49,10 @@ service Admin {
rpc CreateAdminJob(CreateAdminJobRequest) returns (CreateAdminJobResponse);
rpc CancelAdminJob(CancelAdminJobRequest) returns (CancelAdminJobResponse);
rpc RetryAdminJob(RetryAdminJobRequest) returns (RetryAdminJobResponse);
rpc ListAdminDlqJobs(ListAdminDlqJobsRequest) returns (ListAdminDlqJobsResponse);
rpc GetAdminDlqJob(GetAdminDlqJobRequest) returns (GetAdminDlqJobResponse);
rpc RetryAdminDlqJob(RetryAdminDlqJobRequest) returns (RetryAdminDlqJobResponse);
rpc RemoveAdminDlqJob(RemoveAdminDlqJobRequest) returns (RemoveAdminDlqJobResponse);
rpc ListAdminAgents(ListAdminAgentsRequest) returns (ListAdminAgentsResponse);
rpc RestartAdminAgent(RestartAdminAgentRequest) returns (AdminAgentCommandResponse);
rpc UpdateAdminAgent(UpdateAdminAgentRequest) returns (AdminAgentCommandResponse);
@@ -533,6 +537,43 @@ message RetryAdminJobResponse {
AdminJob job = 1;
}
message ListAdminDlqJobsRequest {
int32 offset = 1;
int32 limit = 2;
}
message ListAdminDlqJobsResponse {
repeated AdminDlqEntry items = 1;
int64 total = 2;
int32 offset = 3;
int32 limit = 4;
}
message GetAdminDlqJobRequest {
string id = 1;
}
message GetAdminDlqJobResponse {
AdminDlqEntry item = 1;
}
message RetryAdminDlqJobRequest {
string id = 1;
}
message RetryAdminDlqJobResponse {
AdminJob job = 1;
}
message RemoveAdminDlqJobRequest {
string id = 1;
}
message RemoveAdminDlqJobResponse {
string status = 1;
string job_id = 2;
}
message ListAdminAgentsRequest {}
message ListAdminAgentsResponse {

View File

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

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