Add fenced attempt mutation and outcome publication
This commit is contained in:
parent
2db0e22232
commit
80c631f780
5 changed files with 1868 additions and 18 deletions
|
|
@ -10,6 +10,8 @@ pub enum CoordinationError {
|
|||
Config(String),
|
||||
#[error("local permit capacity exhausted")]
|
||||
LocalFull,
|
||||
#[error("invalid coordination argument: {0}")]
|
||||
InvalidArgument(String),
|
||||
/// Value exceeds the IEEE-754 exact integer range used by Lua `tonumber`.
|
||||
#[error("value {value} exceeds max safe integer {max}")]
|
||||
UnsafeInteger { value: u64, max: u64 },
|
||||
|
|
|
|||
|
|
@ -14,5 +14,5 @@ pub use local::LocalPermitGate;
|
|||
pub use store::{CoordinationStore, StoreConfig};
|
||||
pub use types::{
|
||||
ActivateOptions, LeaseOwner, LeaseRecord, LiveLease, MAX_SAFE_INTEGER, OpCode, OpResult,
|
||||
ProgressHint, Reservation, ensure_safe_integer,
|
||||
ProgressHint, ProgressValue, Reservation, ensure_safe_integer,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
-- KEYS: lease, progress. ARGV: attempt_id, fence, worker_id, bytes, ttl_ms.
|
||||
-- KEYS: lease, progress.
|
||||
-- ARGV: attempt_id, fence, worker_id, bytes, total_bytes, members, total_members, ttl_ms.
|
||||
-- Fence and bytes remain decimal strings to preserve integer identity.
|
||||
local function now_ms()
|
||||
local t = redis.call('TIME')
|
||||
|
|
@ -24,7 +25,10 @@ local attempt_id = ARGV[1]
|
|||
local fence = ARGV[2]
|
||||
local worker_id = ARGV[3]
|
||||
local bytes = ARGV[4]
|
||||
local ttl_ms = tonumber(ARGV[5])
|
||||
local total_bytes = ARGV[5]
|
||||
local completed_members = ARGV[6]
|
||||
local total_members = ARGV[7]
|
||||
local ttl_ms = tonumber(ARGV[8])
|
||||
|
||||
local raw = redis.call('GET', lease_key)
|
||||
if not raw then
|
||||
|
|
@ -38,11 +42,38 @@ end
|
|||
if not owner_eq(rec, attempt_id, fence, worker_id) then
|
||||
return {'FAIL_STOLEN'}
|
||||
end
|
||||
local old_raw = redis.call('GET', progress_key)
|
||||
if old_raw then
|
||||
local old = cjson.decode(old_raw)
|
||||
if old.attempt_id == attempt_id and fence_str(old.fence) == fence_str(fence) then
|
||||
if tonumber(old.bytes) > tonumber(bytes)
|
||||
or tonumber(old.completed_members or '0') > tonumber(completed_members) then
|
||||
return {'FAIL_STALE'}
|
||||
end
|
||||
if old.total_bytes then
|
||||
if total_bytes == '' then
|
||||
total_bytes = fence_str(old.total_bytes)
|
||||
elseif fence_str(old.total_bytes) ~= fence_str(total_bytes) then
|
||||
return {'FAIL_STALE'}
|
||||
end
|
||||
end
|
||||
if old.total_members then
|
||||
if total_members == '' then
|
||||
total_members = fence_str(old.total_members)
|
||||
elseif fence_str(old.total_members) ~= fence_str(total_members) then
|
||||
return {'FAIL_STALE'}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
local prog = {
|
||||
attempt_id = attempt_id,
|
||||
fence = fence_str(fence),
|
||||
bytes = fence_str(bytes)
|
||||
bytes = fence_str(bytes),
|
||||
completed_members = fence_str(completed_members)
|
||||
}
|
||||
if total_bytes ~= '' then prog.total_bytes = fence_str(total_bytes) end
|
||||
if total_members ~= '' then prog.total_members = fence_str(total_members) end
|
||||
redis.call('SET', progress_key, cjson.encode(prog))
|
||||
redis.call('PEXPIRE', progress_key, ttl_ms)
|
||||
return {'OK', fence_str(bytes)}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ use crate::keys::KeySpace;
|
|||
use crate::scripts::Scripts;
|
||||
use crate::types::{
|
||||
ActivateOptions, LeaseOwner, LeaseRecord, LiveLease, OpCode, OpResult, ProgressHint,
|
||||
Reservation, ensure_safe_integer,
|
||||
ProgressValue, Reservation,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
|
|
@ -284,10 +284,31 @@ impl CoordinationStore {
|
|||
job_id: &str,
|
||||
owner: &LeaseOwner,
|
||||
bytes: u64,
|
||||
) -> Result<OpResult, CoordinationError> {
|
||||
self.report_progress_value(
|
||||
job_id,
|
||||
owner,
|
||||
ProgressValue {
|
||||
completed_bytes: bytes,
|
||||
total_bytes: None,
|
||||
completed_members: 0,
|
||||
total_members: None,
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn report_progress_value(
|
||||
&self,
|
||||
job_id: &str,
|
||||
owner: &LeaseOwner,
|
||||
progress: ProgressValue,
|
||||
) -> Result<OpResult, CoordinationError> {
|
||||
owner.validate()?;
|
||||
ensure_safe_integer(bytes)?;
|
||||
let progress = progress.validate()?;
|
||||
let ttl_ms = duration_ms(self.config.progress_ttl);
|
||||
let total_bytes = progress.total_bytes.map(|value| value.to_string());
|
||||
let total_members = progress.total_members.map(|value| value.to_string());
|
||||
self.eval_script(
|
||||
&self.scripts.report_progress,
|
||||
&[self.keys.lease(job_id), self.keys.progress(job_id)],
|
||||
|
|
@ -295,7 +316,10 @@ impl CoordinationStore {
|
|||
owner.attempt_id.as_str(),
|
||||
&owner.fence.to_string(),
|
||||
owner.worker_id.as_str(),
|
||||
&bytes.to_string(),
|
||||
&progress.completed_bytes.to_string(),
|
||||
total_bytes.as_deref().unwrap_or(""),
|
||||
&progress.completed_members.to_string(),
|
||||
total_members.as_deref().unwrap_or(""),
|
||||
&ttl_ms.to_string(),
|
||||
],
|
||||
)
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ pub enum OpCode {
|
|||
FailStolen,
|
||||
FailFull,
|
||||
FailFence,
|
||||
FailStale,
|
||||
}
|
||||
|
||||
impl OpCode {
|
||||
|
|
@ -49,6 +50,7 @@ impl OpCode {
|
|||
"FAIL_STOLEN" => Self::FailStolen,
|
||||
"FAIL_FULL" => Self::FailFull,
|
||||
"FAIL_FENCE" => Self::FailFence,
|
||||
"FAIL_STALE" => Self::FailStale,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
|
@ -124,6 +126,44 @@ pub struct ProgressHint {
|
|||
pub fence: u64,
|
||||
#[serde(deserialize_with = "deserialize_u64_lenient")]
|
||||
pub bytes: u64,
|
||||
#[serde(default, deserialize_with = "deserialize_optional_u64_lenient")]
|
||||
pub total_bytes: Option<u64>,
|
||||
#[serde(default, deserialize_with = "deserialize_u64_lenient")]
|
||||
pub completed_members: u64,
|
||||
#[serde(default, deserialize_with = "deserialize_optional_u64_lenient")]
|
||||
pub total_members: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct ProgressValue {
|
||||
pub completed_bytes: u64,
|
||||
pub total_bytes: Option<u64>,
|
||||
pub completed_members: u64,
|
||||
pub total_members: Option<u64>,
|
||||
}
|
||||
|
||||
impl ProgressValue {
|
||||
pub fn validate(self) -> Result<Self, CoordinationError> {
|
||||
ensure_safe_integer(self.completed_bytes)?;
|
||||
ensure_safe_integer(self.completed_members)?;
|
||||
if let Some(total) = self.total_bytes {
|
||||
ensure_safe_integer(total)?;
|
||||
if self.completed_bytes > total {
|
||||
return Err(CoordinationError::InvalidArgument(
|
||||
"completed bytes exceed total bytes".into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
if let Some(total) = self.total_members {
|
||||
ensure_safe_integer(total)?;
|
||||
if self.completed_members > total {
|
||||
return Err(CoordinationError::InvalidArgument(
|
||||
"completed members exceed total members".into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(self)
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize_u64_lenient<'de, D>(deserializer: D) -> Result<u64, D::Error>
|
||||
|
|
@ -158,6 +198,23 @@ where
|
|||
deserializer.deserialize_any(U64Visitor)
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum LenientU64 {
|
||||
Number(u64),
|
||||
String(String),
|
||||
}
|
||||
|
||||
fn deserialize_optional_u64_lenient<'de, D>(deserializer: D) -> Result<Option<u64>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
Option::<LenientU64>::deserialize(deserializer)?.map_or(Ok(None), |value| match value {
|
||||
LenientU64::Number(value) => Ok(Some(value)),
|
||||
LenientU64::String(value) => value.parse().map(Some).map_err(serde::de::Error::custom),
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct OpResult {
|
||||
pub code: OpCode,
|
||||
|
|
|
|||
Loading…
Reference in a new issue