okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
0a3aefa
main
1%% Inspect real stream owners while publishers fill an unread TCP connection. 2- module ( albedo_stream_pressure_probe ). 3- export ([ pressure_json / 1 , subscriptions_json / 0 , removed_json / 0 , 4progress_json / 1 , session_pressure_json / 1 , session_removed_json / 1 , burst_json / 3 , 5suspend_session_json / 1 , resume_session_json / 1 , worker_exit_json / 3 , maintenance_json / 1 ]). 6 7rows ( Table ) -> 8case ets : whereis ( Table ) of 9undefined -> []; 10_ -> ets : tab2list ( Table ) 11end . 12 13bytes ( Binary ) when is_binary ( Binary ) -> byte_size ( Binary ); 14bytes ( Map ) when is_map ( Map ) -> bytes ( maps : to_list ( Map )); 15bytes ( Tuple ) when is_tuple ( Tuple ) -> bytes ( tuple_to_list ( Tuple )); 16bytes ([ Head | Tail ]) -> bytes ( Head ) + bytes ( Tail ); 17bytes ( _ ) -> 0 . 18 19blocked ( Owner ) -> 20case process_info ( Owner , current_stacktrace ) of 21{ current_stacktrace , Frames } -> 22case lists : any ( fun ({ _ , Function , _ , _ }) -> 23lists : member ( Function , [ send , send_event , port_command ]) 24end , Frames ) of true -> 1 ; false -> 0 end ; 25undefined -> 0 26end . 27 28sample ({ Ref , { Owner , _ , Control , _ }}) -> 29{ Count , Bytes } = case ets : lookup ( albedo_bus_queues , Ref ) of 30[{ _ , { _ , C , B , _ }}] -> { C , B }; 31[] -> { 0 , 0 } 32end , 33case process_info ( Owner , [ messages , message_queue_len ]) of 34undefined -> # { count =>Count , bytes =>Bytes , mailbox =>0 , 35mailbox_bytes =>0 , blocked =>0 , overflow =>atomics : get ( Control , 1 )}; 36Info -> # { count =>Count , bytes =>Bytes , 37mailbox =>proplists : get_value ( message_queue_len , Info ), 38mailbox_bytes =>bytes ( proplists : get_value ( messages , Info )), 39blocked =>blocked ( Owner ), overflow =>atomics : get ( Control , 1 )} 40end . 41 42measure () -> 43lists : foldl ( fun ( Row , Peak ) -> 44maps : merge_with ( fun ( _ , A , B ) -> max ( A , B ) end , Peak , sample ( Row )) 45end , # { count =>0 , bytes =>0 , mailbox =>0 , mailbox_bytes =>0 , blocked =>0 , overflow =>0 }, rows ( albedo_bus )). 46 47activity_event ( Id , Index , Size ) -> 48Prefix = iolist_to_binary ([<< "pressure:" >>, Id , << ":" >>, integer_to_binary ( Index ), << ":" >>]), 49Remaining = max ( 0 , min ( 256 - byte_size ( Prefix ), Size )), 50Scalar = case Size > 4096 of true -> << 240 , 159 , 152 , 128 >>; false -> << "x" >> end , 51Text = << Prefix / binary , ( binary : copy ( Scalar , Remaining )) / binary >>, 52Line = # { kind =><< "assistant" >>, text =>Text }, 53json : encode ( # { type =><< "activity" >>, data =># { session_id =>Id , 54cursor =># { generation =><< "AAAAAAAAAAAAAAAAAAAAAA" >>, sequence =>Index }, 55status =># { phase =><< "idle" >>, run_id =>null , 56interrupt_requested =>false , blocking_reason =>null }, 57current_progress =>[], activity =># { lines =>lists : duplicate ( 12 , Line ), 58output_scalars =>Index , output_utf8_bytes =>Index , 59observed_at =><< "2026-10-03T00:00:00Z" >>, 60latest_input =>null , latest_answer =>null }}}). 61 62publish ( Ids , Start , Count , Size ) -> 63lists : foreach ( fun ( Index ) -> 64Id = lists : nth ( 1 + (( Index - 1 ) rem length ( Ids )), Ids ), 65albedo_bus : publish ( iolist_to_binary ( activity_event ( Id , Index , Size ))) 66end , lists : seq ( Start , Start + Count - 1 )), 67nil . 68 69burst_event ( _Id , Size ) when Size > 1048576 -> 70%% Internal admission boundary: this oversized payload is deliberately not 71%% a valid public event and must never reach a socket. 72json : encode ( # { type =><< "invalidate" >>, data =># { padding =>binary : copy (<< "x" >>, Size )}}); 73burst_event ( Id , Size ) when Size =< 16 -> 74json : encode ( # { type =><< "invalidate" >>, data =># { session_ids =>[ Id ], 75urls =>[<< "/sessions/" , Id / binary >>], scope_dirty =>false }}); 76burst_event ( Id , Size ) -> activity_event ( Id , 1 , Size ). 77 78publish_batch ( Ids , Published ) -> 79Workers = [ spawn_monitor ( fun () -> 80publish ([ Id ], Published div length ( Ids ) + 1 , 12 , 4096 ) 81end ) || Id <- Ids ], 82lists : foreach ( fun ({ Worker , Monitor }) -> 83receive 84{ 'DOWN' , Monitor , process , Worker , normal } -> ok ; 85{ 'DOWN' , Monitor , process , Worker , Reason } -> error ({ publisher_failed , Reason }) 86after 10000 -> error ( publisher_blocked ) 87end 88end , Workers ). 89 90pressure_json ( Ids ) -> 91io_lib : format ( "~s" , [ json : encode ( pressure ( Ids , 0 , measure ()))]). 92 93pressure ( Ids , Published , Peak ) when Published < 10000 -> 94publish_batch ( Ids , Published ), 95Current = measure (), 96Next = maps : merge_with ( fun ( _ , A , B ) -> max ( A , B ) end , Peak , Current ), 97case maps : get ( overflow , Current ) > 0 andalso maps : get ( blocked , Next ) > 0 of 98true -> Next # { published =>Published + 96 , pressure =>true }; 99false -> 100%% Pace below a responsive subscriber's per-flush queue budget. 101receive after 150 -> ok end , 102pressure ( Ids , Published + 96 , Next ) 103end ; 104pressure ( _ , Published , Peak ) -> Peak # { published =>Published , pressure =>false }. 105 106burst_json ( Ids , Count , Size ) -> 107[{ _ , { Owner , _ , _ , _ }}] = rows ( albedo_bus ), 108%% Hold the real stream handler at a barrier while testing each budget. 109ok = sys : suspend ( Owner ), 110try 111Peak = lists : foldl ( fun ( _ , Acc ) -> 112[ Id | _ ] = Ids , 113albedo_bus : publish ( iolist_to_binary ( burst_event ( Id , Size ))), 114maps : merge_with ( fun ( _ , A , B ) -> max ( A , B ) end , Acc , measure ()) 115end , measure (), lists : seq ( 1 , Count )), 116json : encode ( Peak ) 117after sys : resume ( Owner ) 118end . 119 120subscriptions_json () -> json : encode ( # { subscribers =>length ( rows ( albedo_bus )), 121queues =>length ( rows ( albedo_bus_queues ))}). 122 123removed_json () -> removed ( 1000 ). 124removed ( 0 ) -> subscriptions_json (); 125removed ( Left ) -> 126case { rows ( albedo_bus ), rows ( albedo_bus_queues )} of 127{[], []} -> subscriptions_json (); 128_ -> receive after 10 -> ok end , removed ( Left - 1 ) 129end . 130 131session_state ( Id ) -> 132{ some , Session } = 'albedo@daemon@session' : live ( Id ), 133{ ok , Owner } = 'gleam@erlang@process' : subject_owner ( Session ), 134sys : get_state ( Owner ). 135 136wakes ( wake ) -> 1 ; 137wakes ( Tuple ) when is_tuple ( Tuple ) -> wakes ( tuple_to_list ( Tuple )); 138wakes ([ Head | Tail ]) -> wakes ( Head ) + wakes ( Tail ); 139wakes ( _ ) -> 0 . 140 141session_sample ( Id ) -> 142State = session_state ( Id ), 143Watchers = 'albedo@daemon@session_diagnostics' : watcher_owners ( State ), 144Samples = [ case process_info ( Watcher , messages ) of 145undefined -> # { wakes =>0 , blocked =>0 }; 146{ messages , Messages } -> # { wakes =>wakes ( Messages ), blocked =>blocked ( Watcher )} 147end || Watcher <- Watchers ], 148Peak = lists : foldl ( fun ( Sample , Acc ) -> 149maps : merge_with ( fun ( _ , A , B ) -> max ( A , B ) end , Acc , Sample ) 150end , # { wakes =>0 , blocked =>0 }, Samples ), 151Peak # { watchers =>length ( Watchers ), sequence =>'albedo@daemon@session_diagnostics' : sequence ( State )}. 152 153session_pressure_json ( Id ) -> json : encode ( session_pressure ( Id , 1500 , # {})). 154session_pressure ( Id , Left , Peak ) -> 155Sample = session_sample ( Id ), 156Next = maps : merge_with ( fun ( _ , A , B ) -> max ( A , B ) end , Peak , Sample ), 157Complete = maps : get ( blocked , Next ) > 0 andalso not 'albedo@daemon@bus' : is_running ( Id ), 158case Complete orelse Left =:= 0 of 159true -> Next ; 160false -> receive after 10 -> ok end , session_pressure ( Id , Left - 1 , Next ) 161end . 162 163session_removed_json ( Id ) -> json : encode ( session_removed ( Id , 1000 )). 164session_removed ( Id , Left ) -> 165Sample = session_sample ( Id ), 166case maps : get ( watchers , Sample ) =:= 0 orelse Left =:= 0 of 167true -> Sample ; 168false -> receive after 10 -> ok end , session_removed ( Id , Left - 1 ) 169end . 170 171%% The actor owns one projection regardless of subscriber count. Inspect that 172%% bounded value rather than copying the worker's full durable argument state. 173progress_json ( Id ) -> 174State = session_state ( Id ), 175Projection = 'albedo@daemon@session_diagnostics' : progress ( State ), 176json : encode ( # { projection_bytes =>bytes ( Projection ), 177projection_size =>erlang : external_size ( Projection ), 178watchers =>length ( 'albedo@daemon@session_diagnostics' : watcher_owners ( State ))}). 179 180%% sys calls acknowledge suspension/resumption of the real session owner. 181suspend_session_json ( Id ) -> 182{ some , Session } = 'albedo@daemon@session' : live ( Id ), 183{ ok , Owner } = 'gleam@erlang@process' : subject_owner ( Session ), 184ok = sys : suspend ( Owner ), 185json : encode ( # { suspended =>true }). 186 187resume_session_json ( Id ) -> 188{ some , Session } = 'albedo@daemon@session' : live ( Id ), 189{ ok , Owner } = 'gleam@erlang@process' : subject_owner ( Session ), 190ok = sys : resume ( Owner ), 191json : encode ( # { resumed =>true }). 192 193%% The ready file acknowledges the monitor before the HTTP client cancels. 194worker_exit_json ( Id , RunId , ReadyPath ) -> 195State = session_state ( Id ), 196{ some , Worker } = 'albedo@daemon@session_diagnostics' : worker ( State , RunId ), 197Monitor = monitor ( process , Worker ), 198true = is_process_alive ( Worker ), 199ok = file : write_file ( ReadyPath , << "monitor_ready" >>), 200receive 201{ 'DOWN' , Monitor , process , Worker , Reason } -> 202Classification = case Reason of 203normal -> << "normal" >>; 204killed -> << "killed" >>; 205_ -> << "other" >> 206end , 207json : encode ( # { reason =>Classification }) 208after 30000 -> 209demonitor ( Monitor , [ flush ]), 210error ( worker_still_alive ) 211end . 212 213%% Hold a real owner across several timer ticks, then let the sweep encounter 214%% its death. Calls are traced at the worker boundary, without reading registry 215%% state or assuming where its maintenance field is stored. 216maintenance_json ( Id ) -> 217{ some , Session } = 'albedo@daemon@session' : live ( Id ), 218{ ok , Owner } = 'gleam@erlang@process' : subject_owner ( Session ), 219Module = 'albedo@daemon@maintenance' , 220{ module , Module } = code : ensure_loaded ( Module ), 221%% The registry admits one maintenance worker at a time, so the probe 222%% observes the run slot empty before it suspends anything: a worker 223%% already inside run would hold the admission slot across the suspend, 224%% its call untraced, and the entry window would see no run at all. 225%% Tracing goes on first, so every run call is traced from the wait on. 226erlang : trace_pattern ({ Module , run , 1 }, true , [ local ]), 227erlang : trace ( all , true , [ call , { tracer , self ()}]), 228try 229await_run_slot ( Module , erlang : monotonic_time ( millisecond ) + 15000 ), 230erlang : suspend_process ( Owner ), 231Worker = receive 232{ trace , Pid , call , { Module , run , [ _ ]}} -> Pid 233after 3000 -> error ( maintenance_not_started ) 234end , 235Monitor = monitor ( process , Worker ), 236Deadline = erlang : monotonic_time ( millisecond ) + 2000 , 237Workers = maintenance_calls ( Module , Deadline , [ Worker ]), 238exit ( Owner , kill ), 239Outcome = receive 240{ 'DOWN' , Monitor , process , Worker , normal } -> << "completed" >>; 241{ 'DOWN' , Monitor , process , Worker , Reason } -> error ({ sweep_failed , Reason }) 242after 7000 -> error ( maintenance_still_blocked ) 243end , 244json : encode ( # { admitted =>length ( lists : usort ( Workers )), outcome =>Outcome }) 245after 246erlang : trace ( all , false , [ call ]), 247erlang : trace_pattern ({ Module , run , 1 }, false , [ local ]), 248try erlang : resume_process ( Owner ) catch _ : _ -> ok end 249end . 250 251maintenance_calls ( Module , Deadline , Workers ) -> 252Remaining = max ( 0 , Deadline - erlang : monotonic_time ( millisecond )), 253receive 254{ trace , Pid , call , { Module , run , [ _ ]}} -> 255maintenance_calls ( Module , Deadline , [ Pid | Workers ]) 256after Remaining -> Workers 257end . 258 259%% No process is inside maintenance:run/1 when the probe suspends the owner. 260%% Calls traced while waiting belong to workers that finished before the 261%% suspend and are drained uncounted: the admission window starts at the 262%% suspend. A call that slips past the last drain comes from a worker at run 263%% entry by construction, so it still meets the held owner. 264await_run_slot ( Module , Deadline ) -> 265drain_run_calls ( Module ), 266case run_slot_busy ( Module ) of 267false -> ok ; 268true -> 269case erlang : monotonic_time ( millisecond ) >= Deadline of 270true -> error ( maintenance_worker_stuck ); 271false -> 272receive after 50 -> ok end , 273await_run_slot ( Module , Deadline ) 274end 275end . 276 277drain_run_calls ( Module ) -> 278receive 279{ trace , _ , call , { Module , run , [ _ ]}} -> drain_run_calls ( Module ) 280after 0 -> ok 281end . 282 283run_slot_busy ( Module ) -> 284lists : any ( fun ( Pid ) -> in_run ( Pid , Module ) end , erlang : processes ()). 285 286in_run ( Pid , Module ) -> 287case process_info ( Pid , current_stacktrace ) of 288{ current_stacktrace , Frames } -> 289lists : any ( fun ({ M , F , A , _ }) -> { M , F , A } =:= { Module , run , 1 } end , 290Frames ); 291undefined -> false 292end .