diff --git a/README.md b/README.md index 6080c68..f6c166a 100644 --- a/README.md +++ b/README.md @@ -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) | | `away_message` | TEXT | Away message (empty string if not away) | | `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)`. @@ -2258,7 +2258,7 @@ Index on `(uuid)`. | `ip` | TEXT | Real IP address of this client connection | | `hostname` | TEXT | Reverse DNS hostname of this client connection | | `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)`. diff --git a/internal/db/queries.go b/internal/db/queries.go index 7b437df..29239ea 100644 --- a/internal/db/queries.go +++ b/internal/db/queries.go @@ -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() - _, _ = database.conn.ExecContext( + _, err := database.conn.ExecContext( ctx, "UPDATE sessions SET last_seen = ? WHERE id = ?", now, sessionID, ) + if err != nil { + return fmt.Errorf( + "update session last_seen: %w", err, + ) + } - _, _ = database.conn.ExecContext( + _, err = database.conn.ExecContext( ctx, "UPDATE clients SET last_seen = ? WHERE id = ?", 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. diff --git a/internal/ircserver/conn.go b/internal/ircserver/conn.go index 74e4dd1..f8024f0 100644 --- a/internal/ircserver/conn.go +++ b/internal/ircserver/conn.go @@ -368,6 +368,17 @@ func (c *Conn) handleMessage( 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] if !ok { c.sendNumeric( diff --git a/internal/ircserver/server_test.go b/internal/ircserver/server_test.go index 3717474..99e860b 100644 --- a/internal/ircserver/server_test.go +++ b/internal/ircserver/server_test.go @@ -369,6 +369,39 @@ func TestPingPong(t *testing.T) { 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) { t.Parallel()