()
| 220 | } |
| 221 | |
| 222 | async function main() { |
| 223 | const args = process.argv.slice(2); |
| 224 | const verbose = args.includes('--verbose'); |
| 225 | const projectIdx = args.indexOf('--project'); |
| 226 | const projectFilter = projectIdx >= 0 ? args[projectIdx + 1] : null; |
| 227 | const hoursIdx = args.indexOf('--hours'); |
| 228 | const hours = hoursIdx >= 0 ? Number(args[hoursIdx + 1]) : 24; |
| 229 | |
| 230 | const redis = getRedisCache(); |
| 231 | |
| 232 | console.log('═'.repeat(70)); |
| 233 | console.log('SESSION LIFECYCLE HEALTH CHECK'); |
| 234 | console.log(`time: ${new Date().toISOString()}`); |
| 235 | console.log(`deadman: ${DEADMAN_MS / 60_000} min`); |
| 236 | console.log(`stuck cutoff: ${STUCK_CUTOFF_MS / 60_000} min (deadman + 3 reaper ticks)`); |
| 237 | console.log(`ch window: ${hours}h`); |
| 238 | if (projectFilter) console.log(`project: ${projectFilter}`); |
| 239 | console.log('═'.repeat(70)); |
| 240 | |
| 241 | // 1. Redis inventory |
| 242 | const allProjects = await redis.smembers('session:projects'); |
| 243 | const projects = projectFilter |
| 244 | ? [projectFilter] |
| 245 | : allProjects.slice().sort(); |
| 246 | |
| 247 | header('REDIS — active sessions'); |
| 248 | console.log(` ${allProjects.length} projects in session:projects`); |
| 249 | |
| 250 | const redisInfo = await inspectRedis(projects); |
| 251 | console.log(` ${redisInfo.total} total active sessions`); |
| 252 | console.log(` ${redisInfo.totalEligible} past deadman (expected to reap soon)`); |
| 253 | console.log( |
| 254 | ` ${redisInfo.totalStuck} past deadman + ${STUCK_GRACE_MS / 60_000}min (stuck — reaper is behind)` |
| 255 | ); |
| 256 | console.log( |
| 257 | ` ${redisInfo.totalOrphans} wallclock entries with no blob (in sampled ${SAMPLE_SIZE_PER_PROJECT}/project)` |
| 258 | ); |
| 259 | |
| 260 | if (verbose && redisInfo.stats.length > 0) { |
| 261 | console.log(''); |
| 262 | console.log( |
| 263 | ` ${'project'.padEnd(40)} ${'active'.padStart(8)} ${'eligible'.padStart(10)} ${'stuck'.padStart(8)} ${'orphans'.padStart(8)} ${'oldest'.padStart(10)}` |
| 264 | ); |
| 265 | for (const s of redisInfo.stats.sort((a, b) => b.active - a.active)) { |
| 266 | console.log( |
| 267 | ` ${s.project.padEnd(40)} ${fmt(s.active, 8)} ${fmt(s.eligible, 10)} ${fmt(s.stuck, 8)} ${fmt(s.orphans, 8)} ${s.oldestAgeMin.toFixed(1).padStart(8)}m` |
| 268 | ); |
| 269 | } |
| 270 | } |
| 271 | |
| 272 | // 2. Sessions queue |
| 273 | header('SESSIONS QUEUE — createSessionEnd jobs'); |
| 274 | const queue = await inspectQueue(); |
| 275 | console.log(` waiting: ${queue.waiting}`); |
| 276 | console.log(` delayed: ${queue.delayed}`); |
| 277 | console.log(` active: ${queue.active}`); |
| 278 | console.log(` failed: ${queue.failed}`); |
| 279 | console.log(` completed: ${queue.completed} (removed-on-complete)`); |
no test coverage detected