Use a numeric --in as given, without walking /projects.json

Merged
JE
4d ago
+202-20

What

A numeric project ID (--in 123, --project 123) is now used as given, with no request. Until now ResolveProject walked every page of /projects.json (resolver.go:432) to find the project's name. It did this on every command that takes --in <id>. Most of the 85 callers discard the name. An ID that wasn't in the list was already passed through for the API to judge, so the walk validated nothing.

The name comes back only when the session already holds the project list. The two callers that show it now get it another way:

  • config project --project <id> reads that one project with the new ProjectName (GET /projects/<id>.json) to confirm the project exists before saving it. A 404 or 403 still says "not found".
  • timeline --in <id> takes the name from the events' bucket. With no events it falls back to "project #id", the fallback it already had.

Name resolution (--in "Project name") is unchanged and still lists projects.

Why

BC5's API hosts are short of CPU. See this Basecamp card: Ops found that basecamp-cli/* requests use 18% of Rails CPU on the API hosts, and that ProjectsController#index is the third most expensive endpoint there.

In 5 minutes of API-host logs (19 Oct, 18:56–19:01 UTC), the CLI's ProjectsController#index requests used about 193 s of CPU. About 4.5% of all API-host Rails CPU in that window went to these walks. 161 s of it came from requests that sent If-None-Match, and only the SDK's raw GetAll path sends that on page 1, which is the path this resolver uses. Those requests broke down as:

  • about 3,500 page-1 requests, one per walk,
  • about 4,200 later pages,
  • 109 s spent answering 304s.

The bug-triage tooling we run ourselves calls basecamp cards list --in <id> and its siblings many times per sync. Each call walked the whole list first.

Name-based --in still walks the list, and versions already installed keep walking until they upgrade. #909 (pace every request) is complementary to this change, not overlapping.

Behaviour changes

  • An archived project's ID now gets its name from config project, so it can be saved. Before, the archived project was missing from the active list, so it was refused as not found.
  • An ID with leading zeros is normalized ("00123" → "123") without a list lookup. Before, it was normalized only when the list had it, and otherwise passed through as typed.

Testing

  • New resolver tests:
    • a numeric ID makes no request at all;
    • ProjectName reads exactly one project;
    • a 404 or 403 gives an empty name;
    • a 500 is an error;
    • a project missing from a cached list (an archived one) is still read by ID;
    • 00123 and +123 resolve to 123.
  • The template-copy tests now expect one request fewer: the walk they used to record is gone.
  • The config tests serve /projects/<id>.json.
  • bin/ci exits 0 on Linux at 47125c48.
    • On macOS, make lint flags internal/connector/driver/proctime_darwin.go (gosec G115, unconvert). That fails on main too and is untouched here.

Summary by cubic

A numeric project ID (--in 123, --project 123) is now used as given, without walking every page of /projects.json first. That walk ran on every command taking --in <id> and returned a name most callers discard; the API-host CPU cost it caused was called out by Ops as significant.

Behavior changes

  • config project --project <id> reads that one project (GET /projects/<id>.json) to confirm it exists before saving.
  • timeline --in <id> takes the project name from the events' bucket, keeping the fallback format for empty timelines.
  • An archived project's ID is now accepted by config project; ProjectName reads the project itself when it is missing from the session's active-only list.
  • Numeric IDs are canonicalized ("00123" → "123") even without a cached list, so callers that compare IDs with bucket strings still match.
  • Name-based resolution (--in "Name") is unchanged and still lists projects.