update cicd
This commit is contained in:
@@ -3,7 +3,9 @@ package redis
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
goredis "github.com/redis/go-redis/v9"
|
||||
@@ -12,14 +14,24 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
JobQueueKey = "render:jobs:queue"
|
||||
LogChannel = "render:jobs:logs"
|
||||
ResourceChannel = "render:agents:resources"
|
||||
JobUpdateChannel = "render:jobs:updates"
|
||||
JobQueueKey = "render:jobs:queue:v2"
|
||||
JobInflightKey = "render:jobs:inflight"
|
||||
JobInflightMetaKey = "render:jobs:inflight:meta"
|
||||
JobSequenceKey = "render:jobs:queue:seq"
|
||||
LogChannel = "render:jobs:logs"
|
||||
ResourceChannel = "render:agents:resources"
|
||||
JobUpdateChannel = "render:jobs:updates"
|
||||
defaultQueuePoll = time.Second
|
||||
defaultInflightTTL = 15 * time.Minute
|
||||
)
|
||||
|
||||
type RedisAdapter struct{ client *goredis.Client }
|
||||
|
||||
type inflightMeta struct {
|
||||
ReadyScore float64 `json:"ready_score"`
|
||||
ClaimedAt int64 `json:"claimed_at"`
|
||||
}
|
||||
|
||||
func NewAdapter(addr, password string, db int) (*RedisAdapter, error) {
|
||||
client := goredis.NewClient(&goredis.Options{Addr: addr, Password: password, DB: db})
|
||||
if err := client.Ping(context.Background()).Err(); err != nil {
|
||||
@@ -31,13 +43,26 @@ func NewAdapter(addr, password string, db int) (*RedisAdapter, error) {
|
||||
func (r *RedisAdapter) Client() *goredis.Client { return r.client }
|
||||
|
||||
func (r *RedisAdapter) Enqueue(ctx context.Context, job *model.Job) error {
|
||||
data, err := json.Marshal(job)
|
||||
if job == nil || strings.TrimSpace(job.ID) == "" {
|
||||
return errors.New("job id is required")
|
||||
}
|
||||
priority := int64(0)
|
||||
if job.Priority != nil {
|
||||
priority = *job.Priority
|
||||
}
|
||||
seq, err := r.client.Incr(ctx, JobSequenceKey).Result()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
timestamp := time.Now().UnixNano()
|
||||
score := float64(-(int64(*job.Priority) * 1000000000) - timestamp)
|
||||
return r.client.ZAdd(ctx, JobQueueKey, goredis.Z{Score: score, Member: data}).Err()
|
||||
score := float64((-priority * 1_000_000_000_000) + seq)
|
||||
jobID := strings.TrimSpace(job.ID)
|
||||
if err := r.client.HDel(ctx, JobInflightMetaKey, jobID).Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := r.client.ZRem(ctx, JobInflightKey, jobID).Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
return r.client.ZAdd(ctx, JobQueueKey, goredis.Z{Score: score, Member: jobID}).Err()
|
||||
}
|
||||
|
||||
func (r *RedisAdapter) Dequeue(ctx context.Context) (*model.Job, error) {
|
||||
@@ -56,27 +81,57 @@ func (r *RedisAdapter) Dequeue(ctx context.Context) (*model.Job, error) {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
case <-time.After(time.Second):
|
||||
case <-time.After(defaultQueuePoll):
|
||||
continue
|
||||
}
|
||||
}
|
||||
var raw []byte
|
||||
switch member := res[0].Member.(type) {
|
||||
case string:
|
||||
raw = []byte(member)
|
||||
case []byte:
|
||||
raw = member
|
||||
default:
|
||||
return nil, fmt.Errorf("unexpected redis queue payload type %T", member)
|
||||
}
|
||||
var job model.Job
|
||||
if err := json.Unmarshal(raw, &job); err != nil {
|
||||
jobID := fmt.Sprintf("%v", res[0].Member)
|
||||
meta, err := json.Marshal(inflightMeta{ReadyScore: res[0].Score, ClaimedAt: time.Now().Unix()})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &job, nil
|
||||
leaseScore := float64(time.Now().Add(defaultInflightTTL).Unix())
|
||||
pipe := r.client.TxPipeline()
|
||||
pipe.ZAdd(ctx, JobInflightKey, goredis.Z{Score: leaseScore, Member: jobID})
|
||||
pipe.HSet(ctx, JobInflightMetaKey, jobID, meta)
|
||||
if _, err := pipe.Exec(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &model.Job{ID: jobID}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (r *RedisAdapter) Ack(ctx context.Context, jobID string) error {
|
||||
jobID = strings.TrimSpace(jobID)
|
||||
if jobID == "" {
|
||||
return nil
|
||||
}
|
||||
pipe := r.client.TxPipeline()
|
||||
pipe.ZRem(ctx, JobQueueKey, jobID)
|
||||
pipe.ZRem(ctx, JobInflightKey, jobID)
|
||||
pipe.HDel(ctx, JobInflightMetaKey, jobID)
|
||||
_, err := pipe.Exec(ctx)
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *RedisAdapter) ListExpiredInflight(ctx context.Context, now time.Time, limit int64) ([]string, error) {
|
||||
if limit <= 0 {
|
||||
limit = 100
|
||||
}
|
||||
return r.client.ZRangeByScore(ctx, JobInflightKey, &goredis.ZRangeBy{Min: "-inf", Max: fmt.Sprintf("%d", now.Unix()), Offset: 0, Count: limit}).Result()
|
||||
}
|
||||
|
||||
func (r *RedisAdapter) TouchInflight(ctx context.Context, jobID string, ttl time.Duration) error {
|
||||
jobID = strings.TrimSpace(jobID)
|
||||
if jobID == "" {
|
||||
return nil
|
||||
}
|
||||
if ttl <= 0 {
|
||||
ttl = defaultInflightTTL
|
||||
}
|
||||
return r.client.ZAddXX(ctx, JobInflightKey, goredis.Z{Score: float64(time.Now().Add(ttl).Unix()), Member: jobID}).Err()
|
||||
}
|
||||
|
||||
func (r *RedisAdapter) Publish(ctx context.Context, jobID string, logLine string, progress float64) error {
|
||||
payload, err := json.Marshal(dto.LogEntry{JobID: jobID, Line: logLine, Progress: progress})
|
||||
if err != nil {
|
||||
@@ -173,6 +228,7 @@ func (r *RedisAdapter) SubscribeJobUpdates(ctx context.Context) (<-chan string,
|
||||
}()
|
||||
return ch, nil
|
||||
}
|
||||
|
||||
func (c *RedisAdapter) Set(ctx context.Context, key string, value interface{}, expiration time.Duration) error {
|
||||
return c.client.Set(ctx, key, value, expiration).Err()
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
221
internal/api/proto/app/v1/video_metadata.pb.go
Normal file
221
internal/api/proto/app/v1/video_metadata.pb.go
Normal 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
|
||||
}
|
||||
121
internal/api/proto/app/v1/video_metadata_grpc.pb.go
Normal file
121
internal/api/proto/app/v1/video_metadata_grpc.pb.go
Normal 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",
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
245
internal/service/__test__/service_video_metadata_test.go
Normal file
245
internal/service/__test__/service_video_metadata_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -65,31 +65,61 @@ func buildAdminJob(job *model.Job) *appv1.AdminJob {
|
||||
if job == nil {
|
||||
return nil
|
||||
}
|
||||
agentID := strconv.FormatInt(*job.AgentID, 10)
|
||||
var agentID *string
|
||||
if job.AgentID != nil {
|
||||
value := strconv.FormatInt(*job.AgentID, 10)
|
||||
agentID = &value
|
||||
}
|
||||
return &appv1.AdminJob{
|
||||
Id: job.ID,
|
||||
Status: string(*job.Status),
|
||||
Priority: int32(*job.Priority),
|
||||
UserId: *job.UserID,
|
||||
Status: stringValue(job.Status),
|
||||
Priority: int32(int64Value(job.Priority)),
|
||||
UserId: stringValue(job.UserID),
|
||||
Name: job.ID,
|
||||
TimeLimit: *job.TimeLimit,
|
||||
InputUrl: *job.InputURL,
|
||||
OutputUrl: *job.OutputURL,
|
||||
TotalDuration: *job.TotalDuration,
|
||||
CurrentTime: *job.CurrentTime,
|
||||
Progress: *job.Progress,
|
||||
AgentId: &agentID,
|
||||
Logs: *job.Logs,
|
||||
Config: *job.Config,
|
||||
Cancelled: *job.Cancelled,
|
||||
RetryCount: int32(*job.RetryCount),
|
||||
MaxRetries: int32(*job.MaxRetries),
|
||||
CreatedAt: timestamppb.New(*job.CreatedAt),
|
||||
UpdatedAt: timestamppb.New(*job.UpdatedAt),
|
||||
VideoId: stringPointerOrNil(*job.VideoID),
|
||||
TimeLimit: int64Value(job.TimeLimit),
|
||||
InputUrl: stringValue(job.InputURL),
|
||||
OutputUrl: stringValue(job.OutputURL),
|
||||
TotalDuration: int64Value(job.TotalDuration),
|
||||
CurrentTime: int64Value(job.CurrentTime),
|
||||
Progress: float64Value(job.Progress),
|
||||
AgentId: agentID,
|
||||
Logs: stringValue(job.Logs),
|
||||
Config: stringValue(job.Config),
|
||||
Cancelled: boolValue(job.Cancelled),
|
||||
RetryCount: int32(int64Value(job.RetryCount)),
|
||||
MaxRetries: int32(int64Value(job.MaxRetries)),
|
||||
CreatedAt: timeToProto(job.CreatedAt),
|
||||
UpdatedAt: timeToProto(job.UpdatedAt),
|
||||
VideoId: job.VideoID,
|
||||
}
|
||||
}
|
||||
|
||||
func buildAdminDlqEntry(entry *dto.DLQEntry) *appv1.AdminDlqEntry {
|
||||
if entry == nil {
|
||||
return nil
|
||||
}
|
||||
return &appv1.AdminDlqEntry{
|
||||
Job: buildAdminJob(entry.Job),
|
||||
FailureTime: timestamppb.New(time.Unix(entry.FailureTime, 0).UTC()),
|
||||
Reason: entry.Reason,
|
||||
RetryCount: int32(entry.RetryCount),
|
||||
}
|
||||
}
|
||||
|
||||
func int64Value(value *int64) int64 {
|
||||
if value == nil {
|
||||
return 0
|
||||
}
|
||||
return *value
|
||||
}
|
||||
|
||||
func float64Value(value *float64) float64 {
|
||||
if value == nil {
|
||||
return 0
|
||||
}
|
||||
return *value
|
||||
}
|
||||
|
||||
func buildAdminAgent(agent *dto.AgentWithStats) *appv1.AdminAgent {
|
||||
if agent == nil || agent.Agent == nil {
|
||||
return nil
|
||||
@@ -526,15 +556,20 @@ func (s *appServices) buildAdminPopupAd(ctx context.Context, item *model.PopupAd
|
||||
}
|
||||
|
||||
payload := &appv1.AdminPopupAd{
|
||||
Id: item.ID,
|
||||
UserId: item.UserID,
|
||||
Type: item.Type,
|
||||
Label: item.Label,
|
||||
Value: item.Value,
|
||||
IsActive: boolValue(item.IsActive),
|
||||
MaxTriggersPerSession: func() int32 { if item.MaxTriggersPerSession != nil { return *item.MaxTriggersPerSession }; return 0 }(),
|
||||
CreatedAt: timeToProto(item.CreatedAt),
|
||||
UpdatedAt: timeToProto(item.UpdatedAt),
|
||||
Id: item.ID,
|
||||
UserId: item.UserID,
|
||||
Type: item.Type,
|
||||
Label: item.Label,
|
||||
Value: item.Value,
|
||||
IsActive: boolValue(item.IsActive),
|
||||
MaxTriggersPerSession: func() int32 {
|
||||
if item.MaxTriggersPerSession != nil {
|
||||
return *item.MaxTriggersPerSession
|
||||
}
|
||||
return 0
|
||||
}(),
|
||||
CreatedAt: timeToProto(item.CreatedAt),
|
||||
UpdatedAt: timeToProto(item.UpdatedAt),
|
||||
}
|
||||
|
||||
ownerEmail, err := s.loadAdminUserEmail(ctx, item.UserID)
|
||||
|
||||
@@ -40,14 +40,19 @@ func toProtoPopupAd(item *model.PopupAd) *appv1.PopupAd {
|
||||
return nil
|
||||
}
|
||||
return &appv1.PopupAd{
|
||||
Id: item.ID,
|
||||
Type: item.Type,
|
||||
Label: item.Label,
|
||||
Value: item.Value,
|
||||
IsActive: boolValue(item.IsActive),
|
||||
MaxTriggersPerSession: func() int32 { if item.MaxTriggersPerSession != nil { return *item.MaxTriggersPerSession }; return 0 }(),
|
||||
CreatedAt: timeToProto(item.CreatedAt),
|
||||
UpdatedAt: timeToProto(item.UpdatedAt),
|
||||
Id: item.ID,
|
||||
Type: item.Type,
|
||||
Label: item.Label,
|
||||
Value: item.Value,
|
||||
IsActive: boolValue(item.IsActive),
|
||||
MaxTriggersPerSession: func() int32 {
|
||||
if item.MaxTriggersPerSession != nil {
|
||||
return *item.MaxTriggersPerSession
|
||||
}
|
||||
return 0
|
||||
}(),
|
||||
CreatedAt: timeToProto(item.CreatedAt),
|
||||
UpdatedAt: timeToProto(item.UpdatedAt),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -8,9 +8,9 @@ import (
|
||||
|
||||
"github.com/google/uuid"
|
||||
"google.golang.org/grpc/metadata"
|
||||
_ "modernc.org/sqlite"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
_ "modernc.org/sqlite"
|
||||
appv1 "stream.api/internal/api/proto/app/v1"
|
||||
"stream.api/internal/database/model"
|
||||
"stream.api/internal/database/query"
|
||||
@@ -89,8 +89,8 @@ func notificationTestContext(userID, role string) context.Context {
|
||||
))
|
||||
}
|
||||
|
||||
func notificationPtrString(v string) *string { return &v }
|
||||
func notificationPtrBool(v bool) *bool { return &v }
|
||||
func notificationPtrString(v string) *string { return &v }
|
||||
func notificationPtrBool(v bool) *bool { return &v }
|
||||
func notificationPtrFloat64(v float64) *float64 { return &v }
|
||||
|
||||
func seedNotificationUser(t *testing.T, db *gorm.DB, user model.User) model.User {
|
||||
|
||||
@@ -65,12 +65,7 @@ func (s *appServices) buildReferralShareLink(username *string) *string {
|
||||
return nil
|
||||
}
|
||||
path := "/ref/" + url.PathEscape(trimmed)
|
||||
base := strings.TrimRight(strings.TrimSpace(s.frontendBaseURL), "/")
|
||||
if base == "" {
|
||||
return &path
|
||||
}
|
||||
link := base + path
|
||||
return &link
|
||||
return &path
|
||||
}
|
||||
|
||||
func (s *appServices) loadReferralUsersByUsername(ctx context.Context, username string) ([]model.User, error) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -7,12 +7,12 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
_ "modernc.org/sqlite"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/metadata"
|
||||
"google.golang.org/grpc/status"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
_ "modernc.org/sqlite"
|
||||
appv1 "stream.api/internal/api/proto/app/v1"
|
||||
"stream.api/internal/database/model"
|
||||
"stream.api/internal/database/query"
|
||||
@@ -152,9 +152,9 @@ func popupTestContext(userID, role string) context.Context {
|
||||
))
|
||||
}
|
||||
|
||||
func popupPtrString(v string) *string { return &v }
|
||||
func popupPtrBool(v bool) *bool { return &v }
|
||||
func popupPtrInt32(v int32) *int32 { return &v }
|
||||
func popupPtrString(v string) *string { return &v }
|
||||
func popupPtrBool(v bool) *bool { return &v }
|
||||
func popupPtrInt32(v int32) *int32 { return &v }
|
||||
func popupPtrTime(v time.Time) *time.Time { return &v }
|
||||
|
||||
func popupSeedUser(t *testing.T, db *gorm.DB, user model.User) model.User {
|
||||
@@ -209,10 +209,10 @@ func TestPopupAdsUserFlow(t *testing.T) {
|
||||
user := popupSeedUser(t, db, model.User{ID: uuid.NewString(), Email: "user@example.com", Role: popupPtrString("USER")})
|
||||
|
||||
createResp, err := (&popupAdsAppService{appServices: services}).CreatePopupAd(popupTestContext(user.ID, "USER"), &appv1.CreatePopupAdRequest{
|
||||
Type: "url",
|
||||
Label: "Homepage Campaign",
|
||||
Value: "https://example.com/landing",
|
||||
IsActive: popupPtrBool(true),
|
||||
Type: "url",
|
||||
Label: "Homepage Campaign",
|
||||
Value: "https://example.com/landing",
|
||||
IsActive: popupPtrBool(true),
|
||||
MaxTriggersPerSession: popupPtrInt32(5),
|
||||
})
|
||||
if err != nil {
|
||||
@@ -231,11 +231,11 @@ func TestPopupAdsUserFlow(t *testing.T) {
|
||||
}
|
||||
|
||||
updateResp, err := (&popupAdsAppService{appServices: services}).UpdatePopupAd(popupTestContext(user.ID, "USER"), &appv1.UpdatePopupAdRequest{
|
||||
Id: createResp.Item.Id,
|
||||
Type: "script",
|
||||
Label: "Homepage Campaign v2",
|
||||
Value: `<script async src="//example.com/ad.js"></script>`,
|
||||
IsActive: popupPtrBool(false),
|
||||
Id: createResp.Item.Id,
|
||||
Type: "script",
|
||||
Label: "Homepage Campaign v2",
|
||||
Value: `<script async src="//example.com/ad.js"></script>`,
|
||||
IsActive: popupPtrBool(false),
|
||||
MaxTriggersPerSession: popupPtrInt32(8),
|
||||
})
|
||||
if err != nil {
|
||||
@@ -302,11 +302,11 @@ func TestPopupAdsAdminFlow(t *testing.T) {
|
||||
user := popupSeedUser(t, db, model.User{ID: uuid.NewString(), Email: "user@example.com", Role: popupPtrString("USER")})
|
||||
|
||||
createResp, err := services.CreateAdminPopupAd(popupTestContext(admin.ID, "ADMIN"), &appv1.CreateAdminPopupAdRequest{
|
||||
UserId: user.ID,
|
||||
Type: "url",
|
||||
Label: "Admin Campaign",
|
||||
Value: "https://example.com/admin",
|
||||
IsActive: popupPtrBool(true),
|
||||
UserId: user.ID,
|
||||
Type: "url",
|
||||
Label: "Admin Campaign",
|
||||
Value: "https://example.com/admin",
|
||||
IsActive: popupPtrBool(true),
|
||||
MaxTriggersPerSession: popupPtrInt32(7),
|
||||
})
|
||||
if err != nil {
|
||||
@@ -325,12 +325,12 @@ func TestPopupAdsAdminFlow(t *testing.T) {
|
||||
}
|
||||
|
||||
updateResp, err := services.UpdateAdminPopupAd(popupTestContext(admin.ID, "ADMIN"), &appv1.UpdateAdminPopupAdRequest{
|
||||
Id: createResp.Item.Id,
|
||||
UserId: user.ID,
|
||||
Type: "script",
|
||||
Label: "Admin Campaign v2",
|
||||
Value: `<script async src="//example.com/admin-v2.js"></script>`,
|
||||
IsActive: popupPtrBool(false),
|
||||
Id: createResp.Item.Id,
|
||||
UserId: user.ID,
|
||||
Type: "script",
|
||||
Label: "Admin Campaign v2",
|
||||
Value: `<script async src="//example.com/admin-v2.js"></script>`,
|
||||
IsActive: popupPtrBool(false),
|
||||
MaxTriggersPerSession: popupPtrInt32(11),
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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-" {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
576
internal/workflow/agent/agent.go
Normal file
576
internal/workflow/agent/agent.go
Normal 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)
|
||||
}
|
||||
154
internal/workflow/agent/docker.go
Normal file
154
internal/workflow/agent/docker.go
Normal 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
|
||||
}
|
||||
26
internal/workflow/agent/parser.go
Normal file
26
internal/workflow/agent/parser.go
Normal 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
|
||||
}
|
||||
43
internal/workflow/agent/parser_test.go
Normal file
43
internal/workflow/agent/parser_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user