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:
@@ -12,6 +12,7 @@ import (
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
goredis "github.com/redis/go-redis/v9"
|
||||
|
||||
"github.com/geo-platform/tenant-api/internal/shared/auth"
|
||||
@@ -23,6 +24,7 @@ import (
|
||||
const desktopAccountHealthReportTTL = 24 * time.Hour
|
||||
|
||||
type DesktopAccountService struct {
|
||||
pool *pgxpool.Pool
|
||||
repo repository.DesktopAccountRepository
|
||||
clientRepo repository.DesktopClientRepository
|
||||
redis *goredis.Client
|
||||
@@ -31,12 +33,14 @@ type DesktopAccountService struct {
|
||||
}
|
||||
|
||||
func NewDesktopAccountService(
|
||||
pool *pgxpool.Pool,
|
||||
repo repository.DesktopAccountRepository,
|
||||
clientRepo repository.DesktopClientRepository,
|
||||
redis *goredis.Client,
|
||||
rabbitMQClient *rabbitmq.Client,
|
||||
) *DesktopAccountService {
|
||||
return &DesktopAccountService{
|
||||
pool: pool,
|
||||
repo: repo,
|
||||
clientRepo: clientRepo,
|
||||
redis: redis,
|
||||
@@ -46,29 +50,33 @@ func NewDesktopAccountService(
|
||||
}
|
||||
|
||||
type DesktopAccountView struct {
|
||||
ID string `json:"id"`
|
||||
Platform string `json:"platform"`
|
||||
PlatformUID string `json:"platform_uid"`
|
||||
DisplayName string `json:"display_name"`
|
||||
AvatarURL *string `json:"avatar_url"`
|
||||
Health string `json:"health"`
|
||||
RuntimeHealth *string `json:"runtime_health"`
|
||||
RuntimeVerifiedAt *time.Time `json:"runtime_verified_at"`
|
||||
RuntimeCheckedAt *time.Time `json:"runtime_checked_at"`
|
||||
RuntimeAuthState *string `json:"runtime_auth_state"`
|
||||
RuntimeProbeState *string `json:"runtime_probe_state"`
|
||||
RuntimeAuthReason *string `json:"runtime_auth_reason"`
|
||||
HealthSource string `json:"health_source"`
|
||||
ClientID *string `json:"client_id"`
|
||||
AccountFingerprint *string `json:"account_fingerprint"`
|
||||
VerifiedAt *time.Time `json:"verified_at"`
|
||||
Tags []string `json:"tags"`
|
||||
SyncVersion int64 `json:"sync_version"`
|
||||
DeletedAt *time.Time `json:"deleted_at"`
|
||||
DeleteRequestedAt *time.Time `json:"delete_requested_at"`
|
||||
ClientOnline *bool `json:"client_online"`
|
||||
ClientLastSeenAt *time.Time `json:"client_last_seen_at"`
|
||||
ClientDeviceName *string `json:"client_device_name"`
|
||||
ID string `json:"id"`
|
||||
Platform string `json:"platform"`
|
||||
PlatformUID string `json:"platform_uid"`
|
||||
DisplayName string `json:"display_name"`
|
||||
AvatarURL *string `json:"avatar_url"`
|
||||
Health string `json:"health"`
|
||||
RuntimeHealth *string `json:"runtime_health"`
|
||||
RuntimeVerifiedAt *time.Time `json:"runtime_verified_at"`
|
||||
RuntimeCheckedAt *time.Time `json:"runtime_checked_at"`
|
||||
RuntimeAuthState *string `json:"runtime_auth_state"`
|
||||
RuntimeProbeState *string `json:"runtime_probe_state"`
|
||||
RuntimeAuthReason *string `json:"runtime_auth_reason"`
|
||||
HealthSource string `json:"health_source"`
|
||||
ClientID *string `json:"client_id"`
|
||||
AccountFingerprint *string `json:"account_fingerprint"`
|
||||
VerifiedAt *time.Time `json:"verified_at"`
|
||||
Tags []string `json:"tags"`
|
||||
SyncVersion int64 `json:"sync_version"`
|
||||
DeletedAt *time.Time `json:"deleted_at"`
|
||||
DeleteRequestedAt *time.Time `json:"delete_requested_at"`
|
||||
ClientOnline *bool `json:"client_online"`
|
||||
AccountSessionOnline *bool `json:"account_session_online"`
|
||||
TaskConsumerOnline *bool `json:"task_consumer_online"`
|
||||
ClientLastSeenAt *time.Time `json:"client_last_seen_at"`
|
||||
ClientDeviceName *string `json:"client_device_name"`
|
||||
QueuedPublishTaskCount int `json:"queued_publish_task_count"`
|
||||
OldestQueuedPublishTaskAt *time.Time `json:"oldest_queued_publish_task_at"`
|
||||
}
|
||||
|
||||
type UpsertDesktopAccountRequest struct {
|
||||
@@ -143,12 +151,13 @@ func (s *DesktopAccountService) ListByClient(ctx context.Context, client *reposi
|
||||
return nil, response.ErrInternal(50081, "desktop_accounts_query_failed", "failed to list desktop accounts")
|
||||
}
|
||||
|
||||
clientMap, presenceMap := s.loadClientState(ctx, client.WorkspaceID, rows)
|
||||
clientMap, accountPresenceMap, clientPresenceMap, taskConsumerPresenceMap := s.loadClientState(ctx, client.WorkspaceID, rows)
|
||||
items := make([]DesktopAccountView, 0, len(rows))
|
||||
for _, item := range rows {
|
||||
items = append(items, s.buildDesktopAccountView(&item, clientMap, presenceMap))
|
||||
items = append(items, s.buildDesktopAccountView(&item, clientMap, accountPresenceMap, clientPresenceMap, taskConsumerPresenceMap))
|
||||
}
|
||||
s.applyRuntimeHealth(ctx, client.WorkspaceID, items)
|
||||
s.applyPublishQueueSummaries(ctx, client.WorkspaceID, items)
|
||||
return items, nil
|
||||
}
|
||||
|
||||
@@ -162,12 +171,13 @@ func (s *DesktopAccountService) ListForActor(ctx context.Context, actor auth.Act
|
||||
return nil, response.ErrInternal(50081, "desktop_accounts_query_failed", "failed to list desktop accounts")
|
||||
}
|
||||
|
||||
clientMap, presenceMap := s.loadClientState(ctx, actor.PrimaryWorkspaceID, rows)
|
||||
clientMap, accountPresenceMap, clientPresenceMap, taskConsumerPresenceMap := s.loadClientState(ctx, actor.PrimaryWorkspaceID, rows)
|
||||
items := make([]DesktopAccountView, 0, len(rows))
|
||||
for _, item := range rows {
|
||||
items = append(items, s.buildDesktopAccountView(&item, clientMap, presenceMap))
|
||||
items = append(items, s.buildDesktopAccountView(&item, clientMap, accountPresenceMap, clientPresenceMap, taskConsumerPresenceMap))
|
||||
}
|
||||
s.applyRuntimeHealth(ctx, actor.PrimaryWorkspaceID, items)
|
||||
s.applyPublishQueueSummaries(ctx, actor.PrimaryWorkspaceID, items)
|
||||
return items, nil
|
||||
}
|
||||
|
||||
@@ -211,7 +221,7 @@ func (s *DesktopAccountService) Upsert(ctx context.Context, client *repository.D
|
||||
return nil, appErr
|
||||
}
|
||||
|
||||
view := s.buildDesktopAccountView(account, nil, nil)
|
||||
view := s.buildDesktopAccountView(account, nil, nil, nil, nil)
|
||||
return &view, nil
|
||||
}
|
||||
|
||||
@@ -355,7 +365,7 @@ func (s *DesktopAccountService) Patch(ctx context.Context, client *repository.De
|
||||
return nil, response.ErrInternal(50084, "desktop_account_patch_failed", "failed to patch desktop account")
|
||||
}
|
||||
|
||||
view := s.buildDesktopAccountView(account, nil, nil)
|
||||
view := s.buildDesktopAccountView(account, nil, nil, nil, nil)
|
||||
return &view, nil
|
||||
}
|
||||
|
||||
@@ -372,7 +382,7 @@ func (s *DesktopAccountService) Tombstone(ctx context.Context, client *repositor
|
||||
return nil, response.ErrInternal(50085, "desktop_account_delete_failed", "failed to delete desktop account")
|
||||
}
|
||||
|
||||
view := s.buildDesktopAccountView(account, nil, nil)
|
||||
view := s.buildDesktopAccountView(account, nil, nil, nil, nil)
|
||||
return &view, nil
|
||||
}
|
||||
|
||||
@@ -398,7 +408,7 @@ func (s *DesktopAccountService) RequestDelete(ctx context.Context, actor auth.Ac
|
||||
return nil, response.ErrInternal(50086, "desktop_account_request_delete_failed", "failed to update desktop account delete request")
|
||||
}
|
||||
|
||||
view := s.buildDesktopAccountView(account, nil, nil)
|
||||
view := s.buildDesktopAccountView(account, nil, nil, nil, nil)
|
||||
return &view, nil
|
||||
}
|
||||
|
||||
@@ -527,6 +537,99 @@ func (s *DesktopAccountService) applyRuntimeHealth(ctx context.Context, workspac
|
||||
}
|
||||
}
|
||||
|
||||
type desktopAccountPublishQueueSummary struct {
|
||||
Count int
|
||||
OldestAt *time.Time
|
||||
}
|
||||
|
||||
func (s *DesktopAccountService) applyPublishQueueSummaries(ctx context.Context, workspaceID int64, items []DesktopAccountView) {
|
||||
if s == nil || s.pool == nil || len(items) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
accountIDs := make([]uuid.UUID, 0, len(items))
|
||||
accountIndex := make(map[uuid.UUID]int, len(items))
|
||||
for index, item := range items {
|
||||
accountID, err := uuid.Parse(item.ID)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
accountIDs = append(accountIDs, accountID)
|
||||
accountIndex[accountID] = index
|
||||
}
|
||||
if len(accountIDs) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
summaries, err := s.loadQueuedPublishTaskSummaries(ctx, workspaceID, accountIDs)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for accountID, summary := range summaries {
|
||||
index, ok := accountIndex[accountID]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
items[index].QueuedPublishTaskCount = summary.Count
|
||||
items[index].OldestQueuedPublishTaskAt = summary.OldestAt
|
||||
}
|
||||
}
|
||||
|
||||
func (s *DesktopAccountService) loadQueuedPublishTaskSummaries(
|
||||
ctx context.Context,
|
||||
workspaceID int64,
|
||||
accountIDs []uuid.UUID,
|
||||
) (map[uuid.UUID]desktopAccountPublishQueueSummary, error) {
|
||||
if s == nil || s.pool == nil || len(accountIDs) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT
|
||||
dt.target_account_id,
|
||||
COUNT(*)::int,
|
||||
MIN(COALESCE(dt.enqueued_at, dt.created_at))
|
||||
FROM desktop_tasks AS dt
|
||||
WHERE dt.workspace_id = $1
|
||||
AND dt.kind = 'publish'
|
||||
AND dt.status = 'queued'
|
||||
AND dt.target_account_id = ANY($2::uuid[])
|
||||
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'
|
||||
)
|
||||
GROUP BY dt.target_account_id
|
||||
`, workspaceID, uniqueUUIDs(accountIDs), desktopPublishMaxAttempts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
summaries := make(map[uuid.UUID]desktopAccountPublishQueueSummary)
|
||||
for rows.Next() {
|
||||
var (
|
||||
accountID uuid.UUID
|
||||
count int
|
||||
oldestAt time.Time
|
||||
)
|
||||
if err := rows.Scan(&accountID, &count, &oldestAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
oldestAt = oldestAt.UTC()
|
||||
summaries[accountID] = desktopAccountPublishQueueSummary{
|
||||
Count: count,
|
||||
OldestAt: &oldestAt,
|
||||
}
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return summaries, nil
|
||||
}
|
||||
|
||||
func shouldApplyDesktopAccountRuntimeHealth(item DesktopAccountView, report bufferedDesktopAccountHealthReport) bool {
|
||||
if item.VerifiedAt == nil {
|
||||
return true
|
||||
@@ -551,10 +654,14 @@ func effectiveDesktopAccountRuntimeHealth(report bufferedDesktopAccountHealthRep
|
||||
func (s *DesktopAccountService) buildDesktopAccountView(
|
||||
account *repository.DesktopAccount,
|
||||
clientMap map[uuid.UUID]*repository.DesktopClient,
|
||||
presenceClientMap map[uuid.UUID]uuid.UUID,
|
||||
accountPresenceMap map[uuid.UUID]uuid.UUID,
|
||||
clientPresenceMap map[uuid.UUID]bool,
|
||||
taskConsumerPresenceMap map[uuid.UUID]bool,
|
||||
) DesktopAccountView {
|
||||
var clientID *string
|
||||
var clientOnline *bool
|
||||
var accountSessionOnline *bool
|
||||
var taskConsumerOnline *bool
|
||||
var clientLastSeenAt *time.Time
|
||||
var clientDeviceName *string
|
||||
|
||||
@@ -562,40 +669,55 @@ func (s *DesktopAccountService) buildDesktopAccountView(
|
||||
value := account.ClientID.String()
|
||||
clientID = &value
|
||||
|
||||
online := false
|
||||
clientOnline = &online
|
||||
clientOnlineValue := false
|
||||
clientOnline = &clientOnlineValue
|
||||
accountSessionOnlineValue := false
|
||||
accountSessionOnline = &accountSessionOnlineValue
|
||||
taskConsumerOnlineValue := false
|
||||
taskConsumerOnline = &taskConsumerOnlineValue
|
||||
|
||||
if clientMap != nil {
|
||||
if client, ok := clientMap[*account.ClientID]; ok && client != nil {
|
||||
clientLastSeenAt = client.LastSeenAt
|
||||
clientDeviceName = client.DeviceName
|
||||
if presenceClientMap != nil {
|
||||
if activeClientID, ok := presenceClientMap[account.DesktopID]; ok && activeClientID == *account.ClientID {
|
||||
online = true
|
||||
}
|
||||
}
|
||||
clientOnlineValue = resolveDesktopClientOnline(
|
||||
client,
|
||||
clientPresenceMap[*account.ClientID],
|
||||
clientPresenceMap != nil,
|
||||
s.nowUTC(),
|
||||
)
|
||||
}
|
||||
}
|
||||
if accountPresenceMap != nil {
|
||||
if activeClientID, ok := accountPresenceMap[account.DesktopID]; ok && activeClientID == *account.ClientID {
|
||||
accountSessionOnlineValue = true
|
||||
}
|
||||
}
|
||||
if taskConsumerPresenceMap != nil && taskConsumerPresenceMap[*account.ClientID] {
|
||||
taskConsumerOnlineValue = true
|
||||
}
|
||||
}
|
||||
|
||||
return DesktopAccountView{
|
||||
ID: account.DesktopID.String(),
|
||||
Platform: account.Platform,
|
||||
PlatformUID: normalizePlatformUID(account.PlatformUID),
|
||||
DisplayName: account.DisplayName,
|
||||
AvatarURL: account.AvatarURL,
|
||||
Health: account.Health,
|
||||
HealthSource: "database",
|
||||
ClientID: clientID,
|
||||
AccountFingerprint: account.AccountFingerprint,
|
||||
VerifiedAt: account.VerifiedAt,
|
||||
Tags: account.Tags,
|
||||
SyncVersion: account.SyncVersion,
|
||||
DeletedAt: account.DeletedAt,
|
||||
DeleteRequestedAt: account.DeleteRequestedAt,
|
||||
ClientOnline: clientOnline,
|
||||
ClientLastSeenAt: clientLastSeenAt,
|
||||
ClientDeviceName: clientDeviceName,
|
||||
ID: account.DesktopID.String(),
|
||||
Platform: account.Platform,
|
||||
PlatformUID: normalizePlatformUID(account.PlatformUID),
|
||||
DisplayName: account.DisplayName,
|
||||
AvatarURL: account.AvatarURL,
|
||||
Health: account.Health,
|
||||
HealthSource: "database",
|
||||
ClientID: clientID,
|
||||
AccountFingerprint: account.AccountFingerprint,
|
||||
VerifiedAt: account.VerifiedAt,
|
||||
Tags: account.Tags,
|
||||
SyncVersion: account.SyncVersion,
|
||||
DeletedAt: account.DeletedAt,
|
||||
DeleteRequestedAt: account.DeleteRequestedAt,
|
||||
ClientOnline: clientOnline,
|
||||
AccountSessionOnline: accountSessionOnline,
|
||||
TaskConsumerOnline: taskConsumerOnline,
|
||||
ClientLastSeenAt: clientLastSeenAt,
|
||||
ClientDeviceName: clientDeviceName,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -603,7 +725,7 @@ func (s *DesktopAccountService) loadClientState(
|
||||
ctx context.Context,
|
||||
workspaceID int64,
|
||||
accounts []repository.DesktopAccount,
|
||||
) (map[uuid.UUID]*repository.DesktopClient, map[uuid.UUID]uuid.UUID) {
|
||||
) (map[uuid.UUID]*repository.DesktopClient, map[uuid.UUID]uuid.UUID, map[uuid.UUID]bool, map[uuid.UUID]bool) {
|
||||
clientIDs := make([]uuid.UUID, 0, len(accounts)*2)
|
||||
accountIDs := make([]uuid.UUID, 0, len(accounts))
|
||||
seen := make(map[uuid.UUID]struct{}, len(accounts)*2)
|
||||
@@ -621,8 +743,8 @@ func (s *DesktopAccountService) loadClientState(
|
||||
clientIDs = append(clientIDs, *account.ClientID)
|
||||
}
|
||||
|
||||
presenceClientMap := loadDesktopAccountPresence(ctx, s.redis, accountIDs)
|
||||
for _, clientID := range presenceClientMap {
|
||||
accountPresenceMap := loadDesktopAccountPresence(ctx, s.redis, accountIDs)
|
||||
for _, clientID := range accountPresenceMap {
|
||||
if _, ok := seen[clientID]; ok {
|
||||
continue
|
||||
}
|
||||
@@ -644,7 +766,16 @@ func (s *DesktopAccountService) loadClientState(
|
||||
}
|
||||
}
|
||||
|
||||
return clientMap, presenceClientMap
|
||||
clientPresenceMap := loadDesktopClientPresence(ctx, s.redis, clientIDs)
|
||||
taskConsumerPresenceMap := loadDesktopTaskConsumerPresence(ctx, s.redis, clientIDs)
|
||||
return clientMap, accountPresenceMap, clientPresenceMap, taskConsumerPresenceMap
|
||||
}
|
||||
|
||||
func (s *DesktopAccountService) nowUTC() time.Time {
|
||||
if s != nil && s.now != nil {
|
||||
return s.now().UTC()
|
||||
}
|
||||
return time.Now().UTC()
|
||||
}
|
||||
|
||||
func resolveDesktopClientOnline(
|
||||
|
||||
Reference in New Issue
Block a user