git_actors_case_study.md (35258B)
1 # Git Actors Case Study 2 3 # Why Custom Git Management Uses an Actor System Instead of HTTP Requests 4 5 ## Executive Summary 6 7 A real world project uses an actor-based architecture for git management, not 8 primarily because of WebSocket integration, but because git operations are 9 inherently stateful, long-running, and require shared mutable state that HTTP's 10 stateless request-response model cannot efficiently handle. 11 12 See also: 13 [DAL Architecture Overview](/system_design/dal_architecture_overview.md) 14 15 --- 16 17 ## 1. The Core Problem: Stateful Git Operations 18 19 ### What HTTP Gives You 20 21 ``` 22 Client → HTTP Request → Server → HTTP Response → Client 23 ``` 24 25 - Stateless: Each request is independent 26 - Fire-and-forget: No persistent connection 27 - No shared context between requests 28 29 ### What Git Actually Requires 30 31 ``` 32 Client → Load Repo → Edit Files → Save → Commit → Switch Branch → ... 33 ↓ ↓ ↓ ↓ 34 [Actor maintains working directory state] 35 ``` 36 37 Git operations are stateful: 38 39 1. You load a repository once 40 2. Make multiple edits over time 41 3. The edits persist in a working directory 42 4. You compile, stage, commit incrementally 43 5. The state persists until explicitly saved or the session ends 44 45 --- 46 47 ## 2. Architectural Analysis of the Codebase 48 49 ### The Three-Layer Architecture 50 51 ``` 52 ┌──────────────────────────────────────────────────────────────────────────┐ 53 │ FRONTEND (Svelte) │ 54 │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ 55 │ │ FileTree │ │ CodeEditor │ │ GitEditor │ │ Canvas │ │ 56 │ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │ 57 │ │ │ │ │ │ 58 │ └────────────────┼────────────────┼────────────────┘ │ 59 │ │ │ 60 │ ┌───────────▼───────────────┐ │ 61 │ │ WebSocket Connection │◄──── Persistent Connection │ 62 │ │ (TypeScript Client) │ │ 63 │ └─────────────┬─────────────┘ │ 64 └────────────────────────────┼─────────────────────────────────────────────┘ 65 │ Binary Protocol (MessagePack) 66 ▼ 67 ┌─────────────────────────────────────────────────────────────────────────┐ 68 │ BACKEND (Rust) │ 69 │ ┌─────────────────────────────────────────────────────────────────┐ │ 70 │ │ WebSocket Actor (per-connection) │ │ 71 │ │ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ │ 72 │ │ │ StaticSession │ │ DynamicSession │ │ Ingress Router │ │ │ 73 │ │ │ (auth, sender) │ │ (actor sender) │ │ │ │ │ 74 │ │ └────────┬────────┘ └────────┬────────┘ └─────────────────┘ │ │ 75 │ └───────────┼────────────────────┼────────────────────────────────┘ │ 76 │ │ │ │ 77 │ └──────────┬─────────┘ │ 78 │ │ │ 79 │ ┌──────────────────────▼──────────────────────────────────────────┐ │ 80 │ │ ALLOCATOR ACTOR (Singleton per Server) │ │ 81 │ │ │ │ 82 │ │ HashMap<(project_id, branch) → (GitActorSender, JoinHandle)> │ │ 83 │ │ │ │ 84 │ │ Messages: Register | DeRegister | Kill | GetSender | GC │ │ 85 │ └─────────────────────────┬───────────────────────────────────────┘ │ 86 │ │ │ 87 │ ┌──────────────────┼──────────────────┐ │ 88 │ ▼ ▼ ▼ │ 89 │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ 90 │ │ Git Files │ │ Git Files │ │ Git Files │ ... │ 91 │ │ Actor 1 │ │ Actor 2 │ │ Actor 3 │ │ 92 │ │ (proj:1, │ │ (proj:2, │ │ (proj:1, │ │ 93 │ │ branch:a) │ │ branch:x) │ │ branch:b) │ │ 94 │ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │ 95 │ │ │ │ │ 96 │ ▼ ▼ ▼ │ 97 │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ 98 │ │ Working Dir │ │ Working Dir │ │ Working Dir │ │ 99 │ │ (TempDir) │ │ (TempDir) │ │ (TempDir) │ │ 100 │ │ + Compiler │ │ + Compiler │ │ + Compiler │ │ 101 │ │ State │ │ State │ │ State │ │ 102 │ └─────────────┘ └─────────────┘ └─────────────┘ │ 103 │ │ 104 │ ┌─────────────────────────────────────────────────────────────┐ │ 105 │ │ DATABASE (PostgreSQL) │ │ 106 │ │ ┌────────────┐ ┌────────────┐ ┌────────────────────────┐ │ │ 107 │ │ │ Git Blobs │ │ Git File │ │ Auth Sessions │ │ │ 108 │ │ │ (tarballs) │ │ Actor │ │ WebSocket Sessions │ │ │ 109 │ │ │ │ │ Sessions │ │ │ │ │ 110 │ │ └────────────┘ └────────────┘ └────────────────────────┘ │ │ 111 │ └─────────────────────────────────────────────────────────────┘ │ 112 └─────────────────────────────────────────────────────────────────────────┘ 113 ``` 114 115 --- 116 117 ## 3. Why Actors, Not HTTP? 118 119 ### 3.1 Stateful Working Directory 120 121 ```rust 122 // From git_files/actor.rs 123 pub async fn git_files_actor<X: ...>( 124 mut rx: mpsc::Receiver<IncomingFileActorMessage>, 125 project_id: i32, 126 branch: String, 127 Storage handle: Arc<dyn GitDataTransfer + 'static>, 128 ) -> Result<(), NanoServiceError> { 129 130 // 1. Load git data from storage ONCE 131 let git_data = storage_handle.load_git_data(project_id, branch.clone()).await?; 132 133 // 2. Unpack to temp directory ONCE 134 unpack_tar_gz(git_data, source_dir.clone())?; 135 136 // 3. Cache compiler state in memory 137 let mut compiler_state = EntryPointStates::new(); 138 let dep_graph = load_from_disk(&source_dir).unwrap_or(DependencyGraph::new()); 139 140 // 4. Listen for operations on this specific working directory 141 while let Some(message) = rx.recv().await { 142 match message { 143 IncomingFileActorMessage::ReadFile(tx, path) => { ... } 144 IncomingFileActorMessage::WriteFile(tx, path, data) => { ... } 145 IncomingFileActorMessage::Compile(tx, path) => { ... } 146 // ... 147 } 148 } 149 } 150 ``` 151 152 If this were HTTP: 153 154 ``` 155 HTTP POST /files/read → Must reload repo, unpack tarball, return file 156 HTTP POST /files/write → Must reload repo, unpack tarball, write, repack, save 157 HTTP POST /compile → Must reload repo, unpack tarball, load graph, compile 158 ``` 159 160 Each request would: 161 162 - Download the entire repo from DB (expensive) 163 - Unpack the tarball (slow) 164 - Perform tiny operation 165 - Save back to DB 166 - No caching of compilation state 167 168 ### 3.2 Compiler State Persistence 169 170 ```rust 171 // The actor maintains compilation state across requests 172 let mut compiler_state = EntryPointStates::new(); 173 174 // First compile: builds dependency graph from scratch 175 let outcome = compile_entry_point(source_dir.as_path(), file_path, &mut compiler_state).await; 176 177 // Second compile: reuses cached graph, only recompiles changed nodes 178 let outcome = compile_entry_point(source_dir.as_path(), file_path, &mut compiler_state).await; 179 ``` 180 181 Why this matters: 182 183 - Dependency graphs can be megabytes for complex CAD projects 184 - Incremental compilation: Change one file → only recompile affected nodes 185 - HTTP cannot do this: No shared state between requests 186 187 ### 3.3 Reference Counting & Session Management 188 189 ```rust 190 // From allocator/actor.rs 191 pub async fn websocket_allocator_actor(...) { 192 let mut allocator = AllocatorMap::new(); // In-memory state 193 194 while let Some(message) = rx.recv().await { 195 match message { 196 IncomingAllocatorMessage::Register(tx, project_id, branch) => { 197 // Check if actor already exists 198 // If yes: increment ref_count, return existing sender 199 // If no: spawn new actor, return new sender 200 } 201 IncomingAllocatorMessage::DeRegister(tx, project_id, branch) => { 202 // Decrement ref_count 203 // If ref_count == 0: set time_zeroed for garbage collection 204 } 205 } 206 } 207 } 208 ``` 209 210 ```rust 211 // From state.rs 212 pub type AllocatorMap = HashMap<AllocatorKey, (GitActorSender, ActorJoinHandle)>; 213 214 #[derive(Debug, PartialEq, Hash, Eq)] 215 pub struct AllocatorKey { 216 pub project_id: i32, 217 pub branch: String 218 } 219 ``` 220 221 The Session Model: 222 223 ``` 224 User A opens project 1, branch "main" → ref_count = 1 225 User B opens project 1, branch "main" → ref_count = 2, SAME actor 226 User A closes → ref_count = 1, actor stays alive 227 User B closes → ref_count = 0, actor marked for GC 228 ``` 229 230 HTTP Alternative Problems: 231 232 ``` 233 HTTP: No persistent state. Each request is independent. 234 - User A opens project: start session 235 - User A makes 100 edits: 100 independent requests 236 - User B opens same project: start ANOTHER session 237 - Database: Two separate copies of the repo loaded 238 - Memory: Double memory usage 239 - Coherence: Two separate working directories, no shared state 240 ``` 241 242 ### 3.4 Locking & Consistency 243 244 ```rust 245 // From git_files/actor.rs - AcquireLock message 246 IncomingFileActorMessage::AcquireLock(sender, rx) => { 247 let _ = sender.send(OutgoingFileActorMessage::LockAcquired); 248 249 // Mutex-like behavior: hold lock until ReleaseLock 250 let outcome = match rx.await { 251 Ok(message) => message, 252 Err(_) => continue, 253 }; 254 match outcome { 255 IncomingFileActorMessage::ReleaseLock(tx) => { 256 let _: Result<(), OutgoingFileActorMessage> = 257 tx.send(OutgoingFileActorMessage::LockReleased); 258 }, 259 _ => continue, 260 } 261 } 262 ``` 263 264 Why locks matter: 265 266 - Two users editing the same file simultaneously 267 - User A's write must complete before User B's write 268 - Actor ensures sequential processing of messages 269 - No race conditions, no lost updates 270 271 ### 3.5 Garbage Collection & Cleanup 272 273 ```rust 274 // From garbage_collector.rs 275 pub async fn garbage_collector<X>(alloc_sender: AllocatorMessageSender) { 276 loop { 277 sleep(Duration::from_secs(20)).await; 278 let _ = send_gc_request(&alloc_sender).await; 279 } 280 } 281 ``` 282 283 The lifecycle: 284 285 1. User opens project → Actor spawned, ref_count = 1 286 2. Multiple users open → ref_count incremented 287 3. User closes → ref_count decremented 288 4. Last user closes → ref_count = 0, time_zeroed set 289 5. GC runs every 20 seconds → deletes actors with time_zeroed > 120 seconds ago 290 6. Grace period: If user reopens within 2 minutes, actor still exists 291 292 HTTP can't do this: No state to clean up, but also no caching benefits. 293 294 --- 295 296 ## 4. The WebSocket Integration (Secondary Benefit) 297 298 You asked: "Is it because of its integration with WebSocket frontend?" 299 300 Partially, but it's not the primary reason. Here's the relationship: 301 302 ### WebSocket Benefits (Secondary) 303 304 ``` 305 ┌───────────────────────────────────────────────────────────────┐ 306 │ HTTP vs WebSocket │ 307 ├───────────────────────────────────────────────────────────────┤ 308 │ HTTP: │ 309 │ - Open connection, send request, get response, close │ 310 │ - Good for: auth, project CRUD, one-off operations │ 311 │ │ 312 │ WebSocket: │ 313 │ - Persistent connection, bidirectional messaging │ 314 │ - Good for: real-time file editing, compilation feedback, │ 315 │ multiplayer sync, server-initiated notifications │ 316 └───────────────────────────────────────────────────────────────┘ 317 ``` 318 319 ### But HTTP Could Also Work with Actors! 320 321 ```rust 322 // Hypothetical HTTP approach with actors (NOT how it's done here) 323 // HTTP endpoints would still talk to actors internally: 324 325 async fn read_file_handler( 326 Query((project_id, branch)): Query<(i32, String)>, 327 ) -> impl IntoResponse { 328 // 1. Get actor sender from allocator 329 let sender = get_actor_sender(project_id, &branch).await?; 330 331 // 2. Send message to actor 332 let path = "file.txt"; 333 send_read_file_request(path, &sender).await 334 } 335 ``` 336 337 So why WebSocket? 338 339 | Requirement | HTTP | WebSocket | Actor System | 340 | ------------------------ | ---- | --------- | ------------ | 341 | Stateful file operations | ❌ | ⚠️ | ✅ | 342 | Compiler state caching | ❌ | ❌ | ✅ | 343 | Session ref counting | ❌ | ⚠️ | ✅ | 344 | Real-time updates | ❌ | ✅ | ✅ | 345 | Locking/concurrency | ❌ | ⚠️ | ✅ | 346 | Server→client push | ❌ | ✅ | ✅ | 347 348 ## 5. System Design Patterns Used 349 350 ### 5.1 Actor Pattern (Erlang-style) 351 352 ```rust 353 // Single-threaded message processing per actor 354 while let Some(message) = rx.recv().await { 355 // Process ONE message at a time 356 // No locks needed within the actor 357 // Actor owns all its state 358 } 359 ``` 360 361 ### 5.2 Resource Pool Pattern (Allocator) 362 363 ```rust 364 // One actor per (project, branch) tuple 365 // Reused across multiple websocket sessions 366 // Ref counting prevents premature cleanup 367 ``` 368 369 ### 5.3 Supervisor Pattern (implied) 370 371 ```rust 372 // If actor panics, it's isolated 373 // WebSocket handler aborts ping actor 374 // Cleanup still runs on session end 375 ``` 376 377 ### 5.4 Message Passing Concurrency 378 379 ```rust 380 // No shared mutable state 381 // Communication via channels only 382 // Type-safe message protocols 383 ``` 384 385 ### 5.5 Event Sourcing (implied) 386 387 ```rust 388 // Changes don't modify stored data immediately 389 // SaveSnapshot packages entire working dir 390 // Stored as tarball in DB (immutable blob) 391 ``` 392 393 --- 394 395 ## 6. Comparison: Actor vs HTTP Architectures 396 397 ### Actor-Based (Current) 398 399 ``` 400 ┌────────────────────────────────────────────────────────────────────┐ 401 │ ACTOR SYSTEM │ 402 ├───────┬───┬──────────────┬───────────────┬───────────────┬─────────┤ 403 │ │ │ 404 │ Session 1 ─┬─► DynamicSession ─┬─► Allocator ──┬─► GitActor ──────┼──► TempDir 405 │ │ │ │ │ + State 406 │ Session 2 ─┤ │ │ │ 407 │ │ │ └─► GitActor ──────┼──► TempDir 408 │ Session 3 ─┘ │ │ + State 409 │ │ │ 410 │ Global GC ────────────────────────────────────────────────────────┼──► Cleanup 411 │ │ 412 │ ✓ Single copy of repo in memory per (project, branch) │ 413 │ ✓ Compiler state persists across requests │ 414 │ ✓ Atomic operations with locks │ 415 │ ✓ Graceful cleanup via ref counting │ 416 │ ✓ WebSocket naturally maps to actor sessions │ 417 └────────────────────────────────────────────────────────────────────┘ 418 ``` 419 420 ### HTTP-Based Alternative (Theoretical) 421 422 ``` 423 ┌─────────────────────────────────────────────────────────────────┐ 424 │ HTTP STATELESS SYSTEM │ 425 ├───────┬───┬──────────────┬───────────────┬───────────────┬──────┤ 426 │ │ │ 427 │ HTTP/1 ──► Load Project ──► Edit ──► Save ──► Compile ──► ... │ 428 │ │ │ │ │ │ 429 │ ▼ ▼ ▼ ▼ │ 430 │ Download Upload Upload Upload │ 431 │ tarball tarball tarball tarball │ 432 │ │ │ │ │ │ 433 │ └───────────┴────────┴─┬─────┘ │ 434 │ DB (every operation) │ 435 │ │ 436 │ ✗ Download entire repo on every request │ 437 │ ✗ Unpack/repack tarball on every operation │ 438 │ ✗ No compiler state caching │ 439 │ ✗ No incremental compilation │ 440 │ ✗ Multiple users = multiple copies of same repo │ 441 │ ✗ Slow response times │ 442 │ ✗ High database load │ 443 └─────────────────────────────────────────────────────────────────┘ 444 ``` 445 446 --- 447 448 ## 7. Summary: Why Actor System Wins 449 450 | Factor | HTTP | Actor System | 451 | ------------------------ | ------------------------------ | ------------------------------- | 452 | Stateful file operations | ❌ Would need external cache | ✅ Actors own working directory | 453 | Compiler state | ❌ Must reload on each compile | ✅ Cached in memory | 454 | Incremental compilation | ❌ Full rebuild every time | ✅ Only changed nodes | 455 | Session management | ⚠️ External session store | ✅ Ref counting built-in | 456 | Locking | ⚠️ Database locks | ✅ Message queue serializes | 457 | Memory efficiency | ❌ N copies for N users | ✅ 1 copy shared via sender | 458 | Real-time updates | ❌ Long-polling/comet | ✅ WebSocket native | 459 | Graceful cleanup | ⚠️ TTL-based | ✅ GC with grace period | 460 | WebSocket integration | ⚠️ Request-response over WS | ✅ Native message passing | 461 | Multiplayer sync | ❌ Complex broadcast logic | ✅ Actors as session boundaries | 462 463 The actor system is used because git operations are fundamentally stateful, and 464 the actor model provides: 465 466 1. Stateful working directories — Load once, edit many times 467 2. Compiler state caching — Incremental compilation via dependency graphs 468 3. Session multiplexing — One actor, many websocket connections 469 4. Reference counting — Memory-efficient session management 470 5. Locking — Sequential 471 472 --- 473 474 ## 9. Reference: HashMap Allocator Details 475 476 ### What the HashMap Allocator Does for the Git Management Actor 477 478 The AllocatorMap (`HashMap<AllocatorKey, (GitActorSender, ActorJoinHandle)>`) 479 acts as the in-memory registry for all running git file actors on this server. 480 Here's what it does: 481 482 #### Core Responsibilities 483 484 1. **Tracks Live Actors**: Every time a new git file actor is spawned (on first 485 Register), its (sender, join_handle) tuple gets inserted into the HashMap 486 keyed by (project_id, branch). 487 2. **Enables Actor Reuse**: When a subsequent Register comes in for the same 488 project/branch, instead of spawning a new actor, the allocator: 489 - Increments the ref_count in the DB (for tracking how many clients are using 490 it) 491 - Looks up the existing sender in the HashMap and returns it (no new actor 492 spawned) 493 - This is why both senders in your multi-client tests work — they point to 494 the same actor. 495 3. **Provides Fast O(1) Sender Lookup**: The `get_sender` process does a HashMap 496 lookup to retrieve a cloned sender. This is a synchronous, non-DB operation — 497 critical for low-latency websocket routing. 498 4. **Cleans Up on Server Restart**: The actor starts by wiping all sessions in 499 the DB for this server tag, ensuring stale state from a previous crashed 500 instance is gone. 501 5. **Enables Targeted Kill**: The kill process removes the entry from the 502 HashMap, then waits on the join handle to confirm the actor stopped. 503 504 #### Relationship Between Websocket Actor Allocator and Git File Actor Allocator 505 506 They are the same allocator — **there is only one actor managing everything**. 507 Here's how they relate: 508 509 ``` 510 ┌────────────────────────────────────────────────────────────────────┐ 511 │ WEBSOCKET CONNECTION #1 │ 512 │ (one per connected client browser) │ 513 │ │ 514 │ - Owns its own ping actor (health monitoring) │ 515 │ - Owns DynamicSession + StaticSession state │ 516 │ - Communicates with the allocator via mpsc channel │ 517 └────────────────────────────────────────────────────────────────────┘ 518 │ 519 │ send_register_request() 520 ▼ 521 ┌────────────────────────────────────────────────────────────────────┐ 522 │ ALLOCATOR ACTOR (Single Global Actor) │ 523 │ ┌───────────────────────────────────────────────────────────────┐ │ 524 │ │ AllocatorMap HashMap (in-memory state) │ │ 525 │ │ Key: AllocatorKey { project_id, branch } │ │ 526 │ │ Value: (GitActorSender, JoinHandle) │ │ 527 │ │ │ │ 528 │ │ Example entries: │ │ 529 │ │ (project:42, "main") -> (sender_A, handle_1) │ │ 530 │ │ (project:42, "dev") -> (sender_B, handle_2) │ │ 531 │ │ (project:99, "main") -> (sender_C, handle_3) │ │ 532 │ └───────────────────────────────────────────────────────────────┘ │ 533 │ │ 534 │ Receives messages: Register, DeRegister, Kill, GetSender, GC │ 535 └────────────────────────────────────────────────────────────────────┘ 536 │ 537 ┌─────────────┴────────────┐ 538 │ │ 539 ▼ ▼ 540 ┌──────────────────────────────────┐ ┌──────────────────────────────────┐ 541 │ GIT FILE ACTOR │ │ GIT FILE ACTOR │ 542 │ (project:42, branch:"main") │ │ (project:42, branch:"dev") │ 543 │ │ │ │ 544 │ - Owns temp dir on disk │ │ - Owns temp dir on disk │ 545 │ - Handles file operations │ │ - Handles file operations │ 546 │ - Has compiler state │ │ - Has compiler state │ 547 │ - Persists to DB on save │ │ - Persists to DB on save │ 548 └──────────────────────────────────┘ └──────────────────────────────────┘ 549 ``` 550 551 #### The Message Flow 552 553 ``` 554 Client Browser Websocket Actor 555 │ │ 556 │──── websocket connect ─────────────────>│ 557 │ │ 558 │ │ 1. auth check 559 │ │ 2. calls allocator_actor_constructor() 560 │ │ (static singleton, runs once per server) 561 │ │ 562 │ │ 3. send_register_request(project_id, "main") 563 │ │ to allocator via mpsc::Sender 564 │ │ 565 │ ▼ 566 │ ┌───────────────────┐ 567 │ │ ALLOCATOR ACTOR │ 568 │ │ │ 569 │ │ checks DB → no existing session 570 │ │ spawns git_files_actor_constructor() 571 │ │ inserts into HashMap 572 │ │ creates DB session (ref_count=1) 573 │ └───────────────────┘ 574 │ │ 575 │ │ returns GitActorSender 576 │ │ 577 │ ▼ 578 │ ┌───────────────────┐ 579 │ │ GIT FILE ACTOR │ 580 │ │ (project:42, main)│ 581 │ │ - loads tarball │ 582 │ │ - extracts files │ 583 │ │ - in-memory state │ 584 │ └───────────────────┘ 585 │ │ 586 │<────── websocket messages ──────────────┤ 587 │ (routed to git file actor) │ 588 │ │ 589 │ ─────── disconnect ────────────────────>│ 590 │ │ cleanup() → send_deregister_request() 591 │ │ ref_count decremented in DB 592 │ │ HashMap entry NOT removed (actor stays alive) 593 │ │ 594 │ [if ref_count == 0, background GC sends Kill 595 │ → removes from HashMap, deletes DB session] 596 ``` 597 598 #### Key Distinction: Two Different Session Types 599 600 | Session Type | Storage | Purpose | Managed By | 601 | ---------------------- | ---------------------------------- | -------------------------------------------------------------------- | ------------------------------------ | 602 | Websocket Session | DB only (websocket_sessions table) | Track which users are connected, server tag for cleanup | Websocket actor's cleanup() function | 603 | Git File Actor Session | DB + HashMap | Track actor lifecycle, ref_count for sharing, server tag for cleanup | Allocator actor | 604 605 The websocket session exists purely in the DB to survive server restarts (so you 606 know a user was connected before the crash). The git file actor session lives in 607 both DB and HashMap — the DB for persistence across restarts, the HashMap for 608 fast in-memory access. 609 610 #### Why One Allocator Handles Both 611 612 The name "websocket allocator" in some comments is a bit misleading — it 613 **doesn't allocate websocket connections**. It allocates git file actors on 614 behalf of websocket connections. All websocket connections on the server share 615 the same allocator actor because: 616 617 1. **Actor Model**: The allocator is a single tokio task with an mpsc channel. 618 All websocket actors send messages to it. 619 2. **Shared State**: The HashMap needs to be shared across all websocket 620 connections so they can find/reuse the same git file actor for a given 621 project/branch. 622 3. **Efficiency**: One allocator means one place to manage actor lifecycles, 623 reference counting, and cleanup — avoiding distributed state synchronization 624 issues. 625 626 #### The Reference Counting Mechanism 627 628 ``` 629 Register → ref_count++ 630 DeRegister → ref_count-- (DB sets time_zeroed when it hits 0) 631 ↓ 632 Background GC polls DB every 30s 633 ↓ 634 Finds sessions where time_zeroed IS NOT NULL 635 ↓ 636 Sends Kill to allocator → removes from HashMap, deletes DB row 637 ``` 638 639 This is how the system handles clients disconnecting at different times — the 640 actor only dies when the last client deregisters.