feat(desktop&tenant): Enhance desktop account and task consumer presence management
Desktop Client Build / Resolve Build Metadata (push) Successful in 1m13s
Desktop Client Build / Build Desktop Client (push) Has been cancelled
Desktop Client Build / Publish Client Artifacts to NAS (push) Has been cancelled
Backend CI / Backend (push) Failing after 13m49s
Frontend CI / Frontend (push) Successful in 6m38s
Desktop Client Build / Resolve Build Metadata (push) Successful in 1m13s
Desktop Client Build / Build Desktop Client (push) Has been cancelled
Desktop Client Build / Publish Client Artifacts to NAS (push) Has been cancelled
Backend CI / Backend (push) Failing after 13m49s
Frontend CI / Frontend (push) Successful in 6m38s
- Added new fields to DesktopAccountInfo for tracking account session and task consumer online status, along with queued publish task count and oldest queued publish task timestamp. - Updated DesktopDispatchEvent to include server time for better synchronization. - Implemented separate presence management for task consumers, including marking presence and loading presence state. - Enhanced DesktopAccountService to apply publish queue summaries and manage task consumer presence. - Introduced new methods for replaying queued publish tasks in DesktopDispatchHandler. - Updated tests to cover new presence management logic and ensure correct behavior of account session and task consumer online status.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package transport
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"time"
|
||||
@@ -11,6 +12,8 @@ import (
|
||||
|
||||
"github.com/geo-platform/tenant-api/internal/bootstrap"
|
||||
"github.com/geo-platform/tenant-api/internal/shared/stream"
|
||||
tenantapp "github.com/geo-platform/tenant-api/internal/tenant/app"
|
||||
"github.com/geo-platform/tenant-api/internal/tenant/repository"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -19,6 +22,7 @@ const (
|
||||
dispatchPingPeriod = 25 * time.Second
|
||||
dispatchReadLimitBytes = 2048
|
||||
dispatchCloseGracePause = 500 * time.Millisecond
|
||||
dispatchReplayLimit = 20
|
||||
)
|
||||
|
||||
type DesktopDispatchHandler struct {
|
||||
@@ -66,6 +70,7 @@ func (h *DesktopDispatchHandler) Dispatch(c *gin.Context) {
|
||||
|
||||
subscriber, cancel := hub.Subscribe(client.ID.String())
|
||||
defer cancel()
|
||||
tenantapp.MarkDesktopTaskConsumerPresent(c.Request.Context(), h.app.Redis, client.ID)
|
||||
|
||||
conn.SetReadLimit(dispatchReadLimitBytes)
|
||||
_ = conn.SetReadDeadline(time.Now().Add(dispatchPongWait))
|
||||
@@ -86,16 +91,19 @@ func (h *DesktopDispatchHandler) Dispatch(c *gin.Context) {
|
||||
}
|
||||
}()
|
||||
|
||||
serverTime := time.Now().UTC()
|
||||
connected := stream.DesktopDispatchEvent{
|
||||
Type: "connected",
|
||||
TargetClientID: client.ID.String(),
|
||||
WorkspaceID: client.WorkspaceID,
|
||||
UpdatedAt: time.Now().UTC(),
|
||||
UpdatedAt: serverTime,
|
||||
ServerTime: &serverTime,
|
||||
}
|
||||
if err := writeDispatchFrame(conn, connected); err != nil {
|
||||
_ = conn.Close()
|
||||
return
|
||||
}
|
||||
h.replayQueuedPublishTasks(c.Request.Context(), client, conn)
|
||||
|
||||
pingTicker := time.NewTicker(dispatchPingPeriod)
|
||||
defer pingTicker.Stop()
|
||||
@@ -139,3 +147,154 @@ func writeDispatchFrame(conn *websocket.Conn, event stream.DesktopDispatchEvent)
|
||||
_ = conn.SetWriteDeadline(time.Now().Add(dispatchWriteWait))
|
||||
return conn.WriteMessage(websocket.TextMessage, body)
|
||||
}
|
||||
|
||||
func (h *DesktopDispatchHandler) replayQueuedPublishTasks(ctx context.Context, client *repository.DesktopClient, conn *websocket.Conn) {
|
||||
if h == nil || h.app == nil || h.app.DB == nil || client == nil || conn == nil {
|
||||
return
|
||||
}
|
||||
|
||||
rows, err := h.app.DB.Query(ctx, `
|
||||
SELECT
|
||||
dt.desktop_id,
|
||||
dt.job_id,
|
||||
dt.tenant_id,
|
||||
dt.workspace_id,
|
||||
dt.target_account_id,
|
||||
dt.target_client_id,
|
||||
dt.platform_id,
|
||||
dt.kind,
|
||||
dt.payload,
|
||||
dt.status,
|
||||
dt.priority,
|
||||
dt.lane,
|
||||
dt.lane_weight,
|
||||
dt.source,
|
||||
dt.scheduler_group_key,
|
||||
dt.monitor_task_id,
|
||||
dt.supersedes_task_id,
|
||||
dt.control_flags,
|
||||
dt.interrupt_generation,
|
||||
dt.dedup_key,
|
||||
dt.active_attempt_id,
|
||||
dt.lease_expires_at,
|
||||
dt.attempts,
|
||||
dt.result,
|
||||
dt.error,
|
||||
dt.started_at,
|
||||
dt.interrupted_at,
|
||||
dt.interrupt_reason,
|
||||
dt.enqueued_at,
|
||||
dt.created_at,
|
||||
dt.updated_at
|
||||
FROM desktop_tasks AS dt
|
||||
WHERE dt.tenant_id = $1
|
||||
AND dt.workspace_id = $2
|
||||
AND dt.target_client_id = $3
|
||||
AND dt.kind = 'publish'
|
||||
AND dt.status = 'queued'
|
||||
AND dt.attempts < 3
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM desktop_publish_jobs AS j
|
||||
WHERE j.desktop_id = dt.job_id
|
||||
AND j.status <> 'queued'
|
||||
)
|
||||
ORDER BY dt.priority DESC,
|
||||
COALESCE(dt.enqueued_at, dt.created_at) ASC,
|
||||
dt.created_at ASC,
|
||||
dt.desktop_id ASC
|
||||
LIMIT $4
|
||||
`, client.TenantID, client.WorkspaceID, client.ID, dispatchReplayLimit)
|
||||
if err != nil {
|
||||
if h.app.Logger != nil {
|
||||
h.app.Logger.Warn("desktop dispatch replay query failed",
|
||||
zap.String("client_id", client.ID.String()),
|
||||
zap.Error(err),
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
replayed := 0
|
||||
for rows.Next() {
|
||||
task, scanErr := scanDispatchReplayDesktopTask(rows)
|
||||
if scanErr != nil {
|
||||
if h.app.Logger != nil {
|
||||
h.app.Logger.Warn("desktop dispatch replay scan failed",
|
||||
zap.String("client_id", client.ID.String()),
|
||||
zap.Error(scanErr),
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err := writeDispatchFrame(conn, tenantapp.DesktopDispatchEventFromTask(task, "task_available")); err != nil {
|
||||
if h.app.Logger != nil {
|
||||
h.app.Logger.Warn("desktop dispatch replay write failed",
|
||||
zap.String("client_id", client.ID.String()),
|
||||
zap.String("task_id", task.DesktopID.String()),
|
||||
zap.Error(err),
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
replayed++
|
||||
}
|
||||
if err := rows.Err(); err != nil && h.app.Logger != nil {
|
||||
h.app.Logger.Warn("desktop dispatch replay rows failed",
|
||||
zap.String("client_id", client.ID.String()),
|
||||
zap.Error(err),
|
||||
)
|
||||
}
|
||||
if replayed > 0 && h.app.Logger != nil {
|
||||
h.app.Logger.Info("desktop dispatch replayed queued publish tasks",
|
||||
zap.String("client_id", client.ID.String()),
|
||||
zap.Int("count", replayed),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
type dispatchReplayDesktopTaskScanner interface {
|
||||
Scan(dest ...any) error
|
||||
}
|
||||
|
||||
func scanDispatchReplayDesktopTask(row dispatchReplayDesktopTaskScanner) (*repository.DesktopTask, error) {
|
||||
var task repository.DesktopTask
|
||||
err := row.Scan(
|
||||
&task.DesktopID,
|
||||
&task.JobID,
|
||||
&task.TenantID,
|
||||
&task.WorkspaceID,
|
||||
&task.TargetAccountID,
|
||||
&task.TargetClientID,
|
||||
&task.Platform,
|
||||
&task.Kind,
|
||||
&task.Payload,
|
||||
&task.Status,
|
||||
&task.Priority,
|
||||
&task.Lane,
|
||||
&task.LaneWeight,
|
||||
&task.Source,
|
||||
&task.SchedulerGroupKey,
|
||||
&task.MonitorTaskID,
|
||||
&task.SupersedesTaskID,
|
||||
&task.ControlFlags,
|
||||
&task.InterruptGeneration,
|
||||
&task.DedupKey,
|
||||
&task.ActiveAttemptID,
|
||||
&task.LeaseExpiresAt,
|
||||
&task.Attempts,
|
||||
&task.Result,
|
||||
&task.Error,
|
||||
&task.StartedAt,
|
||||
&task.InterruptedAt,
|
||||
&task.InterruptReason,
|
||||
&task.EnqueuedAt,
|
||||
&task.CreatedAt,
|
||||
&task.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &task, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user