fusee-server 1.0.0

Fusee server mode: the server holds the log, workflow code runs in the application
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
//! Storage and quorum permit sessions for a worker connected to a dedicated primary.
use super::*;

#[derive(Clone)]
pub(crate) struct ReplicaStorage {
    shared: Arc<Shared>,
}

impl ReplicaStorage {
    pub(crate) fn new(node: &ReplicaNode) -> Self {
        Self {
            shared: node.shared.clone(),
        }
    }

    pub(crate) fn status(&self) -> watch::Receiver<NodeStatus> {
        self.shared.status.subscribe()
    }

    fn epoch(&self) -> io::Result<u64> {
        let epoch = self.shared.epoch.load(Ordering::Acquire);
        if self.shared.status.borrow().ready {
            Ok(epoch)
        } else {
            Err(other("replica is not the ready primary"))
        }
    }

    /// A segment group's only shard (format 2 batches, materialized).
    pub(crate) fn open(&self, shard: u32, epoch: u64) -> io::Result<ReplicatedEnv> {
        if shard != 0 || self.epoch()? != epoch {
            return Err(other("replica cannot open this shard or epoch"));
        }
        if let Kind::Log(n) = self.shared.decide(Kind::Segments) {
            return Err(other(format!(
                "this group's log holds the {n} shards of an embedded engine; open it with \
                 ReplicaNode::engine_config, not as remote storage of a dedicated server"
            )));
        }
        let dir = self.shard_dir(0)?;
        let worker_lock = fusee::local::lock_directory(&dir)?;
        let local = LocalEnv::open(&dir, self.shared.local.clone())?;
        Ok(ReplicatedEnv::new(
            local,
            self.shared.clone(),
            epoch,
            Mode::Segments {
                slot: Arc::new(Mutex::new(Slot::default())),
                inflight: false,
            },
            dir,
            worker_lock,
        ))
    }

    /// Shard `shard` of a log group of `shards` (log format 3 sections).
    pub(crate) fn open_log(
        &self,
        shard: u32,
        shards: u32,
        epoch: u64,
    ) -> io::Result<ReplicatedEnv> {
        if shard >= shards || self.epoch()? != epoch {
            return Err(other("replica cannot open this shard or epoch"));
        }
        // The committed log as of now: an engine reopened after a clean
        // shutdown in the same primary epoch sees what the last one wrote.
        let (tx, rx) = std::sync::mpsc::sync_channel(1);
        self.shared
            .barriers
            .send(Barrier::Recovery { epoch, reply: tx })
            .map_err(|_| other("replica stopped"))?;
        let recovery = rx
            .recv()
            .map_err(|_| other("replica stopped before the engine opened"))??;
        let dir = self.shard_dir(shard)?;
        let worker_lock = fusee::local::lock_directory(&dir)?;
        let local = LocalEnv::open(&dir, self.shared.local.clone())?;
        Ok(ReplicatedEnv::new(
            local,
            self.shared.clone(),
            epoch,
            Mode::Log {
                index: shard,
                slot: Arc::new(log3::ShardSlot::default()),
                want: None,
                reported: 0,
                recovery: Some(recovery),
                resumed_ns: 0,
            },
            dir,
            worker_lock,
        ))
    }

    /// A standby shard of a log group: opened on any member, it waits for
    /// this replica to become the ready primary (see
    /// [`ReplicaNode::standby_config`]). Its run store and checkpoints are
    /// opened now; the committed log is taken at promotion.
    pub(super) fn open_standby(&self, shard: u32, standby: Standby) -> io::Result<ReplicatedEnv> {
        if shard >= standby.shards {
            return Err(other("replica cannot open this shard"));
        }
        if matches!(self.shared.kind(), Kind::Segments) {
            return Err(other(
                "a replicated segment group has no standby engine; open it with engine_config",
            ));
        }
        let dir = self.shard_dir(shard)?;
        let worker_lock = fusee::local::lock_directory(&dir)?;
        let local = LocalEnv::open(&dir, self.shared.local.clone())?;
        let mut env = ReplicatedEnv::new(
            local,
            self.shared.clone(),
            0,
            Mode::Log {
                index: shard,
                slot: Arc::new(log3::ShardSlot::default()),
                want: None,
                reported: 0,
                recovery: None,
                resumed_ns: 0,
            },
            dir,
            worker_lock,
        );
        env.standby = Some(standby);
        Ok(env)
    }

