@@ -16,17 +16,82 @@ use hyperactor_macros::HandleClient;
16
16
use hyperactor_macros:: RefClient ;
17
17
use serde:: Deserialize ;
18
18
use serde:: Serialize ;
19
- use tokio:: sync:: mpsc:: Sender ;
20
19
21
20
use crate :: object:: GenericStateObject ;
21
+ use crate :: object:: Kind ;
22
+ use crate :: object:: LogSpec ;
23
+ use crate :: object:: LogState ;
24
+ use crate :: object:: Name ;
25
+ use crate :: object:: StateObject ;
26
+
27
+ pub trait LogHandler : Sync + Send + std:: fmt:: Debug + ' static {
28
+ // we cannot call it handle here as it conflicts with hyperactor macro
29
+ fn handle_log ( & self , logs : Vec < GenericStateObject > ) -> Result < ( ) > ;
30
+ }
31
+
32
+ /// A log handler that flushes GenericStateObject to stdout.
33
+ #[ derive( Debug ) ]
34
+ pub struct StdlogHandler ;
35
+
36
+ impl LogHandler for StdlogHandler {
37
+ fn handle_log ( & self , logs : Vec < GenericStateObject > ) -> Result < ( ) > {
38
+ for log in logs {
39
+ let metadata = log. metadata ( ) ;
40
+ let deserialized_data: StateObject < LogSpec , LogState > = log. data ( ) . deserialized ( ) ?;
41
+
42
+ // Deserialize the message and process line by line with UTF-8
43
+ let message_lines = deserialize_message_lines ( & deserialized_data. state . message ) ?;
44
+
45
+ // TODO: @lky D77377307 do not use raw string to distinguish between stdout and stderr
46
+ if metadata. kind != Kind :: Log {
47
+ continue ;
48
+ }
49
+ match & metadata. name {
50
+ Name :: StdoutLog ( ( hostname, pid) ) => {
51
+ for line in message_lines {
52
+ // TODO: @lky hostname and pid should only be printed for non-aggregated logs. =
53
+ // For aggregated logs, we should leave as is for better aggregation.
54
+ println ! ( "[{} {}] {}" , hostname, pid, line) ;
55
+ }
56
+ }
57
+ Name :: StderrLog ( ( hostname, pid) ) => {
58
+ for line in message_lines {
59
+ eprintln ! ( "[{} {}] {}" , hostname, pid, line) ;
60
+ }
61
+ }
62
+ }
63
+ }
64
+ Ok ( ( ) )
65
+ }
66
+ }
67
+
68
+ /// Deserialize a Serialized message and split it into UTF-8 lines
69
+ fn deserialize_message_lines (
70
+ serialized_message : & hyperactor:: data:: Serialized ,
71
+ ) -> Result < Vec < String > > {
72
+ // Try to deserialize as String first
73
+ if let Ok ( message_str) = serialized_message. deserialized :: < String > ( ) {
74
+ return Ok ( message_str. lines ( ) . map ( |s| s. to_string ( ) ) . collect ( ) ) ;
75
+ }
76
+
77
+ // If that fails, try to deserialize as Vec<u8> and convert to UTF-8
78
+ if let Ok ( message_bytes) = serialized_message. deserialized :: < Vec < u8 > > ( ) {
79
+ let message_str = String :: from_utf8 ( message_bytes) ?;
80
+ return Ok ( message_str. lines ( ) . map ( |s| s. to_string ( ) ) . collect ( ) ) ;
81
+ }
82
+
83
+ // If both fail, return an error
84
+ anyhow:: bail!( "Failed to deserialize message as either String or Vec<u8>" )
85
+ }
22
86
23
87
/// A client to interact with the state actor.
24
88
#[ derive( Debug ) ]
25
89
#[ hyperactor:: export(
26
90
handlers = [ ClientMessage ] ,
27
91
) ]
28
92
pub struct ClientActor {
29
- sender : Sender < Vec < GenericStateObject > > ,
93
+ // TODO: extend hyperactor macro to support a generic to avoid using Box here.
94
+ log_handler : Box < dyn LogHandler > ,
30
95
}
31
96
32
97
/// Endpoints for the client actor.
@@ -37,15 +102,17 @@ pub enum ClientMessage {
37
102
}
38
103
39
104
pub struct ClientActorParams {
40
- pub sender : Sender < Vec < GenericStateObject > > ,
105
+ pub log_handler : Box < dyn LogHandler > ,
41
106
}
42
107
43
108
#[ async_trait]
44
109
impl Actor for ClientActor {
45
110
type Params = ClientActorParams ;
46
111
47
- async fn new ( ClientActorParams { sender } : ClientActorParams ) -> Result < Self , anyhow:: Error > {
48
- Ok ( Self { sender } )
112
+ async fn new (
113
+ ClientActorParams { log_handler } : ClientActorParams ,
114
+ ) -> Result < Self , anyhow:: Error > {
115
+ Ok ( Self { log_handler } )
49
116
}
50
117
}
51
118
@@ -57,7 +124,7 @@ impl ClientMessageHandler for ClientActor {
57
124
_cx : & Context < Self > ,
58
125
logs : Vec < GenericStateObject > ,
59
126
) -> Result < ( ) , anyhow:: Error > {
60
- self . sender . send ( logs) . await ?;
127
+ self . log_handler . handle_log ( logs) ?;
61
128
Ok ( ( ) )
62
129
}
63
130
}
@@ -69,18 +136,38 @@ mod tests {
69
136
use hyperactor:: ActorRef ;
70
137
use hyperactor:: channel;
71
138
use hyperactor:: channel:: ChannelAddr ;
139
+ use hyperactor:: clock:: Clock ;
140
+ use hyperactor:: data:: Serialized ;
141
+ use tokio:: sync:: mpsc:: Sender ;
72
142
73
143
use super :: * ;
74
144
use crate :: create_remote_client;
75
145
use crate :: test_utils:: log_items;
76
146
use crate :: test_utils:: spawn_actor;
77
147
148
+ /// A log handler that flushes GenericStateObject to a mpsc channel.
149
+ #[ derive( Debug ) ]
150
+ struct MpscLogHandler {
151
+ sender : Sender < Vec < GenericStateObject > > ,
152
+ }
153
+
154
+ impl LogHandler for MpscLogHandler {
155
+ fn handle_log ( & self , logs : Vec < GenericStateObject > ) -> Result < ( ) > {
156
+ let sender = self . sender . clone ( ) ;
157
+ tokio:: spawn ( async move {
158
+ sender. send ( logs) . await . unwrap ( ) ;
159
+ } ) ;
160
+ Ok ( ( ) )
161
+ }
162
+ }
163
+
78
164
#[ tracing_test:: traced_test]
79
165
#[ tokio:: test]
80
166
async fn test_client_basics ( ) {
81
167
let client_actor_addr = ChannelAddr :: any ( channel:: ChannelTransport :: Unix ) ;
82
168
let ( sender, mut receiver) = tokio:: sync:: mpsc:: channel :: < Vec < GenericStateObject > > ( 10 ) ;
83
- let params = ClientActorParams { sender } ;
169
+ let log_handler = Box :: new ( MpscLogHandler { sender } ) ;
170
+ let params = ClientActorParams { log_handler } ;
84
171
let client_proc_id =
85
172
hyperactor:: reference:: ProcId ( hyperactor:: WorldId ( "client_server" . to_string ( ) ) , 0 ) ;
86
173
let ( client_actor_addr, client_actor_handle, _client_mailbox) = spawn_actor :: < ClientActor > (
@@ -102,7 +189,9 @@ mod tests {
102
189
. unwrap ( ) ;
103
190
104
191
// Collect received messages with timeout
105
- let fetched_logs = tokio:: time:: timeout ( Duration :: from_secs ( 1 ) , receiver. recv ( ) )
192
+ let clock = hyperactor:: clock:: ClockKind :: default ( ) ;
193
+ let fetched_logs = clock
194
+ . timeout ( Duration :: from_secs ( 1 ) , receiver. recv ( ) )
106
195
. await
107
196
. expect ( "timed out waiting for message" )
108
197
. expect ( "channel closed unexpectedly" ) ;
@@ -112,7 +201,53 @@ mod tests {
112
201
assert_eq ! ( fetched_logs, log_items_0_10) ;
113
202
114
203
// Now test that no extra message is waiting
115
- let extra = tokio:: time:: timeout ( Duration :: from_millis ( 100 ) , receiver. recv ( ) ) . await ;
204
+ let extra = clock
205
+ . timeout ( Duration :: from_millis ( 100 ) , receiver. recv ( ) )
206
+ . await ;
116
207
assert ! ( extra. is_err( ) , "expected no more messages" ) ;
117
208
}
209
+
210
+ #[ test]
211
+ fn test_deserialize_message_lines_string ( ) {
212
+ // Test deserializing a String message with multiple lines
213
+ let message = "Line 1\n Line 2\n Line 3" . to_string ( ) ;
214
+ let serialized = Serialized :: serialize_anon ( & message) . unwrap ( ) ;
215
+
216
+ let result = deserialize_message_lines ( & serialized) . unwrap ( ) ;
217
+
218
+ assert_eq ! ( result, vec![ "Line 1" , "Line 2" , "Line 3" ] ) ;
219
+
220
+ // Test deserializing a Vec<u8> message with UTF-8 content
221
+ let message_bytes = "Hello\n World\n UTF-8 \u{1F980} " . as_bytes ( ) . to_vec ( ) ;
222
+ let serialized = Serialized :: serialize_anon ( & message_bytes) . unwrap ( ) ;
223
+
224
+ let result = deserialize_message_lines ( & serialized) . unwrap ( ) ;
225
+
226
+ assert_eq ! ( result, vec![ "Hello" , "World" , "UTF-8 \u{1F980} " ] ) ;
227
+
228
+ // Test deserializing a single line message
229
+ let message = "Single line message" . to_string ( ) ;
230
+ let serialized = Serialized :: serialize_anon ( & message) . unwrap ( ) ;
231
+
232
+ let result = deserialize_message_lines ( & serialized) . unwrap ( ) ;
233
+
234
+ assert_eq ! ( result, vec![ "Single line message" ] ) ;
235
+
236
+ // Test deserializing an empty lines
237
+ let message = "\n \n " . to_string ( ) ;
238
+ let serialized = Serialized :: serialize_anon ( & message) . unwrap ( ) ;
239
+
240
+ let result = deserialize_message_lines ( & serialized) . unwrap ( ) ;
241
+
242
+ assert_eq ! ( result, vec![ "" , "" ] ) ;
243
+
244
+ // Test error handling for invalid UTF-8 bytes
245
+ let invalid_utf8_bytes = vec ! [ 0xFF , 0xFE , 0xFD ] ; // Invalid UTF-8 sequence
246
+ let serialized = Serialized :: serialize_anon ( & invalid_utf8_bytes) . unwrap ( ) ;
247
+
248
+ let result = deserialize_message_lines ( & serialized) ;
249
+
250
+ assert ! ( result. is_err( ) ) ;
251
+ assert ! ( result. unwrap_err( ) . to_string( ) . contains( "invalid utf-8" ) ) ;
252
+ }
118
253
}
0 commit comments