notes

Log | Files | Refs | README

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.