Skip to main content

agave_scheduling_utils/handshake/
server.rs

1use {
2    crate::handshake::{
3        AgaveHandshakeError, AgaveTpuToPackSession, AgaveWorkerSession, ClientLogon,
4        shared::{
5            AgaveSession, GLOBAL_ALLOCATORS, LOGON_FAILURE, LOGON_SUCCESS, MAX_ALLOCATOR_HANDLES,
6            MAX_WORKERS, VERSION,
7        },
8    },
9    agave_scheduler_bindings::PackToWorkerMessage,
10    nix::sys::socket::{self, ControlMessage, MsgFlags, UnixAddr},
11    rts_alloc::Allocator,
12    std::{
13        ffi::CStr,
14        fs::File,
15        io::{IoSlice, Read, Write},
16        os::{
17            fd::{AsRawFd, FromRawFd},
18            unix::net::{UnixListener, UnixStream},
19        },
20        path::Path,
21        time::{Duration, Instant},
22    },
23};
24
25type ShaqError = shaq::error::Error;
26type RtsAllocError = rts_alloc::error::Error;
27
28const HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(1);
29const SHMEM_NAME: &CStr = c"/agave-scheduler-bindings";
30
31/// Implements the Agave side of the scheduler bindings handshake protocol.
32pub struct Server {
33    listener: UnixListener,
34
35    buffer: [u8; 1024],
36}
37
38impl Server {
39    pub fn new(path: impl AsRef<Path>) -> Result<Self, std::io::Error> {
40        let listener = UnixListener::bind(path)?;
41
42        Ok(Self {
43            listener,
44            buffer: [0; 1024],
45        })
46    }
47
48    pub fn accept(&mut self) -> Result<AgaveSession, AgaveHandshakeError> {
49        // Wait for next stream.
50        let (mut stream, _) = self.listener.accept()?;
51        stream.set_read_timeout(Some(HANDSHAKE_TIMEOUT))?;
52
53        match self.handle_logon(&mut stream) {
54            Ok(session) => Ok(session),
55            Err(err) => {
56                let reason = err.to_string();
57                let reason_len = u8::try_from(reason.len()).unwrap_or(u8::MAX);
58
59                let buffer_len = 2usize.checked_add(usize::from(reason_len)).unwrap();
60                self.buffer[0] = LOGON_FAILURE;
61                self.buffer[1] = reason_len;
62                self.buffer[2..buffer_len]
63                    .copy_from_slice(&reason.as_bytes()[..usize::from(reason_len)]);
64
65                stream.set_nonblocking(true)?;
66                // NB: Caller will still error out even if our write fails so it's fine to ignore the
67                // result.
68                let _ = stream.write(&self.buffer[..buffer_len])?;
69
70                Err(err)
71            }
72        }
73    }
74
75    fn handle_logon(
76        &mut self,
77        stream: &mut UnixStream,
78    ) -> Result<AgaveSession, AgaveHandshakeError> {
79        // Receive & validate the logon message.
80        let logon = self.recv_logon(stream)?;
81
82        // Setup the requested shared memory regions.
83        let (session, files) = Self::setup_session(logon)?;
84
85        // Send the file descriptors to the client.
86        let fds_raw: Vec<_> = files.iter().map(|file| file.as_raw_fd()).collect();
87        let iov = [IoSlice::new(&[LOGON_SUCCESS])];
88        let cmsgs = [ControlMessage::ScmRights(&fds_raw)];
89        let sent =
90            socket::sendmsg::<UnixAddr>(stream.as_raw_fd(), &iov, &cmsgs, MsgFlags::empty(), None)
91                .map_err(std::io::Error::from)?;
92        debug_assert_eq!(sent, 1);
93
94        Ok(session)
95    }
96
97    fn recv_logon(&mut self, stream: &mut UnixStream) -> Result<ClientLogon, AgaveHandshakeError> {
98        // Read the logon message.
99        let handshake_start = Instant::now();
100        let mut buffer_len = 0;
101        while buffer_len < self.buffer.len() {
102            let read = stream.read(&mut self.buffer[buffer_len..])?;
103            if read == 0 {
104                return Err(AgaveHandshakeError::EofDuringHandshake);
105            }
106
107            // SAFETY: We cannot read a value greater than buffer.len() which itself is a usize.
108            buffer_len = buffer_len.checked_add(read).unwrap();
109
110            if handshake_start.elapsed() > HANDSHAKE_TIMEOUT {
111                return Err(AgaveHandshakeError::Timeout);
112            }
113        }
114
115        // Ensure exact version match, version will be bumped any time a backwards incompatible
116        // change is made to handshake/shared memory objects.
117        let version = u64::from_le_bytes(self.buffer[..8].try_into().unwrap());
118        if version != VERSION {
119            return Err(AgaveHandshakeError::Version {
120                server: VERSION,
121                client: version,
122            });
123        }
124
125        // Read the logon message, cannot panic as we ensure the correct buf size at compile time
126        // (hence the const just below).
127        const LOGON_END: usize = 8 + core::mem::size_of::<ClientLogon>();
128        let logon = ClientLogon::try_from_bytes(&self.buffer[8..LOGON_END]).unwrap();
129
130        // Put a hard limit of 64 worker threads for now.
131        if !(1..=MAX_WORKERS).contains(&logon.worker_count) {
132            return Err(AgaveHandshakeError::WorkerCount(logon.worker_count));
133        }
134
135        // Hard limit allocator handles to 128.
136        if !(1..=MAX_ALLOCATOR_HANDLES).contains(&logon.allocator_handles) {
137            return Err(AgaveHandshakeError::AllocatorHandles(
138                logon.allocator_handles,
139            ));
140        }
141
142        Ok(logon)
143    }
144
145    pub fn setup_session(
146        logon: ClientLogon,
147    ) -> Result<(AgaveSession, Vec<File>), AgaveHandshakeError> {
148        // Setup the allocator in shared memory (`worker_count` & `allocator_handles` have been
149        // validated so this won't panic).
150        let (allocator_file, tpu_to_pack_allocator) = Self::create_allocator(&logon)?;
151
152        // Setup the global queues.
153        let (tpu_to_pack_file, tpu_to_pack_queue) =
154            Self::create_producer(logon.tpu_to_pack_capacity, true)?;
155        let (progress_tracker_file, progress_tracker) =
156            Self::create_producer(logon.progress_tracker_capacity, false)?;
157
158        // Setup the worker sessions.
159        let (worker_files, workers) = (0..logon.worker_count).try_fold(
160            (Vec::default(), Vec::default()),
161            |(mut fds, mut workers), _| {
162                let allocator = Allocator::join(&allocator_file)?;
163
164                let (pack_to_worker_file, pack_to_worker) =
165                    Self::create_consumer(logon.pack_to_worker_capacity)?;
166                let (worker_to_pack_file, worker_to_pack) =
167                    Self::create_producer(logon.worker_to_pack_capacity, true)?;
168
169                fds.extend([pack_to_worker_file, worker_to_pack_file]);
170                workers.push(AgaveWorkerSession {
171                    allocator,
172                    pack_to_worker,
173                    worker_to_pack,
174                });
175
176                Ok::<_, AgaveHandshakeError>((fds, workers))
177            },
178        )?;
179
180        Ok((
181            AgaveSession {
182                flags: logon.flags,
183                tpu_to_pack: AgaveTpuToPackSession {
184                    allocator: tpu_to_pack_allocator,
185                    producer: tpu_to_pack_queue,
186                },
187                progress_tracker,
188                workers,
189            },
190            [allocator_file, tpu_to_pack_file, progress_tracker_file]
191                .into_iter()
192                .chain(worker_files)
193                .collect(),
194        ))
195    }
196
197    fn create_allocator(logon: &ClientLogon) -> Result<(File, Allocator), RtsAllocError> {
198        let allocator_count = GLOBAL_ALLOCATORS
199            .checked_add(logon.worker_count)
200            .unwrap()
201            .checked_add(logon.allocator_handles)
202            .unwrap();
203
204        let create = |huge: bool| {
205            let allocator_file = Self::create_shmem(huge)?;
206            let allocator_file_size = Self::align_file_size(logon.allocator_size, huge);
207
208            // SAFETY: We just created this file and thus can uniquely initialize it.
209            unsafe {
210                Allocator::create(
211                    &allocator_file,
212                    allocator_file_size,
213                    u32::try_from(allocator_count).unwrap(),
214                    2 * 1024 * 1024,
215                )
216            }
217            .map(|allocator| (allocator_file, allocator))
218        };
219
220        // Try to create with huge pages, fallback to regular pages.
221        create(true).or_else(|_| create(false))
222    }
223
224    fn create_producer<T>(
225        capacity: usize,
226        huge: bool,
227    ) -> Result<(File, shaq::spsc::Producer<T>), ShaqError> {
228        let create = |huge: bool| {
229            let file = Self::create_shmem(huge)?;
230            let minimum_file_size = shaq::spsc::minimum_file_size::<T>(capacity);
231            let file_size = Self::align_file_size(minimum_file_size, huge);
232
233            // SAFETY: uniqely creating as producer
234            unsafe { shaq::spsc::Producer::create(&file, file_size) }
235                .map(|producer| (file, producer))
236        };
237
238        // Try to create with huge pages, fallback to regular pages.
239        match huge {
240            true => create(true).or_else(|_| create(false)),
241            false => create(false),
242        }
243    }
244
245    fn create_consumer(
246        capacity: usize,
247    ) -> Result<(File, shaq::spsc::Consumer<PackToWorkerMessage>), ShaqError> {
248        let create = |huge: bool| {
249            let file = Self::create_shmem(huge)?;
250            let minimum_file_size = shaq::spsc::minimum_file_size::<PackToWorkerMessage>(capacity);
251            let file_size = Self::align_file_size(minimum_file_size, huge);
252
253            // SAFETY: uniquely creating as consumer.
254            unsafe { shaq::spsc::Consumer::create(&file, file_size) }
255                .map(|producer| (file, producer))
256        };
257
258        // Try to create with huge pages, fallback to regular pages.
259        create(true).or_else(|_| create(false))
260    }
261
262    #[cfg(any(
263        target_os = "linux",
264        target_os = "l4re",
265        target_os = "android",
266        target_os = "emscripten"
267    ))]
268    fn create_shmem(huge: bool) -> Result<File, std::io::Error> {
269        let flags = match huge {
270            true => libc::MFD_HUGETLB | libc::MFD_HUGE_2MB,
271            false => 0,
272        };
273
274        unsafe {
275            let ret = libc::memfd_create(SHMEM_NAME.as_ptr(), flags);
276            if ret == -1 {
277                return Err(std::io::Error::last_os_error());
278            }
279
280            Ok(File::from_raw_fd(ret))
281        }
282    }
283
284    #[cfg(not(any(
285        target_os = "linux",
286        target_os = "l4re",
287        target_os = "android",
288        target_os = "emscripten"
289    )))]
290    fn create_shmem(huge: bool) -> Result<File, std::io::Error> {
291        if huge {
292            return Err(std::io::ErrorKind::Unsupported.into());
293        }
294
295        unsafe {
296            // Clean up the previous link if one exists.
297            let ret = libc::shm_unlink(SHMEM_NAME.as_ptr());
298            if ret == -1 {
299                let err = std::io::Error::last_os_error();
300                if err.kind() != std::io::ErrorKind::NotFound {
301                    return Err(err);
302                }
303            }
304
305            // Create a new shared memory object.
306            let ret = libc::shm_open(
307                SHMEM_NAME.as_ptr(),
308                libc::O_CREAT | libc::O_EXCL | libc::O_RDWR,
309                #[cfg(not(target_os = "macos"))]
310                {
311                    libc::S_IRUSR | libc::S_IWUSR
312                },
313                #[cfg(any(target_os = "macos", target_os = "ios"))]
314                {
315                    (libc::S_IRUSR | libc::S_IWUSR) as libc::c_uint
316                },
317            );
318            if ret == -1 {
319                return Err(std::io::Error::last_os_error());
320            }
321            let file = File::from_raw_fd(ret);
322
323            // Clean up after ourself.
324            let ret = libc::shm_unlink(SHMEM_NAME.as_ptr());
325            if ret == -1 {
326                return Err(std::io::Error::last_os_error());
327            }
328
329            Ok(file)
330        }
331    }
332
333    fn align_file_size(size: usize, huge: bool) -> usize {
334        match huge {
335            true => size.next_multiple_of(2 * 1024 * 1024),
336            false => size.next_multiple_of(4096),
337        }
338    }
339}