feat(desktop): auto-recover in-progress desktop tasks on client lifecycle events
- reset stale in_progress tasks owned by a client on startup, offline, and lease expiry - monitor tasks re-queue for another pick; publish tasks move to unknown for manual reconcile - send startup=true flag on first heartbeat so the server can trigger recovery - finalize the linked desktop task when a phase2 monitor callback arrives via desktop_tasks path - short-circuit the desktop monitor attempt write after the callback already finalized it Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -21,14 +21,20 @@ import (
|
||||
const DesktopClientTokenTTL = 30 * 24 * time.Hour
|
||||
|
||||
type DesktopClientService struct {
|
||||
repo repository.DesktopClientRepository
|
||||
redis *goredis.Client
|
||||
repo repository.DesktopClientRepository
|
||||
redis *goredis.Client
|
||||
taskSvc *DesktopTaskService
|
||||
}
|
||||
|
||||
func NewDesktopClientService(repo repository.DesktopClientRepository, redis *goredis.Client) *DesktopClientService {
|
||||
return &DesktopClientService{repo: repo, redis: redis}
|
||||
}
|
||||
|
||||
func (s *DesktopClientService) WithTaskService(taskSvc *DesktopTaskService) *DesktopClientService {
|
||||
s.taskSvc = taskSvc
|
||||
return s
|
||||
}
|
||||
|
||||
type RegisterDesktopClientRequest struct {
|
||||
ClientID string `json:"client_id"`
|
||||
DeviceName string `json:"device_name" binding:"required"`
|
||||
@@ -45,6 +51,7 @@ type HeartbeatDesktopClientRequest struct {
|
||||
ClientVersion *string `json:"client_version"`
|
||||
Channel *string `json:"channel"`
|
||||
AccountIDs []string `json:"account_ids"`
|
||||
Startup bool `json:"startup"`
|
||||
}
|
||||
|
||||
type DesktopClientView struct {
|
||||
@@ -182,6 +189,12 @@ func (s *DesktopClientService) Heartbeat(ctx context.Context, client *repository
|
||||
markDesktopClientPresent(ctx, s.redis, updated.ID)
|
||||
markDesktopAccountsPresent(ctx, s.redis, updated.ID, parseDesktopAccountIDs(req.AccountIDs))
|
||||
|
||||
if req.Startup && s.taskSvc != nil {
|
||||
if err := s.taskSvc.recoverClientTasksOnStartup(ctx, updated); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return &HeartbeatDesktopClientResponse{
|
||||
Client: buildDesktopClientView(updated),
|
||||
ServerTime: time.Now().UTC(),
|
||||
@@ -202,6 +215,11 @@ func (s *DesktopClientService) Revoke(ctx context.Context, client *repository.De
|
||||
}
|
||||
|
||||
clearDesktopClientPresenceState(ctx, s.redis, updated.ID)
|
||||
if s.taskSvc != nil {
|
||||
if err := s.taskSvc.recoverClientTasksOnDisconnect(ctx, updated); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
view := buildDesktopClientView(updated)
|
||||
return &view, nil
|
||||
@@ -213,6 +231,11 @@ func (s *DesktopClientService) Offline(ctx context.Context, client *repository.D
|
||||
}
|
||||
|
||||
clearDesktopClientPresenceState(ctx, s.redis, client.ID)
|
||||
if s.taskSvc != nil {
|
||||
if err := s.taskSvc.recoverClientTasksOnDisconnect(ctx, client); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return &OfflineDesktopClientResponse{
|
||||
ServerTime: time.Now().UTC(),
|
||||
|
||||
Reference in New Issue
Block a user