1package usecase
2
3import (
4 "context"
5 "time"
6
7 "github.com/bnema/dumber/internal/application/port"
8 "github.com/bnema/dumber/internal/domain/entity"
9 "github.com/bnema/dumber/internal/domain/repository"
10 "github.com/bnema/dumber/internal/logging"
11)
12
13// CleanupSessionsUseCase handles automatic cleanup of old exited sessions.
14type CleanupSessionsUseCase struct {
15 sessionRepo repository.SessionRepository
16 processProbe port.SessionProcessProbe
17}
18
19// NewCleanupSessionsUseCase creates a new CleanupSessionsUseCase.
20func NewCleanupSessionsUseCase(
21 sessionRepo repository.SessionRepository,
22 processProbe port.SessionProcessProbe,
23) *CleanupSessionsUseCase {
24 return &CleanupSessionsUseCase{
25 sessionRepo: sessionRepo,
26 processProbe: processProbe,
27 }
28}
29
30// CleanupSessionsInput contains the cleanup configuration.
31type CleanupSessionsInput struct {
32 // MaxExitedSessions is the maximum number of exited sessions to keep.
33 // Sessions beyond this count will be deleted (oldest first).
34 MaxExitedSessions int
35
36 // MaxExitedSessionAgeDays is the maximum age in days for exited sessions.
37 // Sessions older than this will be deleted.
38 // A value of 0 disables age-based cleanup.
39 MaxExitedSessionAgeDays int
40}
41
42// CleanupSessionsOutput contains the cleanup results.
43type CleanupSessionsOutput struct {
44 // DeletedByAge is the number of sessions deleted due to age.
45 DeletedByAge int64
46
47 // DeletedByCount is the number of sessions deleted due to count limit.
48 DeletedByCount int64
49
50 // TotalDeleted is the total number of sessions deleted.
51 TotalDeleted int64
52}
53
54type ReconcileActiveBrowserSessionsInput struct {
55 CurrentSessionID entity.SessionID
56 RecentLimit int
57 EndedAt time.Time
58}
59
60type ReconcileActiveBrowserSessionsOutput struct {
61 EndedDeadSessions int64
62}
63
64// ReconcileActiveBrowserSessions ends only active browser sessions whose recorded process is proven dead.
65func (uc *CleanupSessionsUseCase) ReconcileActiveBrowserSessions(
66 ctx context.Context,
67 input ReconcileActiveBrowserSessionsInput,
68) (ReconcileActiveBrowserSessionsOutput, error) {
69 log := logging.FromContext(ctx)
70 output := ReconcileActiveBrowserSessionsOutput{}
71 if uc.processProbe == nil {
72 return output, nil
73 }
74 if input.RecentLimit <= 0 {
75 input.RecentLimit = 20
76 }
77 endedAt := input.EndedAt
78 if endedAt.IsZero() {
79 endedAt = time.Now()
80 }
81
82 recent, err := uc.sessionRepo.GetRecent(ctx, input.RecentLimit)
83 if err != nil {
84 return output, err
85 }
86 for _, s := range recent {
87 if s == nil || s.ID == input.CurrentSessionID || s.Type != entity.SessionTypeBrowser || !s.IsActive() {
88 continue
89 }
90 if s.ProcessID == nil {
91 log.Debug().Str("session_id", string(s.ID)).Msg("skipping active session without process id")
92 continue
93 }
94 alive, probeErr := uc.processProbe.IsProcessAlive(ctx, *s.ProcessID)
95 if probeErr != nil {
96 log.Warn().Err(probeErr).Str("session_id", string(s.ID)).Int("pid", *s.ProcessID).Msg("failed to probe session process")
97 continue
98 }
99 if alive {
100 continue
101 }
102 if err := uc.sessionRepo.MarkEnded(ctx, s.ID, endedAt); err != nil {
103 return output, err
104 }
105 log.Info().Str("session_id", string(s.ID)).Int("pid", *s.ProcessID).Msg("ended dead browser session")
106 output.EndedDeadSessions++
107 }
108 return output, nil
109}
110
111// Execute cleans up old exited sessions based on the provided configuration.
112// It first deletes sessions older than MaxExitedSessionAgeDays,
113// then enforces the MaxExitedSessions count limit.
114func (uc *CleanupSessionsUseCase) Execute(ctx context.Context, input CleanupSessionsInput) (CleanupSessionsOutput, error) {
115 log := logging.FromContext(ctx)
116 output := CleanupSessionsOutput{}
117
118 // 1. Delete by age first (if enabled)
119 if input.MaxExitedSessionAgeDays > 0 {
120 cutoff := time.Now().AddDate(0, 0, -input.MaxExitedSessionAgeDays)
121 deleted, err := uc.sessionRepo.DeleteExitedBefore(ctx, cutoff)
122 if err != nil {
123 log.Warn().Err(err).Msg("failed to delete sessions by age")
124 } else {
125 output.DeletedByAge = deleted
126 if deleted > 0 {
127 log.Info().
128 Int64("deleted", deleted).
129 Int("max_age_days", input.MaxExitedSessionAgeDays).
130 Msg("cleaned up old sessions by age")
131 }
132 }
133 }
134
135 // 2. Enforce count limit on remaining sessions
136 if input.MaxExitedSessions >= 0 {
137 deleted, err := uc.sessionRepo.DeleteOldestExited(ctx, input.MaxExitedSessions)
138 if err != nil {
139 log.Warn().Err(err).Msg("failed to delete sessions by count")
140 } else {
141 output.DeletedByCount = deleted
142 if deleted > 0 {
143 log.Info().
144 Int64("deleted", deleted).
145 Int("max_count", input.MaxExitedSessions).
146 Msg("cleaned up old sessions by count")
147 }
148 }
149 }
150
151 output.TotalDeleted = output.DeletedByAge + output.DeletedByCount
152
153 if output.TotalDeleted > 0 {
154 log.Info().
155 Int64("total_deleted", output.TotalDeleted).
156 Int64("by_age", output.DeletedByAge).
157 Int64("by_count", output.DeletedByCount).
158 Msg("session cleanup completed")
159 }
160
161 return output, nil
162}