golem_common/
base_model.rs

1// Copyright 2024-2025 Golem Cloud
2//
3// Licensed under the Golem Source License v1.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://license.golem.cloud/LICENSE
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use 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    /// The dual of `TargetWorkerId::into_worker_id`
133    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    /// Converts a `TargetWorkerId` to a `WorkerId` if the worker name is specified
217    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    /// Converts a `TargetWorkerId` to a `WorkerId`. If the worker name was not specified,
225    /// it generates a new unique one, and if the `force_in_shard` set is not empty, it guarantees
226    /// that the generated worker ID will belong to one of the provided shards.
227    ///
228    /// If the worker name was specified, `force_in_shard` is ignored.
229    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    // NOTE: Deprecated, to be removed once the wasm-rpc constructor is changed to accept worker-id
271    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    /// Gets the previous oplog index
362    pub fn previous(&self) -> OplogIndex {
363        OplogIndex(self.0 - 1)
364    }
365
366    /// Gets the next oplog index
367    pub fn next(&self) -> OplogIndex {
368        OplogIndex(self.0 + 1)
369    }
370
371    /// Gets the last oplog index belonging to an inclusive range starting at this oplog index,
372    /// having `count` elements.
373    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}