    fn shard_dir(&self, shard: u32) -> io::Result<PathBuf> {
        let dir = self
            .shared
            .engine_dir
            .lock()
            .map_err(|_| other("poisoned engine directory"))?
            .join(format!("shard-{shard:03}"));
        std::fs::create_dir_all(&dir)?;
        Ok(dir)
    }

    fn proposal(
        &self,
        epoch: u64,
        request: leases::Request,
        reply: impl FnOnce(io::Result<u64>) + Send + 'static,
    ) -> Proposal {
        Proposal {
            queued: self.shared.traffic.timings.start(),
            body: Arc::from([]),
            last_lsn: 0,
            epoch,
            control: Some(request),
            slot: Arc::new(Mutex::new(Slot {
                callback: Some(Box::new(reply)),
                ..Slot::default()
            })),
        }
    }

    /// For synchronous callers: waits on this thread while the queue is full.
    fn request(
        &self,
        epoch: u64,
        request: leases::Request,
        reply: impl FnOnce(io::Result<u64>) + Send + 'static,
    ) -> io::Result<()> {
        self.shared
            .proposals
            .send(self.proposal(epoch, request, reply))
            .map_err(|_| other("replica stopped"))
    }

    /// Proposes and waits for the committed answer without blocking the runtime.
    async fn ask(
        &self,
        epoch: u64,
        request: leases::Request,
        stopped: &'static str,
    ) -> io::Result<u64> {
        let (tx, rx) = oneshot::channel();
        let proposal = self.proposal(epoch, request, move |result| {
            let _ = tx.send(result);
        });
        self.shared
            .proposals
            .send_async(proposal)
            .await
            .map_err(|_| other("replica stopped"))?;
        rx.await.map_err(|_| other(stopped))?
    }

    /// Whether this replica is the ready primary and holds a read lease:
    /// no other member can lead a newer view before it lapses.
    pub(crate) fn lease_valid(&self) -> bool {
        self.shared.status.borrow().ready
            && now_ms() < self.shared.lease_until.load(Ordering::Acquire)
    }

    /// Returns once this primary has shown it still leads the group: at
    /// once under a read lease, else after a no-op commits through a quorum.
    /// A read of this replica's engine started after it returns sees every
    /// write the group acknowledged before the call.
    pub(crate) async fn confirm_quorum(&self) -> io::Result<()> {
        if self.lease_valid() {
            return Ok(());
        }
        self.probe().await
    }

    /// Commits a no-op through a quorum, lease or not.
    pub(crate) async fn probe(&self) -> io::Result<()> {
        self.ask(
            self.epoch()?,
            leases::Request::Probe,
            "primary stopped before quorum confirmation",
        )
        .await?;
        Ok(())
    }
    pub(crate) async fn claim(&self) -> io::Result<(WorkerSession, ReplicatedEnv)> {
        let epoch = self.epoch()?;
        let env = self.open(0, epoch)?;
        let session = self
            .ask(
                epoch,
                leases::Request::Claim,
                "replica stopped before worker claim",
            )
            .await?;
        Ok((
            WorkerSession {
                storage: self.clone(),
                epoch,
                session,
            },
            env,
        ))
    }
}

pub(crate) struct WorkerSession {
    storage: ReplicaStorage,
    epoch: u64,
    session: Op,
}

