Keep active IRC listener users from expiring (closes #121)
check / check (push) Waiting to run

Every command a registered IRC listener client sends, PING and PONG
included, now sets last_seen on its session and its client, so the
idle cleanup no longer removes an IRC user who is still active; one
who sends nothing for SESSION_IDLE_TIMEOUT is still removed. The two
UPDATEs that GetSessionByToken ran for every authenticated HTTP API
request move into UpdateLastSeen, which both transports now call. The
IRC listener refreshes on every command, with no interval between
writes: that is what every HTTP API request already costs. README.md
describes last_seen to match.

Model: opus-5-5
Co-authored-by: clawbot <sneak+clawbot@sneak.cloud>
This commit was merged in pull request #126.
This commit is contained in:
2026-10-08 10:47:55 +02:00
committed by clawbot
parent 0f2e63d0cc
commit ccb2bf7960
4 changed files with 70 additions and 5 deletions
+2 -2
View File
@@ -2243,7 +2243,7 @@ The database schema is managed via embedded SQL migration files in
| `signing_key` | TEXT | Public signing key (empty string if unset) | | `signing_key` | TEXT | Public signing key (empty string if unset) |
| `away_message` | TEXT | Away message (empty string if not away) | | `away_message` | TEXT | Away message (empty string if not away) |
| `created_at` | DATETIME | Session creation time | | `created_at` | DATETIME | Session creation time |
| `last_seen` | DATETIME | Last API request time | | `last_seen` | DATETIME | Last API request or IRC listener command time |
Index on `(uuid)`. Index on `(uuid)`.
@@ -2258,7 +2258,7 @@ Index on `(uuid)`.
| `ip` | TEXT | Real IP address of this client connection | | `ip` | TEXT | Real IP address of this client connection |
| `hostname` | TEXT | Reverse DNS hostname of this client connection | | `hostname` | TEXT | Reverse DNS hostname of this client connection |
| `created_at` | DATETIME | Client creation time | | `created_at` | DATETIME | Client creation time |
| `last_seen` | DATETIME | Last API request time | | `last_seen` | DATETIME | Last API request or IRC listener command time |
Indexes on `(token)` and `(session_id)`. Indexes on `(token)` and `(session_id)`.
+24 -3
View File
@@ -204,21 +204,42 @@ func (database *Database) GetSessionByToken(
) )
} }
_ = database.UpdateLastSeen(ctx, sessionID, clientID)
return sessionID, clientID, nick, nil
}
// UpdateLastSeen sets last_seen to now on a session and on
// one of its clients, so that the idle cleanup keeps them.
func (database *Database) UpdateLastSeen(
ctx context.Context,
sessionID, clientID int64,
) error {
now := time.Now() now := time.Now()
_, _ = database.conn.ExecContext( _, err := database.conn.ExecContext(
ctx, ctx,
"UPDATE sessions SET last_seen = ? WHERE id = ?", "UPDATE sessions SET last_seen = ? WHERE id = ?",
now, sessionID, now, sessionID,
) )
if err != nil {
return fmt.Errorf(
"update session last_seen: %w", err,
)
}
_, _ = database.conn.ExecContext( _, err = database.conn.ExecContext(
ctx, ctx,
"UPDATE clients SET last_seen = ? WHERE id = ?", "UPDATE clients SET last_seen = ? WHERE id = ?",
now, clientID, now, clientID,
) )
if err != nil {
return fmt.Errorf(
"update client last_seen: %w", err,
)
}
return sessionID, clientID, nick, nil return nil
} }
// GetSessionByNick returns session id for a given nick. // GetSessionByNick returns session id for a given nick.
+11
View File
@@ -368,6 +368,17 @@ func (c *Conn) handleMessage(
return return
} }
// Every command is activity, so the idle cleanup must not
// remove this user.
err := c.database.UpdateLastSeen(
ctx, c.sessionID, c.clientID,
)
if err != nil {
c.log.Error(
"failed to update last_seen", "error", err,
)
}
handler, ok := c.commands[msg.Command] handler, ok := c.commands[msg.Command]
if !ok { if !ok {
c.sendNumeric( c.sendNumeric(
+33
View File
@@ -369,6 +369,39 @@ func TestPingPong(t *testing.T) {
assertContains(t, lines, "PONG", "PONG response") assertContains(t, lines, "PONG", "PONG response")
} }
// TestIdleCleanupKeepsActiveUser checks that a user who
// sends commands is kept by the idle cleanup, and a user who
// sends nothing is not.
func TestIdleCleanupKeepsActiveUser(t *testing.T) {
t.Parallel()
const idleTimeout = 500 * time.Millisecond
env := newTestEnv(t)
active := env.dial(t)
active.register("active")
idle := env.dial(t)
idle.register("idle")
time.Sleep(idleTimeout)
active.sendAndExpect("PING :still here", "PONG")
// The idle cleanup in internal/handlers removes exactly
// the users this returns.
stale, err := env.database.GetStaleOrphanSessions(
t.Context(), time.Now().Add(-idleTimeout),
)
if err != nil {
t.Fatalf("get stale sessions: %v", err)
}
if len(stale) != 1 || stale[0].Nick != "idle" {
t.Errorf("cleanup removes %v, want only idle", stale)
}
}
func TestJoinChannel(t *testing.T) { func TestJoinChannel(t *testing.T) {
t.Parallel() t.Parallel()