pico
created pr with
103.1
added 103.2
1: 66cf9b9 = 1: 66cf9b9 feat(pipe): add pipe_monitors table
2: 83b0c70 = 2: 83b0c70 chore(pipe): add new db methods
3: 43aa4a1 = 3: 43aa4a1 chore(pipe): add db impl
4: a75186b = 4: a75186b chore(pipe): add tests for monitoring
5: 9c7fcda = 5: 9c7fcda feat(pipe): add monitor cli
6: 2b86dec = 6: 2b86dec feat(pipe): status and rss commands
7: 8df2aa2 = 7: 8df2aa2 feat(pipe): monitor pub and pipe cmd
8: 11a5bce = 8: 11a5bce chore(pipe): monitor help text
9: 670350c = 9: 670350c refactor(pipe): monitor pipes on throttle interval
10: ade2b83 = 10: ade2b83 fix: window and ping fixes
11: 68f5cd1 = 11: 68f5cd1 chore(pipe): add tests
-: ------- > 12: 74fb40f chore(pipe): add logging stmts
-: ------- > 13: c422818 refactor(pipe): create pipe monitors history table
-: ------- > 14: e0c3a88 fix(pipe): only update monitor if within window
-: ------- > 15: 9a8fbde chore(pipe): implement monitor history db interface
-: ------- > 16: f168507 feat(pipe): record historical monitors
-: ------- > 17: 16df77f feat(pipe): monitor calculate uptime
-: ------- > 18: 10ffdf1 feat(pipe): uptime cli command
-: ------- > 19: 8231ac5 chore(pipe): monitor history stubs
-: ------- > 20: 65a63a9 fix(pipe): uptime cmd
added 103.3
3: 43aa4a1 ! 1: 603ca6e chore(pubsub): add more tests
1: 66cf9b9 < -: ------- feat(pipe): add pipe_monitors table
2: 83b0c70 < -: ------- chore(pipe): add new db methods
4: a75186b ! 2: 17e00b2 feat(pubsub): round robin
13: c422818 ! 3: e3136bd fix(pubsub): check for eof before processing and skip empty byte reads
-: ------- > 4: 9a6d19e fix: rr
5: 9c7fcda < -: ------- feat(pipe): add monitor cli
16: f168507 ! 5: 5b3f3a1 fix: sending 0 byte read
6: 2b86dec < -: ------- feat(pipe): status and rss commands
11: 68f5cd1 ! 6: 4fff471 refactor: fixes
7: 8df2aa2 < -: ------- feat(pipe): monitor pub and pipe cmd
14: e0c3a88 ! 7: d0dfc85 chore: SetDispatch on Broker
8: 11a5bce < -: ------- chore(pipe): monitor help text
9: 670350c < -: ------- refactor(pipe): monitor pipes on throttle interval
10: ade2b83 < -: ------- fix: window and ping fixes
12: 74fb40f < -: ------- chore(pipe): add logging stmts
15: 9a8fbde < -: ------- chore(pipe): implement monitor history db interface
17: 16df77f < -: ------- feat(pipe): monitor calculate uptime
18: 10ffdf1 < -: ------- feat(pipe): uptime cli command
19: 8231ac5 < -: ------- chore(pipe): monitor history stubs
20: 65a63a9 < -: ------- fix(pipe): uptime cmd
cmds
checkout latest patchset:
ssh pr.pico.sh print 103 | git am -3checkout any patchset in a patch request:
ssh pr.pico.sh print 103.[rev] | git am -3add changes to patch request:
git format-patch main --stdout | ssh pr.pico.sh pr add 103
Patchset
103.2
feat(pipe): add pipe_monitors table
Eric Bower
chore(pipe): add new db methods
2025-12-26T21:02:39ZEric Bower
→ chore(pipe): add db impl
2025-12-26T21:24:18ZEric Bower
chore(pipe): add tests for monitoring
2025-12-26T21:37:49ZEric Bower
feat(pipe): add monitor cli
2025-12-28T14:41:40ZEric Bower
feat(pipe): status and rss commands
2025-12-28T14:44:04ZEric Bower
feat(pipe): monitor pub and pipe cmd
2025-12-28T14:55:12ZEric Bower
chore(pipe): monitor help text
2025-12-28T15:26:40ZEric Bower
refactor(pipe): monitor pipes on throttle interval
2025-12-28T15:40:57ZEric Bower
fix: window and ping fixes
2025-12-28T16:22:23ZEric Bower
chore(pipe): add tests
2026-01-03T02:26:40ZEric Bower
chore(pipe): add logging stmts
2026-01-03T03:50:20ZEric Bower
refactor(pipe): create pipe monitors history table
2026-01-05T14:46:42ZEric Bower
fix(pipe): only update monitor if within window
2026-01-07T20:00:47ZEric Bower
chore(pipe): implement monitor history db interface
2026-01-07T21:52:08ZEric Bower
feat(pipe): record historical monitors
2026-01-07T23:19:05ZEric Bower
feat(pipe): monitor calculate uptime
2026-01-07T23:27:20ZEric Bower
feat(pipe): uptime cli command
2026-01-07T23:27:20ZEric Bower
chore(pipe): monitor history stubs
2026-01-08T00:48:50ZEric Bower
fix(pipe): uptime cmd
2026-01-08T00:52:48ZEric Bower
2026-01-08T01:18:38Z
chore(pipe): add db impl
Eric Bower
2025-12-26T21:37:49ZSemantic diff summary
13 added,
1 modified,
0 signature changed,
0 removed
across 3 analyzed files
pkg/db/postgres/storage_test.go
-
function_declarationcleanupTestDatamodified -
function_declarationTestUpsertPipeMonitoradded -
function_declarationTestUpsertPipeMonitor_Updateadded -
function_declarationTestUpdatePipeMonitorLastPingadded -
function_declarationTestRemovePipeMonitoradded -
function_declarationTestFindPipeMonitorByTopic_NotFoundadded
+42
-0
pkg/db/postgres/storage.go
#
| ... | ... | @@ -1514,3 +1514,45 @@ func (me *PsqlDB) InsertAccessLog(log *db.AccessLog) error { | |
| 1514 | 1514 | ) | |
| 1515 | 1515 | return err | |
| 1516 | 1516 | } | |
| 1517 | + | ||
| 1518 | + | func (me *PsqlDB) UpsertPipeMonitor(userID, topic string, dur time.Duration, winEnd *time.Time) error { | |
| 1519 | + | durStr := fmt.Sprintf("%d seconds", int64(dur.Seconds())) | |
| 1520 | + | _, err := me.Db.Exec( | |
| 1521 | + | `INSERT INTO pipe_monitors (user_id, topic, window_dur, window_end) | |
| 1522 | + | VALUES ($1, $2, $3::interval, $4) | |
| 1523 | + | ON CONFLICT (user_id, topic) DO UPDATE SET window_dur = $3::interval, window_end = $4, updated_at = NOW();`, | |
| 1524 | + | userID, | |
| 1525 | + | topic, | |
| 1526 | + | durStr, | |
| 1527 | + | winEnd, | |
| 1528 | + | ) | |
| 1529 | + | return err | |
| 1530 | + | } | |
| 1531 | + | ||
| 1532 | + | func (me *PsqlDB) UpdatePipeMonitorLastPing(userID, topic string, lastPing *time.Time) error { | |
| 1533 | + | _, err := me.Db.Exec( | |
| 1534 | + | `UPDATE pipe_monitors SET last_ping = $3, updated_at = NOW() WHERE user_id = $1 AND topic = $2;`, | |
| 1535 | + | userID, | |
| 1536 | + | topic, | |
| 1537 | + | lastPing, | |
| 1538 | + | ) | |
| 1539 | + | return err | |
| 1540 | + | } | |
| 1541 | + | ||
| 1542 | + | func (me *PsqlDB) RemovePipeMonitor(userID, topic string) error { | |
| 1543 | + | _, err := me.Db.Exec( | |
| 1544 | + | `DELETE FROM pipe_monitors WHERE user_id = $1 AND topic = $2;`, | |
| 1545 | + | userID, | |
| 1546 | + | topic, | |
| 1547 | + | ) | |
| 1548 | + | return err | |
| 1549 | + | } | |
| 1550 | + | ||
| 1551 | + | func (me *PsqlDB) FindPipeMonitorByTopic(userID, topic string) (*db.PipeMonitor, error) { | |
| 1552 | + | monitor := &db.PipeMonitor{} | |
| 1553 | + | err := me.Db.Get(monitor, `SELECT id, user_id, topic, (EXTRACT(EPOCH FROM window_dur) * 1000000000)::bigint as window_dur, window_end, last_ping, created_at, updated_at FROM pipe_monitors WHERE user_id = $1 AND topic = $2;`, userID, topic) | |
| 1554 | + | if err != nil { | |
| 1555 | + | return nil, err | |
| 1556 | + | } | |
| 1557 | + | return monitor, nil | |
| 1558 | + | } |
+111
-1
pkg/db/postgres/storage_test.go
#
| ... | ... | @@ -169,7 +169,7 @@ func cleanupTestData(t *testing.T) { | |
| 169 | 169 | "access_logs", "tuns_event_logs", "analytics_visits", | |
| 170 | 170 | "feed_items", "post_aliases", "post_tags", "posts", | |
| 171 | 171 | "projects", "feature_flags", "payment_history", "tokens", | |
| 172 | - | "public_keys", "app_users", | |
| 172 | + | "public_keys", "pipe_monitors", "app_users", | |
| 173 | 173 | } | |
| 174 | 174 | for _, table := range tables { | |
| 175 | 175 | _, err := testDB.Db.Exec(fmt.Sprintf("DELETE FROM %s", table)) |
| ... | ... | @@ -1472,3 +1472,113 @@ func TestPaymentHistoryData_JSONBRoundtrip(t *testing.T) { | |
| 1472 | 1472 | t.Errorf("expected tx_id 'tx789', got '%s'", txId) | |
| 1473 | 1473 | } | |
| 1474 | 1474 | } | |
| 1475 | + | ||
| 1476 | + | // ============ Pipe Monitor Tests ============ | |
| 1477 | + | ||
| 1478 | + | func TestUpsertPipeMonitor(t *testing.T) { | |
| 1479 | + | cleanupTestData(t) | |
| 1480 | + | ||
| 1481 | + | user, _ := testDB.RegisterUser("pipemonitorowner", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI pipemonitorowner", "comment") | |
| 1482 | + | ||
| 1483 | + | winEnd := time.Now().Add(time.Hour) | |
| 1484 | + | err := testDB.UpsertPipeMonitor(user.ID, "test-topic", 5*time.Minute, &winEnd) | |
| 1485 | + | if err != nil { | |
| 1486 | + | t.Fatalf("UpsertPipeMonitor failed: %v", err) | |
| 1487 | + | } | |
| 1488 | + | ||
| 1489 | + | monitor, err := testDB.FindPipeMonitorByTopic(user.ID, "test-topic") | |
| 1490 | + | if err != nil { | |
| 1491 | + | t.Fatalf("FindPipeMonitorByTopic failed: %v", err) | |
| 1492 | + | } | |
| 1493 | + | if monitor.Topic != "test-topic" { | |
| 1494 | + | t.Errorf("expected topic 'test-topic', got '%s'", monitor.Topic) | |
| 1495 | + | } | |
| 1496 | + | if monitor.WindowDur != 5*time.Minute { | |
| 1497 | + | t.Errorf("expected window_dur 5m, got %v", monitor.WindowDur) | |
| 1498 | + | } | |
| 1499 | + | } | |
| 1500 | + | ||
| 1501 | + | func TestUpsertPipeMonitor_Update(t *testing.T) { | |
| 1502 | + | cleanupTestData(t) | |
| 1503 | + | ||
| 1504 | + | user, _ := testDB.RegisterUser("pipeupdateowner", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI pipeupdateowner", "comment") | |
| 1505 | + | ||
| 1506 | + | winEnd1 := time.Now().Add(time.Hour) | |
| 1507 | + | err := testDB.UpsertPipeMonitor(user.ID, "update-topic", 5*time.Minute, &winEnd1) | |
| 1508 | + | if err != nil { | |
| 1509 | + | t.Fatalf("first UpsertPipeMonitor failed: %v", err) | |
| 1510 | + | } | |
| 1511 | + | ||
| 1512 | + | winEnd2 := time.Now().Add(2 * time.Hour) | |
| 1513 | + | err = testDB.UpsertPipeMonitor(user.ID, "update-topic", 10*time.Minute, &winEnd2) | |
| 1514 | + | if err != nil { | |
| 1515 | + | t.Fatalf("second UpsertPipeMonitor failed: %v", err) | |
| 1516 | + | } | |
| 1517 | + | ||
| 1518 | + | monitor, err := testDB.FindPipeMonitorByTopic(user.ID, "update-topic") | |
| 1519 | + | if err != nil { | |
| 1520 | + | t.Fatalf("FindPipeMonitorByTopic failed: %v", err) | |
| 1521 | + | } | |
| 1522 | + | if monitor.WindowDur != 10*time.Minute { | |
| 1523 | + | t.Errorf("expected window_dur 10m after update, got %v", monitor.WindowDur) | |
| 1524 | + | } | |
| 1525 | + | } | |
| 1526 | + | ||
| 1527 | + | func TestUpdatePipeMonitorLastPing(t *testing.T) { | |
| 1528 | + | cleanupTestData(t) | |
| 1529 | + | ||
| 1530 | + | user, _ := testDB.RegisterUser("pipepingowner", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI pipepingowner", "comment") | |
| 1531 | + | ||
| 1532 | + | winEnd := time.Now().Add(time.Hour) | |
| 1533 | + | err := testDB.UpsertPipeMonitor(user.ID, "ping-topic", 5*time.Minute, &winEnd) | |
| 1534 | + | if err != nil { | |
| 1535 | + | t.Fatalf("UpsertPipeMonitor failed: %v", err) | |
| 1536 | + | } | |
| 1537 | + | ||
| 1538 | + | lastPing := time.Now() | |
| 1539 | + | err = testDB.UpdatePipeMonitorLastPing(user.ID, "ping-topic", &lastPing) | |
| 1540 | + | if err != nil { | |
| 1541 | + | t.Fatalf("UpdatePipeMonitorLastPing failed: %v", err) | |
| 1542 | + | } | |
| 1543 | + | ||
| 1544 | + | monitor, err := testDB.FindPipeMonitorByTopic(user.ID, "ping-topic") | |
| 1545 | + | if err != nil { | |
| 1546 | + | t.Fatalf("FindPipeMonitorByTopic failed: %v", err) | |
| 1547 | + | } | |
| 1548 | + | if monitor.LastPing == nil { | |
| 1549 | + | t.Error("expected last_ping to be set, got nil") | |
| 1550 | + | } | |
| 1551 | + | } | |
| 1552 | + | ||
| 1553 | + | func TestRemovePipeMonitor(t *testing.T) { | |
| 1554 | + | cleanupTestData(t) | |
| 1555 | + | ||
| 1556 | + | user, _ := testDB.RegisterUser("piperemoveowner", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI piperemoveowner", "comment") | |
| 1557 | + | ||
| 1558 | + | winEnd := time.Now().Add(time.Hour) | |
| 1559 | + | err := testDB.UpsertPipeMonitor(user.ID, "remove-topic", 5*time.Minute, &winEnd) | |
| 1560 | + | if err != nil { | |
| 1561 | + | t.Fatalf("UpsertPipeMonitor failed: %v", err) | |
| 1562 | + | } | |
| 1563 | + | ||
| 1564 | + | err = testDB.RemovePipeMonitor(user.ID, "remove-topic") | |
| 1565 | + | if err != nil { | |
| 1566 | + | t.Fatalf("RemovePipeMonitor failed: %v", err) | |
| 1567 | + | } | |
| 1568 | + | ||
| 1569 | + | _, err = testDB.FindPipeMonitorByTopic(user.ID, "remove-topic") | |
| 1570 | + | if err == nil { | |
| 1571 | + | t.Error("expected error finding removed monitor, got nil") | |
| 1572 | + | } | |
| 1573 | + | } | |
| 1574 | + | ||
| 1575 | + | func TestFindPipeMonitorByTopic_NotFound(t *testing.T) { | |
| 1576 | + | cleanupTestData(t) | |
| 1577 | + | ||
| 1578 | + | user, _ := testDB.RegisterUser("pipenotfoundowner", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI pipenotfoundowner", "comment") | |
| 1579 | + | ||
| 1580 | + | _, err := testDB.FindPipeMonitorByTopic(user.ID, "nonexistent-topic") | |
| 1581 | + | if err == nil { | |
| 1582 | + | t.Error("expected error for nonexistent monitor, got nil") | |
| 1583 | + | } | |
| 1584 | + | } |
+16
-0
pkg/db/stub/stub.go
#
| ... | ... | @@ -243,3 +243,19 @@ func (me *StubDB) FindPubkeysInAccessLogs(userID string) ([]string, error) { | |
| 243 | 243 | func (me *StubDB) InsertAccessLog(log *db.AccessLog) error { | |
| 244 | 244 | return errNotImpl | |
| 245 | 245 | } | |
| 246 | + | ||
| 247 | + | func (me *StubDB) UpsertPipeMonitor(userID, topic string, dur time.Duration, winEnd *time.Time) error { | |
| 248 | + | return errNotImpl | |
| 249 | + | } | |
| 250 | + | ||
| 251 | + | func (me *StubDB) UpdatePipeMonitorLastPing(userID, topic string, lastPing *time.Time) error { | |
| 252 | + | return errNotImpl | |
| 253 | + | } | |
| 254 | + | ||
| 255 | + | func (me *StubDB) RemovePipeMonitor(userID, topic string) error { | |
| 256 | + | return errNotImpl | |
| 257 | + | } | |
| 258 | + | ||
| 259 | + | func (me *StubDB) FindPipeMonitorByTopic(userID, topic string) (*db.PipeMonitor, error) { | |
| 260 | + | return nil, errNotImpl | |
| 261 | + | } |