impl WorkerSession {
    pub(crate) fn request_limit(
        &self,
        operation: u8,
        name: String,
        ticket: u64,
        concurrent: u32,
        per_second: f64,
        priority: fusee::limits::Priority,
        tx: mpsc::UnboundedSender<io::Result<Vec<u8>>>,
    ) -> io::Result<()> {
        use crate::limits::op;
        match operation {
            op::DEFINE => fusee::limits::LimitSpec {
                concurrent,
                per_second,
            }
            .validate()
            .map_err(other)?,
            op::ACQUIRE | op::RELEASE if ticket > 0 => {}
            op::RENEW => {}
            _ => return Err(other("invalid replicated limit operation or ticket")),
        }
        let per_second = if operation == op::DEFINE {
            per_second
        } else {
            0.0
        };
        let reply = move |result: io::Result<u64>| {
            let result = result.map(|answer| {
                let mut frame = Vec::new();
                if operation == crate::limits::op::ACQUIRE && answer != 0 {
                    use fusee::limits::answer::{Answer, decode};
                    let (granted, delay) = match decode(ticket, answer) {
                        Answer::Granted(t) => (Some(t), 0),
                        Answer::RetryAfter(d) => (None, d),
                        Answer::Queued => (None, 0),
                    };
                    let mut b = Vec::new();
                    put_u64(&mut b, granted.unwrap_or(ticket));
                    b.push(u8::from(granted.is_some()));
                    put_u64(
                        &mut b,
                        if delay > 0 {
                            fusee::local::unix_ms().saturating_add(delay)
                        } else {
                            0
                        },
                    );
                    put_u64(&mut b, delay);
                    frame::encode(proto::resp::LIMITED, &b, &mut frame);
                }
                frame
            });
            if !matches!(&result,Ok(bytes) if bytes.is_empty()) {
                let _ = tx.send(result);
            }
        };
        let command = leases::Command::new(operation, name, ticket, concurrent, per_second)
            .with_priority(priority);
        self.storage.request(
            self.epoch,
            leases::Request::Worker(self.session, command),
            reply,
        )
    }
}

impl ReplicatedEnv {
    pub(crate) fn local(&mut self) -> &mut LocalEnv {
        &mut self.local
    }

    /// A standby shard: waits up to `timeout` for this replica to become
    /// the ready primary, then takes the committed log batches from the
    /// I/O thread's memory and runs in that primary epoch. `Ok(false)`: not
    /// yet. An error ends the shard: the replica stopped, or the engine
    /// cannot run (it ran in an earlier epoch, or the group's log holds
    /// another engine).
    pub(super) fn wait_promotion(&mut self, timeout: Duration) -> io::Result<bool> {
        let Some(standby) = &self.standby else {
            return Ok(true);
        };
        let deadline = Instant::now() + timeout;
        let epoch = {
            let mut guard = self.shared.role.0.lock().unwrap_or_else(|e| e.into_inner());
            loop {
                // Read under the lock the status sender notifies under, so a
                // promotion between the read and the wait is not missed.
                let s = *self.shared.status.borrow();
                if s.stopped || s.retired {
                    return Err(other("replica stopped"));
                }
                if s.ready {
                    break self.shared.epoch.load(Ordering::Acquire);
                }
                let left = deadline.saturating_duration_since(Instant::now());
                if left.is_zero() {
                    return Ok(false);
                }
                guard = self
                    .shared
                    .role
                    .1
                    .wait_timeout(guard, left)
                    .unwrap_or_else(|e| e.into_inner())
                    .0;
            }
        };
        // One primary epoch for the whole engine, its permits included.
        if let Err(was) =
            standby
                .tenure
                .compare_exchange(0, epoch, Ordering::AcqRel, Ordering::Acquire)
            && was != epoch
        {
            return Err(other(
                "this standby engine ran in an earlier primary epoch; open another",
            ));
        }
        match self.shared.decide(Kind::Log(standby.shards)) {
            Kind::Log(n) if n == standby.shards => {}
            _ => {
                return Err(other(
                    "the group's log was written by an engine with another layout or shard count",
                ));
            }
        }
        let (tx, rx) = std::sync::mpsc::sync_channel(1);
        self.shared
            .barriers
            .send(Barrier::Recovery { epoch, reply: tx })
            .map_err(|_| other("replica stopped"))?;
        let recovery = rx
            .recv()
            .map_err(|_| other("replica stopped before the shard started"))??;
        // A checkpoint image installed while this shard stood by moved the
        // engine directory: open the new one.
        let Mode::Log { index, .. } = self.mode else {
            return Err(other("a replicated segment group's shard has no standby"));
        };
        let dir = ReplicaStorage {
            shared: self.shared.clone(),
        }
        .shard_dir(index)?;
        if dir != self.shard {
            self._worker_lock = fusee::local::lock_directory(&dir)?;
            self.local = LocalEnv::open(&dir, self.shared.local.clone())?;
            self.shard = dir;
        }
        if let Mode::Log { recovery: r, .. } = &mut self.mode {
            *r = Some(recovery);
        }
        self.epoch = epoch;
        self.standby = None;
        Ok(true)
    }
}