gatehouse_runtime_protocol/lifecycle/journal/storage/
slot.rs1use super::*;
3
4#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
5#[serde(deny_unknown_fields)]
6struct Record {
7 schema: String,
8 fence: u64,
9 released: bool,
10 resolve: Request,
11 plan: CreatePlan,
12 healthcheck: Vec<String>,
13}
14impl Record {
15 fn validate(&self) -> Result<()> {
16 if self.schema != "gatehouse-lifecycle-service-slot/v1" || self.fence == 0 {
17 return Err(Invalid("invalid service slot schema/fence"));
18 }
19 self.plan.validate(&self.resolve)?;
20 if !self.healthcheck.is_empty() {
21 let mut health = self.resolve.clone();
22 health.operation = Operation::HealthExec {
23 target: Target {
24 object_id: "0".repeat(64),
25 name: self.plan.name.clone(),
26 labels: self.plan.labels.clone(),
27 container_port: self.plan.container_port,
28 },
29 argv: self.healthcheck.clone(),
30 timeout_ms: 30000,
31 };
32 health.binding.operation_key = health.operation_key()?;
33 health.validate()?;
34 }
35 Ok(())
36 }
37 fn bytes(&self) -> Result<Vec<u8>> {
38 self.validate()?;
39 canonical::encode(&serde_json::to_value(self).map_err(|_| Invalid("slot encoding failed"))?)
40 }
41 fn journal_name(&self) -> String {
42 format!("journal-{}", self.resolve.binding.lease_invocation)
43 }
44}
45
46pub struct ServiceSlot {
49 root: PathBuf,
50 directory: LockedDirectory,
51 record: Record,
52 bytes: Vec<u8>,
53 reserved: bool,
54}
55impl ServiceSlot {
56 pub fn acquire(
57 root: &Path,
58 resolve: Request,
59 plan: CreatePlan,
60 healthcheck: Vec<String>,
61 ) -> Result<Self> {
62 Self::acquire_with(root, resolve, plan, healthcheck, |_| Ok(()))
63 }
64 fn acquire_with(
65 root: &Path,
66 resolve: Request,
67 plan: CreatePlan,
68 healthcheck: Vec<String>,
69 mut stage: impl FnMut(u8) -> Result<()>,
70 ) -> Result<Self> {
71 let directory = Journal::directory(root)?;
72 let previous = Journal::read(&directory, "service.json")?;
73 if previous.is_none() {
74 for member in ["active", "lease.json", "reservation.json", "tombstone.json"] {
75 if Journal::read(&directory, member)?.is_some() {
76 return Err(Invalid(
77 "slot cannot adopt an existing standalone or legacy lease",
78 ));
79 }
80 }
81 }
82 let mut record = Record {
83 schema: "gatehouse-lifecycle-service-slot/v1".into(),
84 fence: 1,
85 released: false,
86 resolve,
87 plan,
88 healthcheck,
89 };
90 record.validate()?;
91 if let Some(bytes) = previous {
92 record.fence = Self::released_fence(root, &directory, &record, &bytes)?;
93 }
94 let name = CString::new(record.journal_name()).unwrap();
95 if Journal::at(
96 &directory,
97 name.to_str().unwrap(),
98 libc::O_RDONLY | libc::O_DIRECTORY,
99 )?
100 .is_some()
101 {
102 return Err(Invalid("service invocation reused"));
103 }
104 let bytes = record.bytes()?;
105 Journal::atomic_with(&directory, "service.json", &bytes, &mut stage)?;
107 stage(3)?;
108 if unsafe { libc::mkdirat(directory.as_raw_fd(), name.as_ptr(), 0o700) } != 0 {
110 return Err(Invalid("cannot provision slot journal; fence retained"));
111 }
112 io(directory.sync_all())?;
113 let slot = Self {
114 root: root.into(),
115 directory,
116 record,
117 bytes,
118 reserved: false,
119 };
120 slot.check()?;
121 Ok(slot)
122 }
123 fn released_fence(root: &Path, directory: &File, record: &Record, bytes: &[u8]) -> Result<u64> {
124 let old: Record = serde_json::from_value(canonical::parse_canonical(bytes)?)
125 .map_err(|_| Invalid("invalid service slot record"))?;
126 old.validate()?;
127 if !old.released
128 || old.resolve.binding.project_id != record.resolve.binding.project_id
129 || old.resolve.binding.resource_id != record.resolve.binding.resource_id
130 {
131 return Err(Invalid(
132 "service slot retained or belongs to another resource",
133 ));
134 }
135 let mut journal =
138 Journal::open_reserved(&root.join(old.journal_name()), &old.resolve, &old.plan)?;
139 journal.finish_release()?;
140 let archive = format!("service-history-{}.json", old.fence);
141 match Journal::read(directory, &archive)? {
142 Some(prior) if prior != bytes => return Err(Invalid("slot history changed")),
143 Some(_) => {}
144 None => Journal::atomic(directory, &archive, bytes)?,
145 }
146 let fence = old
147 .fence
148 .checked_add(1)
149 .ok_or(Invalid("slot fence exhausted"))?;
150 Ok(fence)
151 }
152 pub fn check(&self) -> Result<()> {
153 let now = io(fs::symlink_metadata(&self.root))?;
154 let held = io(self.directory.metadata())?;
155 private(&now, true)?;
156 if now.dev() != held.dev()
157 || now.ino() != held.ino()
158 || Journal::read(&self.directory, "service.json")?.as_deref()
159 != Some(self.bytes.as_slice())
160 {
161 return Err(Invalid("service slot changed outside lock"));
162 }
163 Ok(())
164 }
165 pub fn resolve_request(&self) -> &Request {
166 &self.record.resolve
167 }
168 pub fn plan(&self) -> &CreatePlan {
169 &self.record.plan
170 }
171 pub fn healthcheck(&self) -> &[String] {
172 &self.record.healthcheck
173 }
174 pub fn fence(&self) -> u64 {
175 self.record.fence
176 }
177 pub fn reserve(&mut self) -> Result<Reservation> {
178 self.check()?;
179 if self.reserved || self.record.released {
180 return Err(Invalid("slot reservation already consumed"));
181 }
182 self.reserved = true;
183 Reservation::create(
184 &self.root.join(self.record.journal_name()),
185 &self.record.resolve,
186 self.record.plan.clone(),
187 )
188 }
189 pub fn finish(&mut self, mut journal: Journal) -> Result<()> {
190 self.check()?;
191 let proof = journal
192 .record
193 .resolution
194 .as_ref()
195 .ok_or(Invalid("slot requires resolution proof"))?;
196 if journal.root != self.root.join(self.record.journal_name())
197 || proof.request != self.record.resolve
198 || proof.plan != self.record.plan
199 {
200 return Err(Invalid("journal does not belong to service slot"));
201 }
202 journal.finish_release()?;
203 let mut released = self.record.clone();
204 released.released = true;
205 let bytes = released.bytes()?;
206 Journal::atomic(&self.directory, "service.json", &bytes)?;
207 self.record = released;
208 self.bytes = bytes;
209 Ok(())
210 }
211}
212
213#[cfg(test)]
214mod tests;
215
216mod mapping;
217mod recovery;
218pub use recovery::OperatorRecovery;