Implement crash-safe streaming worker runtime
This commit is contained in:
parent
16cba3c317
commit
cdeef3cc1a
2 changed files with 7448 additions and 194 deletions
|
|
@ -0,0 +1,19 @@
|
|||
ALTER TABLE result_inventory_members
|
||||
DROP CONSTRAINT result_inventory_members_attempt_id_fkey,
|
||||
ADD CONSTRAINT result_inventory_members_attempt_id_fkey
|
||||
FOREIGN KEY (attempt_id)
|
||||
REFERENCES result_inventories (attempt_id)
|
||||
ON UPDATE CASCADE
|
||||
ON DELETE CASCADE
|
||||
DEFERRABLE INITIALLY DEFERRED;
|
||||
|
||||
ALTER TABLE result_inventories
|
||||
ADD COLUMN staging_origin_attempt_id TEXT,
|
||||
ADD COLUMN target_prefix TEXT,
|
||||
ADD CONSTRAINT result_inventories_staging_binding_check CHECK (
|
||||
(staging_origin_attempt_id IS NULL) = (target_prefix IS NULL)
|
||||
);
|
||||
|
||||
UPDATE result_inventories
|
||||
SET staging_origin_attempt_id = attempt_id,
|
||||
target_prefix = '';
|
||||
|
|
@ -3,7 +3,7 @@ use remora_domain::{MemberPath, OpaqueId};
|
|||
use remora_stream::TargetBody;
|
||||
use remora_target::{
|
||||
ContentSha256, CreateObjectRequest, ObjectIdentity, StoredObject, TargetStore,
|
||||
validate_attempt_key,
|
||||
attempt_manifest_key_in, attempt_object_prefix, validate_attempt_key,
|
||||
};
|
||||
use serde::Serialize;
|
||||
use sqlx::PgPool;
|
||||
|
|
@ -26,10 +26,25 @@ impl ResultInventory {
|
|||
}
|
||||
|
||||
pub async fn create(&self, attempt_id: &OpaqueId) -> Result<(), ResultError> {
|
||||
let result = sqlx::query("INSERT INTO result_inventories (attempt_id) VALUES ($1)")
|
||||
.bind(attempt_id.as_str())
|
||||
.execute(&self.pool)
|
||||
.await;
|
||||
self.create_bound(attempt_id, attempt_id, "").await
|
||||
}
|
||||
|
||||
pub async fn create_bound(
|
||||
&self,
|
||||
attempt_id: &OpaqueId,
|
||||
staging_origin_attempt_id: &OpaqueId,
|
||||
target_prefix: &str,
|
||||
) -> Result<(), ResultError> {
|
||||
let result = sqlx::query(
|
||||
"INSERT INTO result_inventories (
|
||||
attempt_id, staging_origin_attempt_id, target_prefix
|
||||
) VALUES ($1, $2, $3)",
|
||||
)
|
||||
.bind(attempt_id.as_str())
|
||||
.bind(staging_origin_attempt_id.as_str())
|
||||
.bind(target_prefix)
|
||||
.execute(&self.pool)
|
||||
.await;
|
||||
match result {
|
||||
Ok(_) => Ok(()),
|
||||
Err(error) if is_unique_violation(&error, "result_inventories_pkey") => {
|
||||
|
|
@ -39,14 +54,100 @@ impl ResultInventory {
|
|||
}
|
||||
}
|
||||
|
||||
pub async fn open_or_create(&self, attempt_id: &OpaqueId) -> Result<(), ResultError> {
|
||||
self.open_or_create_bound(attempt_id, attempt_id, "").await
|
||||
}
|
||||
|
||||
pub async fn open_or_create_bound(
|
||||
&self,
|
||||
attempt_id: &OpaqueId,
|
||||
staging_origin_attempt_id: &OpaqueId,
|
||||
target_prefix: &str,
|
||||
) -> Result<(), ResultError> {
|
||||
match self
|
||||
.create_bound(attempt_id, staging_origin_attempt_id, target_prefix)
|
||||
.await
|
||||
{
|
||||
Ok(()) => Ok(()),
|
||||
Err(ResultError::InventoryExists) => {
|
||||
let binding = sqlx::query_as::<_, (String, Option<String>, Option<String>)>(
|
||||
"SELECT state, staging_origin_attempt_id, target_prefix
|
||||
FROM result_inventories WHERE attempt_id = $1",
|
||||
)
|
||||
.bind(attempt_id.as_str())
|
||||
.fetch_one(&self.pool)
|
||||
.await?;
|
||||
if binding.0 == "open"
|
||||
&& binding.1.as_deref() == Some(staging_origin_attempt_id.as_str())
|
||||
&& binding.2.as_deref() == Some(target_prefix)
|
||||
{
|
||||
Ok(())
|
||||
} else {
|
||||
Err(ResultError::InventoryNotOpen)
|
||||
}
|
||||
}
|
||||
Err(error) => Err(error),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn verify_member_path(
|
||||
&self,
|
||||
attempt_id: &OpaqueId,
|
||||
path: &str,
|
||||
target: &TargetStore,
|
||||
) -> Result<Option<ResultMember>, ResultError> {
|
||||
let member = sqlx::query_as::<_, MemberRow>(
|
||||
"SELECT member_id, path, size, object_store, object_bucket, object_key,
|
||||
object_version_id, object_sha256
|
||||
FROM result_inventory_members
|
||||
WHERE attempt_id = $1 AND path = $2",
|
||||
)
|
||||
.bind(attempt_id.as_str())
|
||||
.bind(path)
|
||||
.fetch_optional(&self.pool)
|
||||
.await?
|
||||
.map(MemberRow::into_member)
|
||||
.transpose()?;
|
||||
let Some(member) = member else {
|
||||
return Ok(None);
|
||||
};
|
||||
let observed = target.verify_exact(&member.exact_identity()).await?;
|
||||
if observed.identity != member.exact_identity()
|
||||
|| observed.size != member.size()
|
||||
|| observed.content_sha256.as_ref() != Some(member.content_sha256())
|
||||
{
|
||||
return Err(ResultError::Integrity);
|
||||
}
|
||||
Ok(Some(member))
|
||||
}
|
||||
|
||||
pub async fn add_member(
|
||||
&self,
|
||||
attempt_id: &OpaqueId,
|
||||
member: &ResultMember,
|
||||
) -> Result<(), ResultError> {
|
||||
validate_member_identity(attempt_id, member)?;
|
||||
let size = i64::try_from(member.size()).map_err(|_| ResultError::SizeOverflow)?;
|
||||
let mut tx = self.pool.begin().await?;
|
||||
let (staging_origin, target_prefix) =
|
||||
sqlx::query_as::<_, (Option<String>, Option<String>)>(
|
||||
"SELECT staging_origin_attempt_id, target_prefix
|
||||
FROM result_inventories WHERE attempt_id = $1",
|
||||
)
|
||||
.bind(attempt_id.as_str())
|
||||
.fetch_one(tx.as_mut())
|
||||
.await?;
|
||||
let staging_origin = staging_origin
|
||||
.ok_or(ResultError::InvalidObjectIdentity)
|
||||
.and_then(|value| {
|
||||
OpaqueId::parse(value).map_err(|_| ResultError::InvalidObjectIdentity)
|
||||
})?;
|
||||
validate_member_identity(
|
||||
target_prefix
|
||||
.as_deref()
|
||||
.ok_or(ResultError::InvalidObjectIdentity)?,
|
||||
&staging_origin,
|
||||
member,
|
||||
)?;
|
||||
let state: Option<String> = sqlx::query_scalar(
|
||||
"SELECT state FROM result_inventories WHERE attempt_id = $1 FOR UPDATE",
|
||||
)
|
||||
|
|
@ -158,7 +259,12 @@ impl ResultInventory {
|
|||
}
|
||||
self.validate_members(attempt_id, target).await?;
|
||||
let (member_count, total_bytes) = self.inventory_totals(attempt_id).await?;
|
||||
let request = CreateObjectRequest::for_attempt_manifest(attempt_id, manifest_id);
|
||||
let (staging_origin, target_prefix) = self.inventory_binding(attempt_id).await?;
|
||||
let request = CreateObjectRequest::for_target_attempt_manifest(
|
||||
&target_prefix,
|
||||
&staging_origin,
|
||||
manifest_id,
|
||||
);
|
||||
let mut body = target.open_create_only(&request).await?;
|
||||
body.write_all(&manifest_header(attempt_id))
|
||||
.await
|
||||
|
|
@ -217,7 +323,9 @@ impl ResultInventory {
|
|||
return Err(ResultError::StaleAttempt);
|
||||
}
|
||||
require_supported_version(input.schema_version)?;
|
||||
let expected_key = remora_target::attempt_manifest_key(attempt_id, &input.manifest_id);
|
||||
let (staging_origin, target_prefix) = self.inventory_binding(attempt_id).await?;
|
||||
let expected_key =
|
||||
attempt_manifest_key_in(&target_prefix, &staging_origin, &input.manifest_id);
|
||||
if input.manifest.identity.key != expected_key {
|
||||
return Err(ResultError::InvalidObjectIdentity);
|
||||
}
|
||||
|
|
@ -291,12 +399,26 @@ impl ResultInventory {
|
|||
attempt_id: &OpaqueId,
|
||||
target: &TargetStore,
|
||||
) -> Result<(), ResultError> {
|
||||
let (staging_origin, target_prefix) =
|
||||
sqlx::query_as::<_, (Option<String>, Option<String>)>(
|
||||
"SELECT staging_origin_attempt_id, target_prefix
|
||||
FROM result_inventories WHERE attempt_id = $1",
|
||||
)
|
||||
.bind(attempt_id.as_str())
|
||||
.fetch_one(&self.pool)
|
||||
.await?;
|
||||
let staging_origin = staging_origin
|
||||
.ok_or(ResultError::InvalidObjectIdentity)
|
||||
.and_then(|value| {
|
||||
OpaqueId::parse(value).map_err(|_| ResultError::InvalidObjectIdentity)
|
||||
})?;
|
||||
let target_prefix = target_prefix.ok_or(ResultError::InvalidObjectIdentity)?;
|
||||
let mut rows = sqlx::query_as::<_, MemberRow>(MEMBER_SELECT)
|
||||
.bind(attempt_id.as_str())
|
||||
.fetch(&self.pool);
|
||||
while let Some(row) = rows.try_next().await? {
|
||||
let member = row.into_member()?;
|
||||
validate_member_identity(attempt_id, &member)?;
|
||||
validate_member_identity(&target_prefix, &staging_origin, &member)?;
|
||||
let observed = target.verify_exact(&member.exact_identity()).await?;
|
||||
if observed.identity != member.exact_identity()
|
||||
|| observed.size != member.size()
|
||||
|
|
@ -318,6 +440,25 @@ impl ResultInventory {
|
|||
Ok((count as u64, bytes as u64))
|
||||
}
|
||||
|
||||
async fn inventory_binding(
|
||||
&self,
|
||||
attempt_id: &OpaqueId,
|
||||
) -> Result<(OpaqueId, String), ResultError> {
|
||||
let (origin, prefix) = sqlx::query_as::<_, (Option<String>, Option<String>)>(
|
||||
"SELECT staging_origin_attempt_id, target_prefix
|
||||
FROM result_inventories WHERE attempt_id = $1",
|
||||
)
|
||||
.bind(attempt_id.as_str())
|
||||
.fetch_one(&self.pool)
|
||||
.await?;
|
||||
let origin = origin
|
||||
.ok_or(ResultError::InvalidObjectIdentity)
|
||||
.and_then(|value| {
|
||||
OpaqueId::parse(value).map_err(|_| ResultError::InvalidObjectIdentity)
|
||||
})?;
|
||||
Ok((origin, prefix.ok_or(ResultError::InvalidObjectIdentity)?))
|
||||
}
|
||||
|
||||
async fn persist_sealed(&self, input: &PublicationInput) -> Result<(), ResultError> {
|
||||
let result = sqlx::query(
|
||||
"UPDATE result_inventories SET
|
||||
|
|
@ -462,11 +603,12 @@ impl InventoryRow {
|
|||
}
|
||||
|
||||
fn validate_member_identity(
|
||||
target_prefix: &str,
|
||||
attempt_id: &OpaqueId,
|
||||
member: &ResultMember,
|
||||
) -> Result<(), ResultError> {
|
||||
validate_attempt_key(member.object().key()).map_err(|_| ResultError::InvalidObjectIdentity)?;
|
||||
let prefix = format!("_remora/a/{}/o/", attempt_id.as_str());
|
||||
let prefix = attempt_object_prefix(target_prefix, attempt_id);
|
||||
if !member.object().key().starts_with(&prefix) {
|
||||
return Err(ResultError::StaleAttempt);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue