Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
266 changes: 3 additions & 263 deletions Cargo.lock

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions components/spider-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@ path = "src/lib.rs"

[dependencies]
non-empty-string = { version = "0.2.6", features = ["serde"] }
rand = "0.9.1"
rmp-serde = "1.3.1"
semver = "1.0.27"
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.149"
spider-derive = { path = "../spider-derive" }
sqlx = { version = "0.8.6", features = ["mysql", "uuid"] }
sqlx = { version = "0.8.6", features = ["mysql"] }
strum = { version = "0.28.0", features = ["derive"] }
thiserror = "2.0.18"
uuid = { version = "1.19.0", features = ["serde", "v4"] }

[dev-dependencies]
tokio = { version = "1.50.0", features = ["macros", "rt-multi-thread"] }
Expand Down
126 changes: 91 additions & 35 deletions components/spider-core/src/types/id.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
use std::{fmt::Debug, marker::PhantomData};
use std::{
fmt::{Debug, Display},
marker::PhantomData,
};

use serde::{Deserialize, Serialize};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use sqlx::{Database, encode::IsNull};
use uuid::Uuid;

use crate::task::TaskIndex;

/// A generic identifier type that wraps a UUID and a type marker.
/// A generic identifier type that wraps a numeric ID and a type marker.
///
/// # Type Parameters:
///
Expand All @@ -15,84 +17,109 @@ use crate::task::TaskIndex;
/// # Examples
///
/// ```rust
/// use spider_core::types::id::Id;
///
/// #[derive(Debug, PartialEq, Eq)]
/// enum SomeTypeIdMarker {}
/// type SomeTypeId = Id<SomeTypeIdMarker>;
/// ```
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Id<TypeMarker: Debug + PartialEq + Eq>(Uuid, PhantomData<TypeMarker>);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Id<TypeMarker: Debug + PartialEq + Eq> {
raw: u64,
_marker: PhantomData<TypeMarker>,
}

impl<TypeMarker: Debug + PartialEq + Eq> Default for Id<TypeMarker> {
fn default() -> Self {
Self::new()
Self::from(0)
}
}

impl<TypeMarker: Debug + PartialEq + Eq> Id<TypeMarker> {
/// Creates a random ID for tests.
///
/// Production IDs should be assigned by persistent storage instead.
#[must_use]
pub fn new() -> Self {
Self(Uuid::new_v4(), PhantomData)
}

#[must_use]
pub const fn from(uid: Uuid) -> Self {
Self(uid, PhantomData)
pub fn random() -> Self {
Self::from(rand::random())
}

#[must_use]
pub const fn as_uuid_ref(&self) -> &Uuid {
&self.0
pub const fn from(id: u64) -> Self {
Self {
raw: id,
_marker: PhantomData,
}
}

#[must_use]
pub const fn as_bytes(&self) -> &UuidBytes {
self.0.as_bytes()
pub const fn get(&self) -> u64 {
self.raw
}
}

impl<TypeMarker, Db> sqlx::Type<Db> for Id<TypeMarker>
impl<TypeMarker: Debug + PartialEq + Eq, Db: Database> sqlx::Type<Db> for Id<TypeMarker>
where
TypeMarker: Debug + PartialEq + Eq,
Db: Database,
Uuid: sqlx::Type<Db>,
u64: sqlx::Type<Db>,
{
fn type_info() -> <Db as Database>::TypeInfo {
<Uuid as sqlx::Type<Db>>::type_info()
<u64 as sqlx::Type<Db>>::type_info()
}

fn compatible(ty: &<Db as Database>::TypeInfo) -> bool {
<Uuid as sqlx::Type<Db>>::compatible(ty)
<u64 as sqlx::Type<Db>>::compatible(ty)
}
}

impl<'encode, TypeMarker, Db> sqlx::Encode<'encode, Db> for Id<TypeMarker>
impl<'encode, TypeMarker: Debug + PartialEq + Eq, Db: Database> sqlx::Encode<'encode, Db>
for Id<TypeMarker>
where
TypeMarker: Debug + PartialEq + Eq,
Db: Database,
Uuid: sqlx::Encode<'encode, Db>,
u64: sqlx::Encode<'encode, Db>,
{
fn encode_by_ref(
&self,
buf: &mut <Db as Database>::ArgumentBuffer<'encode>,
) -> Result<IsNull, sqlx::error::BoxDynError> {
self.0.encode_by_ref(buf)
self.get().encode_by_ref(buf)
}
}

impl<'decode, TypeMarker, Db> sqlx::Decode<'decode, Db> for Id<TypeMarker>
impl<'decode, TypeMarker: Debug + PartialEq + Eq, Db: Database> sqlx::Decode<'decode, Db>
for Id<TypeMarker>
where
TypeMarker: Debug + PartialEq + Eq,
Db: Database,
Uuid: sqlx::Decode<'decode, Db>,
u64: sqlx::Decode<'decode, Db>,
{
fn decode(
value: <Db as Database>::ValueRef<'decode>,
) -> Result<Self, sqlx::error::BoxDynError> {
Uuid::decode(value).map(|uuid| Self(uuid, PhantomData))
u64::decode(value).map(|id| Self::from(id))
}
}

impl<TypeMarker: Debug + PartialEq + Eq> Display for Id<TypeMarker> {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
Display::fmt(&self.get(), formatter)
}
}

impl<TypeMarker: Debug + PartialEq + Eq> Serialize for Id<TypeMarker> {
fn serialize<SerializerImpl: Serializer>(
&self,
serializer: SerializerImpl,
) -> Result<SerializerImpl::Ok, SerializerImpl::Error> {
self.get().serialize(serializer)
}
}

pub type UuidBytes = uuid::Bytes;
impl<'deserializer_lifetime, TypeMarker: Debug + PartialEq + Eq> Deserialize<'deserializer_lifetime>
for Id<TypeMarker>
{
fn deserialize<DeserializerImpl: Deserializer<'deserializer_lifetime>>(
deserializer: DeserializerImpl,
) -> Result<Self, DeserializerImpl::Error> {
u64::deserialize(deserializer).map(Self::from)
}
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ResourceGroupIdMarker {}
Expand Down Expand Up @@ -180,3 +207,32 @@ where
}

pub type SignedJobId = SignedId<JobIdMarker>;

#[cfg(test)]
mod tests {
use super::{JobId, ResourceGroupId};

#[test]
fn id_serializes_as_u64() {
let job_id = JobId::from(42);
let serialized =
serde_json::to_string(&job_id).expect("job id serialization should succeed");

assert_eq!(serialized, "42");
}

#[test]
fn distinct_id_markers_can_share_numeric_values() {
let job_id = JobId::from(7);
let resource_group_id = ResourceGroupId::from(7);

assert_eq!(job_id.get(), resource_group_id.get());
}

#[test]
fn default_id_is_zero() {
let job_id = JobId::default();

assert_eq!(job_id.get(), 0);
}
}
2 changes: 1 addition & 1 deletion components/spider-execution-manager/src/liveness.rs
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ mod tests {
cancellation_token: CancellationToken,
) -> (LivenessHandle, JoinHandle<()>) {
spawn(
ExecutionManagerId::new(),
ExecutionManagerId::random(),
client,
tracker,
cancellation_token,
Expand Down
8 changes: 4 additions & 4 deletions components/spider-execution-manager/src/process_pool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -217,10 +217,10 @@ impl ProcessPool {
fn spawn_executor(&self) -> Result<ExecutorHandle, InternalError> {
let executor_id = self.next_executor_id.fetch_add(1, Ordering::Relaxed);
std::fs::create_dir_all(&self.config.log_dir)?;
let log_path = self.config.log_dir.join(format!(
"{}-{executor_id}.log",
self.config.em_id.as_uuid_ref()
));
let log_path = self
.config
.log_dir
.join(format!("{}-{executor_id}.log", self.config.em_id));
let log_file = File::options().create(true).append(true).open(&log_path)?;

let mut command = Command::new(&self.config.executor_binary_path);
Expand Down
2 changes: 0 additions & 2 deletions components/spider-storage/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ tokio = {
version = "1.50.0",
features = ["macros", "rt-multi-thread", "sync", "time"]
}
uuid = { version = "1.19.0", features = ["serde"] }

[dev-dependencies]
anyhow = "1.0.98"
Expand All @@ -38,4 +37,3 @@ serial_test = { version = "3.2.0", features = ["file_locks"] }
tabled = "0.20.0"
tokio = { version = "1.50.0", features = ["macros", "rt-multi-thread", "sync"] }
tokio-util = { version = "0.7", features = ["rt"] }
uuid = { version = "1.19.0", features = ["v4"] }
29 changes: 12 additions & 17 deletions components/spider-storage/src/db/mariadb.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ impl ExternalJobOrchestration for MariaDbStorageConnector {
) -> Result<JobId, DbError> {
const INSERT_QUERY: &str = formatcp!(
"INSERT INTO `{table}` (`resource_group_id`, `serialized_task_graph`, \
`serialized_job_inputs`) VALUES (?, ?, ?) RETURNING CAST(`id` AS BINARY(16)) AS `id`;",
`serialized_job_inputs`) VALUES (?, ?, ?) RETURNING `id`;",
table = JOBS_TABLE_NAME,
);

Expand Down Expand Up @@ -170,8 +170,7 @@ impl ExternalJobOrchestration for MariaDbStorageConnector {

let outputs_bytes = serialized_outputs.ok_or_else(|| {
DbError::CorruptedDbState(format!(
"job `{}` succeeded but has no serialized outputs",
job_id.as_uuid_ref()
"job `{job_id}` succeeded but has no serialized outputs"
))
})?;
let outputs: Vec<TaskOutput> =
Expand Down Expand Up @@ -201,10 +200,7 @@ impl ExternalJobOrchestration for MariaDbStorageConnector {
}

let message = error_message.ok_or_else(|| {
DbError::CorruptedDbState(format!(
"job `{}` failed but has no error message",
job_id.as_uuid_ref()
))
DbError::CorruptedDbState(format!("job `{job_id}` failed but has no error message"))
})?;
Ok(message)
}
Expand Down Expand Up @@ -344,7 +340,7 @@ impl InternalJobOrchestration for MariaDbStorageConnector {
const DELETE_BATCH_SIZE: usize = 1000;

const SELECT_QUERY: &str = formatcp!(
"SELECT CAST(`id` AS BINARY(16)) FROM `{table}` WHERE `state` IN \
"SELECT `id` FROM `{table}` WHERE `state` IN \
('{succeeded_state}','{failed_state}','{cancelled_state}') AND `ended_at` < NOW() - \
INTERVAL ? SECOND LIMIT {DELETE_BATCH_SIZE} FOR UPDATE;",
table = JOBS_TABLE_NAME,
Expand Down Expand Up @@ -394,8 +390,7 @@ impl ResourceGroupManagement for MariaDbStorageConnector {
password: Vec<u8>,
) -> Result<ResourceGroupId, DbError> {
const QUERY: &str = formatcp!(
"INSERT INTO `{table}` (`external_id`, `password`) VALUES (?, ?) RETURNING CAST(`id` \
AS BINARY(16)) AS `id`;",
"INSERT INTO `{table}` (`external_id`, `password`) VALUES (?, ?) RETURNING `id`;",
table = RESOURCE_GROUPS_TABLE_NAME,
);

Expand Down Expand Up @@ -462,7 +457,7 @@ impl ExecutionManagerLivenessManagement for MariaDbStorageConnector {
ip_address: IpAddr,
) -> Result<ExecutionManagerId, DbError> {
const INSERT_QUERY: &str = formatcp!(
"INSERT INTO `{table}` (`ip_address`) VALUES (?) RETURNING CAST(`id` AS BINARY(16));",
"INSERT INTO `{table}` (`ip_address`) VALUES (?) RETURNING `id`;",
table = EXECUTION_MANAGERS_TABLE_NAME,
);

Expand Down Expand Up @@ -539,8 +534,8 @@ impl ExecutionManagerLivenessManagement for MariaDbStorageConnector {
const UPDATE_BATCH_SIZE: usize = 1000;

const SELECT_QUERY: &str = formatcp!(
"SELECT CAST(`id` AS BINARY(16)) FROM `{table}` WHERE `state` = '{alive_state}' AND \
`last_heartbeat_at` < CURRENT_TIMESTAMP - INTERVAL ? SECOND FOR UPDATE;",
"SELECT `id` FROM `{table}` WHERE `state` = '{alive_state}' AND `last_heartbeat_at` < \
CURRENT_TIMESTAMP - INTERVAL ? SECOND FOR UPDATE;",
table = EXECUTION_MANAGERS_TABLE_NAME,
alive_state = ExecutionManagerState::Alive.as_str(),
);
Expand Down Expand Up @@ -601,7 +596,7 @@ const fn resource_groups_creation_query() -> &'static str {
formatcp!(
r"
CREATE TABLE IF NOT EXISTS `{RESOURCE_GROUPS_TABLE_NAME}` (
`id` UUID NOT NULL DEFAULT UUID_v7(),
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
`external_id` VARCHAR(256) NOT NULL,
`password` VARBINARY(2048) NOT NULL,
PRIMARY KEY (`id`),
Expand All @@ -615,8 +610,8 @@ const fn jobs_creation_query() -> &'static str {
formatcp!(
r"
CREATE TABLE IF NOT EXISTS `{JOBS_TABLE_NAME}` (
`id` UUID NOT NULL DEFAULT UUID_v7(),
`resource_group_id` UUID NOT NULL,
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
`resource_group_id` BIGINT UNSIGNED NOT NULL,
`state` {state_enum} NOT NULL DEFAULT {default_state},
`serialized_task_graph` LONGTEXT NOT NULL,
`serialized_job_inputs` LONGBLOB NOT NULL,
Expand All @@ -642,7 +637,7 @@ const fn execution_managers_creation_query() -> &'static str {
formatcp!(
r"
CREATE TABLE IF NOT EXISTS `{EXECUTION_MANAGERS_TABLE_NAME}` (
`id` UUID NOT NULL DEFAULT UUID_v7(),
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
`ip_address` VARCHAR(45) NOT NULL,
`state` {state_enum} NOT NULL DEFAULT {default_state},
`last_heartbeat_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
Expand Down
Loading
Loading