1use crate::newtype_uuid;
16use bincode::{Decode, Encode};
17use std::collections::HashSet;
18use std::fmt::{Display, Formatter};
19use std::str::FromStr;
20use uuid::Uuid;
21
22newtype_uuid!(
23 ComponentId,
24 golem_api_grpc::proto::golem::component::ComponentId
25);
26
27newtype_uuid!(ProjectId, golem_api_grpc::proto::golem::common::ProjectId);
28
29newtype_uuid!(PluginId, golem_api_grpc::proto::golem::component::PluginId);
30
31newtype_uuid!(
32 PluginInstallationId,
33 golem_api_grpc::proto::golem::common::PluginInstallationId
34);
35
36newtype_uuid!(PlanId, golem_api_grpc::proto::golem::plan::PlanId);
37newtype_uuid!(
38 ProjectGrantId,
39 golem_api_grpc::proto::golem::projectgrant::ProjectGrantId
40);
41newtype_uuid!(
42 ProjectPolicyId,
43 golem_api_grpc::proto::golem::projectpolicy::ProjectPolicyId
44);
45newtype_uuid!(TokenId, golem_api_grpc::proto::golem::token::TokenId);
46
47#[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord, Hash, Encode, Decode)]
48#[cfg_attr(feature = "model", derive(serde::Serialize, serde::Deserialize))]
49#[cfg_attr(feature = "poem", derive(poem_openapi::Object))]
50#[cfg_attr(feature = "poem", oai(rename_all = "camelCase"))]
51#[cfg_attr(feature = "model", serde(rename_all = "camelCase"))]
52pub struct ShardId {
53 pub(crate) value: i64,
54}
55
56impl ShardId {
57 pub fn new(value: i64) -> Self {
58 Self { value }
59 }
60
61 pub fn from_worker_id(worker_id: &WorkerId, number_of_shards: usize) -> Self {
62 let hash = Self::hash_worker_id(worker_id);
63 let value = hash.abs() % number_of_shards as i64;
64 Self { value }
65 }
66
67 pub fn hash_worker_id(worker_id: &WorkerId) -> i64 {
68 let (high_bits, low_bits) = (
69 (worker_id.component_id.0.as_u128() >> 64) as i64,
70 worker_id.component_id.0.as_u128() as i64,
71 );
72 let high = Self::hash_string(&high_bits.to_string());
73 let worker_name = &worker_id.worker_name;
74 let component_worker_name = format!("{low_bits}{worker_name}");
75 let low = Self::hash_string(&component_worker_name);
76 ((high as i64) << 32) | ((low as i64) & 0xFFFFFFFF)
77 }
78
79 fn hash_string(string: &str) -> i32 {
80 let mut hash = 0;
81 if hash == 0 && !string.is_empty() {
82 for val in &mut string.bytes() {
83 hash = 31_i32.wrapping_mul(hash).wrapping_add(val as i32);
84 }
85 }
86 hash
87 }
88
89 pub fn is_left_neighbor(&self, other: &ShardId) -> bool {
90 other.value == self.value + 1
91 }
92}
93
94impl Display for ShardId {
95 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
96 write!(f, "<{}>", self.value)
97 }
98}
99
100#[cfg(feature = "model")]
101impl golem_wasm_rpc::IntoValue for ShardId {
102 fn into_value(self) -> golem_wasm_rpc::Value {
103 golem_wasm_rpc::Value::S64(self.value)
104 }
105
106 fn get_type() -> golem_wasm_ast::analysis::AnalysedType {
107 golem_wasm_ast::analysis::analysed_type::s64()
108 }
109}
110
111pub type ComponentVersion = u64;
112
113#[derive(Clone, Debug, Eq, PartialEq, Hash, Encode, Decode)]
114#[cfg_attr(feature = "model", derive(serde::Serialize, serde::Deserialize))]
115#[cfg_attr(feature = "poem", derive(poem_openapi::Object))]
116#[cfg_attr(feature = "poem", oai(rename_all = "camelCase"))]
117#[cfg_attr(feature = "model", serde(rename_all = "camelCase"))]
118pub struct WorkerId {
119 pub component_id: ComponentId,
120 pub worker_name: String,
121}
122
123impl WorkerId {
124 pub fn to_redis_key(&self) -> String {
125 format!("{}:{}", self.component_id.0, self.worker_name)
126 }
127
128 pub fn to_worker_urn(&self) -> String {
129 format!("urn:worker:{}/{}", self.component_id, self.worker_name)
130 }
131
132 pub fn into_target_worker_id(self) -> TargetWorkerId {
134 TargetWorkerId {
135 component_id: self.component_id,
136 worker_name: Some(self.worker_name),
137 }
138 }
139
140 pub fn validate_worker_name(name: &str) -> Result<(), &'static str> {
141 let length = name.len();
142 if !(1..=512).contains(&length) {
143 Err("Worker name must be between 1 and 512 characters")
144 } else if name.chars().any(|c| c.is_whitespace()) {
145 Err("Worker name must not contain whitespaces")
146 } else if name.contains('/') {
147 Err("Worker name must not contain '/'")
148 } else if name.starts_with('-') {
149 Err("Worker name must not start with '-'")
150 } else {
151 Ok(())
152 }
153 }
154}
155
156impl FromStr for WorkerId {
157 type Err = String;
158
159 fn from_str(s: &str) -> Result<Self, Self::Err> {
160 let parts: Vec<&str> = s.split(':').collect();
161 if parts.len() == 2 {
162 let component_id_uuid = Uuid::from_str(parts[0])
163 .map_err(|_| format!("invalid component id: {s} - expected uuid"))?;
164 let component_id = ComponentId(component_id_uuid);
165 let worker_name = parts[1].to_string();
166 Ok(Self {
167 component_id,
168 worker_name,
169 })
170 } else {
171 Err(format!(
172 "invalid worker id: {s} - expected format: <component_id>:<worker_name>"
173 ))
174 }
175 }
176}
177
178impl Display for WorkerId {
179 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
180 f.write_str(&format!("{}/{}", self.component_id, self.worker_name))
181 }
182}
183
184impl AsRef<WorkerId> for &WorkerId {
185 fn as_ref(&self) -> &WorkerId {
186 self
187 }
188}
189
190#[cfg(feature = "model")]
191impl golem_wasm_rpc::IntoValue for WorkerId {
192 fn into_value(self) -> golem_wasm_rpc::Value {
193 golem_wasm_rpc::Value::Record(vec![
194 self.component_id.into_value(),
195 self.worker_name.into_value(),
196 ])
197 }
198
199 fn get_type() -> golem_wasm_ast::analysis::AnalysedType {
200 use golem_wasm_ast::analysis::analysed_type::{field, record};
201 record(vec![
202 field("component_id", ComponentId::get_type()),
203 field("worker_name", String::get_type()),
204 ])
205 }
206}
207
208#[derive(Clone, Debug, Eq, PartialEq, Hash, Encode, Decode)]
209#[cfg_attr(feature = "model", derive(serde::Serialize, serde::Deserialize))]
210pub struct TargetWorkerId {
211 pub component_id: ComponentId,
212 pub worker_name: Option<String>,
213}
214
215impl TargetWorkerId {
216 pub fn try_into_worker_id(self) -> Option<WorkerId> {
218 self.worker_name.map(|worker_name| WorkerId {
219 component_id: self.component_id,
220 worker_name,
221 })
222 }
223
224 pub fn into_worker_id(
230 self,
231 force_in_shard: &HashSet<ShardId>,
232 number_of_shards: usize,
233 ) -> WorkerId {
234 let TargetWorkerId {
235 component_id,
236 worker_name,
237 } = self;
238 match worker_name {
239 Some(worker_name) => WorkerId {
240 component_id,
241 worker_name,
242 },
243 None => {
244 if force_in_shard.is_empty() || number_of_shards == 0 {
245 let worker_name = Uuid::new_v4().to_string();
246 WorkerId {
247 component_id,
248 worker_name,
249 }
250 } else {
251 let mut current = Uuid::new_v4().to_u128_le();
252 loop {
253 let uuid = Uuid::from_u128_le(current);
254 let worker_name = uuid.to_string();
255 let worker_id = WorkerId {
256 component_id: component_id.clone(),
257 worker_name,
258 };
259 let shard_id = ShardId::from_worker_id(&worker_id, number_of_shards);
260 if force_in_shard.contains(&shard_id) {
261 return worker_id;
262 }
263 current += 1;
264 }
265 }
266 }
267 }
268 }
269
270 pub fn parse_worker_urn(urn: &str) -> Option<TargetWorkerId> {
272 if !urn.starts_with("urn:worker:") {
273 None
274 } else {
275 let remaining = &urn[11..];
276 let parts: Vec<&str> = remaining.split('/').collect();
277 match parts.len() {
278 2 => {
279 let component_id = ComponentId::from_str(parts[0]).ok()?;
280 let worker_name = parts[1];
281 Some(TargetWorkerId {
282 component_id,
283 worker_name: Some(worker_name.to_string()),
284 })
285 }
286 1 => {
287 let component_id = ComponentId::from_str(parts[0]).ok()?;
288 Some(TargetWorkerId {
289 component_id,
290 worker_name: None,
291 })
292 }
293 _ => None,
294 }
295 }
296 }
297}
298
299impl Display for TargetWorkerId {
300 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
301 match &self.worker_name {
302 Some(worker_name) => write!(f, "{}/{}", self.component_id, worker_name),
303 None => write!(f, "{}/*", self.component_id),
304 }
305 }
306}
307
308impl From<WorkerId> for TargetWorkerId {
309 fn from(value: WorkerId) -> Self {
310 value.into_target_worker_id()
311 }
312}
313
314impl From<&WorkerId> for TargetWorkerId {
315 fn from(value: &WorkerId) -> Self {
316 value.clone().into_target_worker_id()
317 }
318}
319
320#[derive(Clone, Debug, Eq, PartialEq, Hash, Encode, Decode)]
321#[cfg_attr(
322 feature = "model",
323 derive(serde::Serialize, serde::Deserialize, golem_wasm_rpc_derive::IntoValue)
324)]
325#[cfg_attr(feature = "poem", derive(poem_openapi::Object))]
326#[cfg_attr(feature = "poem", oai(rename_all = "camelCase"))]
327#[cfg_attr(feature = "model", serde(rename_all = "camelCase"))]
328pub struct PromiseId {
329 pub worker_id: WorkerId,
330 pub oplog_idx: OplogIndex,
331}
332
333impl PromiseId {
334 pub fn to_redis_key(&self) -> String {
335 format!("{}:{}", self.worker_id.to_redis_key(), self.oplog_idx)
336 }
337}
338
339impl Display for PromiseId {
340 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
341 write!(f, "{}/{}", self.worker_id, self.oplog_idx)
342 }
343}
344
345#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Encode, Decode, Default)]
346#[cfg_attr(feature = "poem", derive(poem_openapi::NewType))]
347#[cfg_attr(
348 feature = "model",
349 derive(serde::Serialize, serde::Deserialize, golem_wasm_rpc_derive::IntoValue)
350)]
351pub struct OplogIndex(pub(crate) u64);
352
353impl OplogIndex {
354 pub const NONE: OplogIndex = OplogIndex(0);
355 pub const INITIAL: OplogIndex = OplogIndex(1);
356
357 pub const fn from_u64(value: u64) -> OplogIndex {
358 OplogIndex(value)
359 }
360
361 pub fn previous(&self) -> OplogIndex {
363 OplogIndex(self.0 - 1)
364 }
365
366 pub fn next(&self) -> OplogIndex {
368 OplogIndex(self.0 + 1)
369 }
370
371 pub fn range_end(&self, count: u64) -> OplogIndex {
374 OplogIndex(self.0 + count - 1)
375 }
376}
377
378impl Display for OplogIndex {
379 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
380 write!(f, "{}", self.0)
381 }
382}
383
384impl From<OplogIndex> for u64 {
385 fn from(value: OplogIndex) -> Self {
386 value.0
387 }
388}
389
390#[cfg(test)]
391mod tests {
392 use super::{ComponentId, TargetWorkerId};
393 use test_r::test;
394
395 #[test]
396 fn test_parse_worker_urn() {
397 let component_id = ComponentId::new_v4();
398
399 fn check(urn: String, expected: Option<TargetWorkerId>) {
400 assert_eq!(TargetWorkerId::parse_worker_urn(&urn), expected, "{urn}");
401 }
402
403 check(
404 format!("urn:worker:{component_id}"),
405 Some(TargetWorkerId {
406 component_id: component_id.clone(),
407 worker_name: None,
408 }),
409 );
410 check(
411 format!("urn:worker:{component_id}/worker1"),
412 Some(TargetWorkerId {
413 component_id: component_id.clone(),
414 worker_name: Some("worker1".to_string()),
415 }),
416 );
417 check(format!("urn:worker:{component_id}/worker1/worker2"), None);
418 check(format!("urn:component:{component_id}"), None);
419 }
420}