forked from molecule-ai/molecule-core
Renames: - platform/ → workspace-server/ (Go module path stays as "platform" for external dep compat — will update after plugin module republish) - workspace-template/ → workspace/ Removed (moved to separate repos or deleted): - PLAN.md — internal roadmap (move to private project board) - HANDOFF.md, AGENTS.md — one-time internal session docs - .claude/ — gitignored entirely (local agent config) - infra/cloudflare-worker/ → Molecule-AI/molecule-tenant-proxy - org-templates/molecule-dev/ → standalone template repo - .mcp-eval/ → molecule-mcp-server repo - test-results/ — ephemeral, gitignored Security scrubbing: - Cloudflare account/zone/KV IDs → placeholders - Real EC2 IPs → <EC2_IP> in all docs - CF token prefix, Neon project ID, Fly app names → redacted - Langfuse dev credentials → parameterized - Personal runner username/machine name → generic Community files: - CONTRIBUTING.md — build, test, branch conventions - CODE_OF_CONDUCT.md — Contributor Covenant 2.1 All Dockerfiles, CI workflows, docker-compose, railway.toml, render.yaml, README, CLAUDE.md updated for new directory names. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
60 lines
1.5 KiB
Go
60 lines
1.5 KiB
Go
package registry
|
|
|
|
import (
|
|
"context"
|
|
"log"
|
|
"strings"
|
|
|
|
"github.com/Molecule-AI/molecule-monorepo/platform/internal/db"
|
|
)
|
|
|
|
// OfflineHandler is called when a workspace's liveness key expires.
|
|
type OfflineHandler func(ctx context.Context, workspaceID string)
|
|
|
|
// StartLivenessMonitor subscribes to Redis keyspace expiry events.
|
|
// When a workspace's liveness key (ws:{id}) expires, it marks the workspace offline
|
|
// and calls the onOffline handler.
|
|
func StartLivenessMonitor(ctx context.Context, onOffline OfflineHandler) {
|
|
sub := db.RDB.PSubscribe(ctx, "__keyevent@0__:expired")
|
|
|
|
log.Println("Liveness monitor started — listening for Redis key expirations")
|
|
|
|
ch := sub.Channel()
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
sub.Close()
|
|
return
|
|
case msg := <-ch:
|
|
if msg == nil {
|
|
continue
|
|
}
|
|
key := msg.Payload
|
|
if !strings.HasPrefix(key, "ws:") {
|
|
continue
|
|
}
|
|
parts := strings.SplitN(key, ":", 3)
|
|
if len(parts) != 2 {
|
|
continue
|
|
}
|
|
workspaceID := parts[1]
|
|
|
|
log.Printf("Liveness: workspace %s TTL expired", workspaceID)
|
|
|
|
// Mark offline in Postgres — skip paused and hibernated workspaces (no active container)
|
|
_, err := db.DB.ExecContext(ctx, `
|
|
UPDATE workspaces SET status = 'offline', updated_at = now()
|
|
WHERE id = $1 AND status NOT IN ('removed', 'paused', 'hibernated')
|
|
`, workspaceID)
|
|
if err != nil {
|
|
log.Printf("Liveness: failed to mark %s offline: %v", workspaceID, err)
|
|
continue
|
|
}
|
|
|
|
if onOffline != nil {
|
|
onOffline(ctx, workspaceID)
|
|
}
|
|
}
|
|
}
|
|
}
|