taskflow
This commit is contained in:
+155
-155
@@ -1,4 +1,5 @@
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use chrono::Utc;
|
||||
@@ -18,7 +19,11 @@ use crate::models::{NewUserMembership, TenantStatus};
|
||||
use crate::schema::{tenants, user_memberships};
|
||||
use crate::state::AppState;
|
||||
use crate::tenants::TenantRepository;
|
||||
use crate::workers::{JobExecution, JobHandler};
|
||||
use crate::workers::{
|
||||
job_execution_from_task_error,
|
||||
taskflow::{BoxedTask, Task, TaskContext, TaskError, TaskExecutor, TaskPlanner, TaskResult},
|
||||
JobExecution, JobHandler,
|
||||
};
|
||||
|
||||
pub struct ProvisionTenantJob;
|
||||
|
||||
@@ -40,77 +45,120 @@ impl JobHandler for ProvisionTenantJob {
|
||||
job: crate::models::Job,
|
||||
_storage: crate::storage::TenantStorage,
|
||||
) -> JobExecution {
|
||||
let mut conn = match state.db_unscoped() {
|
||||
Ok(conn) => conn,
|
||||
Err(err) => {
|
||||
warn!(job_id = %job.id, error = ?err, "failed to get connection for tenant provisioning");
|
||||
return JobExecution::Retry {
|
||||
delay: std::time::Duration::from_secs(30),
|
||||
error: "database connection unavailable".into(),
|
||||
};
|
||||
}
|
||||
};
|
||||
let members = ProvisionPayload::from_job(&job).unwrap_or_default();
|
||||
let mut context = ProvisionContext::new(
|
||||
job.id,
|
||||
JOB_PROVISION_TENANT,
|
||||
job.tenant_id,
|
||||
state.clone(),
|
||||
members,
|
||||
);
|
||||
|
||||
let tenant = match TenantRepository::get_by_id(&mut conn, job.tenant_id) {
|
||||
Ok(tenant) => tenant,
|
||||
Err(err) => {
|
||||
warn!(job_id = %job.id, error = ?err, "tenant not found for provisioning");
|
||||
return JobExecution::Failed {
|
||||
error: "tenant not found".into(),
|
||||
};
|
||||
}
|
||||
};
|
||||
let planner = ProvisionPlanner;
|
||||
match TaskExecutor::run(&planner, &mut context).await {
|
||||
Ok(()) => JobExecution::Success,
|
||||
Err(err) => job_execution_from_task_error(err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ProvisionContext {
|
||||
job_id: Uuid,
|
||||
job_type: &'static str,
|
||||
tenant_id: Uuid,
|
||||
state: Arc<AppState>,
|
||||
members: Vec<Uuid>,
|
||||
}
|
||||
|
||||
impl ProvisionContext {
|
||||
fn new(
|
||||
job_id: Uuid,
|
||||
job_type: &'static str,
|
||||
tenant_id: Uuid,
|
||||
state: Arc<AppState>,
|
||||
members: Vec<Uuid>,
|
||||
) -> Self {
|
||||
Self {
|
||||
job_id,
|
||||
job_type,
|
||||
tenant_id,
|
||||
state,
|
||||
members,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TaskContext for ProvisionContext {
|
||||
fn job_id(&self) -> Uuid {
|
||||
self.job_id
|
||||
}
|
||||
|
||||
fn job_type(&self) -> &'static str {
|
||||
self.job_type
|
||||
}
|
||||
}
|
||||
|
||||
struct ProvisionPlanner;
|
||||
|
||||
#[async_trait]
|
||||
impl TaskPlanner<ProvisionContext> for ProvisionPlanner {
|
||||
async fn plan(
|
||||
&self,
|
||||
_ctx: &mut ProvisionContext,
|
||||
) -> TaskResult<Vec<BoxedTask<ProvisionContext>>> {
|
||||
Ok(vec![Box::new(ProvisionTask)])
|
||||
}
|
||||
}
|
||||
|
||||
struct ProvisionTask;
|
||||
|
||||
#[async_trait]
|
||||
impl Task<ProvisionContext> for ProvisionTask {
|
||||
fn name(&self) -> &'static str {
|
||||
"provision-tenant"
|
||||
}
|
||||
|
||||
async fn execute(&self, ctx: &mut ProvisionContext) -> TaskResult<()> {
|
||||
let mut conn = ctx
|
||||
.state
|
||||
.db_unscoped()
|
||||
.map_err(|err| TaskError::retry(Duration::from_secs(30), format!("{err:?}")))?;
|
||||
|
||||
let tenant = TenantRepository::get_by_id(&mut conn, ctx.tenant_id).map_err(|err| {
|
||||
TaskError::fail(format!("tenant not found for provisioning: {err:?}"))
|
||||
})?;
|
||||
drop(conn);
|
||||
|
||||
let mut conn = match state.db_for_tenant(tenant.id) {
|
||||
Ok(conn) => conn,
|
||||
Err(err) => {
|
||||
warn!(job_id = %job.id, error = ?err, "failed to scope connection for tenant provisioning");
|
||||
return JobExecution::Retry {
|
||||
delay: std::time::Duration::from_secs(30),
|
||||
error: "tenant connection unavailable".into(),
|
||||
};
|
||||
}
|
||||
};
|
||||
let mut conn = ctx
|
||||
.state
|
||||
.db_for_tenant(tenant.id)
|
||||
.map_err(|err| TaskError::retry(Duration::from_secs(30), format!("{err:?}")))?;
|
||||
|
||||
if tenant.status == TenantStatus::Active {
|
||||
warn!(
|
||||
job_id = %job.id,
|
||||
job_id = %ctx.job_id(),
|
||||
tenant_id = %tenant.id,
|
||||
"tenant already active; skipping provisioning"
|
||||
);
|
||||
return JobExecution::Success;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if tenant.status != TenantStatus::Creating {
|
||||
warn!(
|
||||
job_id = %job.id,
|
||||
tenant_id = %tenant.id,
|
||||
status = %tenant.status.as_str(),
|
||||
"tenant not in creating state; skipping provisioning"
|
||||
);
|
||||
return JobExecution::Failed {
|
||||
error: format!(
|
||||
"tenant status '{}' not eligible for provisioning",
|
||||
tenant.status.as_str()
|
||||
),
|
||||
};
|
||||
return Err(TaskError::fail(format!(
|
||||
"tenant status '{}' not eligible for provisioning",
|
||||
tenant.status.as_str()
|
||||
)));
|
||||
}
|
||||
let endpoint = match &state.config.quickwit_endpoint {
|
||||
Some(endpoint) => endpoint.trim_end_matches('/').to_owned(),
|
||||
None => {
|
||||
warn!(
|
||||
job_id = %job.id,
|
||||
tenant_id = %tenant.id,
|
||||
"quickwit endpoint not configured; retrying"
|
||||
);
|
||||
return JobExecution::Retry {
|
||||
delay: std::time::Duration::from_secs(30),
|
||||
error: "quickwit endpoint not configured".into(),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
let endpoint = ctx
|
||||
.state
|
||||
.config
|
||||
.quickwit_endpoint
|
||||
.as_ref()
|
||||
.map(|value| value.trim_end_matches('/').to_owned())
|
||||
.ok_or_else(|| {
|
||||
TaskError::retry(Duration::from_secs(30), "quickwit endpoint not configured")
|
||||
})?;
|
||||
|
||||
let index_id = tenant
|
||||
.quickwit_index
|
||||
@@ -119,117 +167,69 @@ impl JobHandler for ProvisionTenantJob {
|
||||
.unwrap_or_else(|| format!("documents-{}", tenant.id));
|
||||
|
||||
let client = Client::new();
|
||||
if let Err(err) = ensure_quickwit_index(&client, &endpoint, &index_id).await {
|
||||
warn!(
|
||||
job_id = %job.id,
|
||||
tenant_id = %tenant.id,
|
||||
error = %err,
|
||||
"failed to ensure quickwit index"
|
||||
);
|
||||
return JobExecution::Retry {
|
||||
delay: std::time::Duration::from_secs(30),
|
||||
error: err.to_string(),
|
||||
};
|
||||
}
|
||||
ensure_quickwit_index(&client, &endpoint, &index_id)
|
||||
.await
|
||||
.map_err(|err| TaskError::retry(Duration::from_secs(30), err.to_string()))?;
|
||||
|
||||
let owner_capability_set_id =
|
||||
match ensure_capability_set(&mut conn, tenant.id, owner_capabilities()) {
|
||||
Ok(set) => set.id,
|
||||
Err(err) => {
|
||||
warn!(
|
||||
job_id = %job.id,
|
||||
tenant_id = %tenant.id,
|
||||
error = ?err,
|
||||
"failed to ensure owner capability set during provisioning"
|
||||
);
|
||||
return JobExecution::Retry {
|
||||
delay: std::time::Duration::from_secs(30),
|
||||
error: "owner capability set unavailable".into(),
|
||||
};
|
||||
}
|
||||
ensure_capability_set(&mut conn, tenant.id, owner_capabilities())
|
||||
.map_err(|_| {
|
||||
TaskError::retry(Duration::from_secs(30), "owner capability set unavailable")
|
||||
})?
|
||||
.id;
|
||||
|
||||
ensure_capability_set(&mut conn, tenant.id, user_capabilities()).map_err(|_| {
|
||||
TaskError::retry(Duration::from_secs(30), "user capability set unavailable")
|
||||
})?;
|
||||
ensure_capability_set(&mut conn, tenant.id, readonly_capabilities()).map_err(|_| {
|
||||
TaskError::retry(
|
||||
Duration::from_secs(30),
|
||||
"readonly capability set unavailable",
|
||||
)
|
||||
})?;
|
||||
ensure_capability_set(&mut conn, tenant.id, webdav_capabilities()).map_err(|_| {
|
||||
TaskError::retry(Duration::from_secs(30), "webdav capability set unavailable")
|
||||
})?;
|
||||
|
||||
for member in &ctx.members {
|
||||
let new_membership = NewUserMembership {
|
||||
id: Uuid::new_v4(),
|
||||
user_id: *member,
|
||||
tenant_id: tenant.id,
|
||||
capability_set_id: Some(owner_capability_set_id),
|
||||
};
|
||||
|
||||
if let Err(err) = ensure_capability_set(&mut conn, tenant.id, user_capabilities()) {
|
||||
warn!(
|
||||
job_id = %job.id,
|
||||
tenant_id = %tenant.id,
|
||||
error = ?err,
|
||||
"failed to ensure user capability set during provisioning"
|
||||
);
|
||||
return JobExecution::Retry {
|
||||
delay: std::time::Duration::from_secs(30),
|
||||
error: "user capability set unavailable".into(),
|
||||
};
|
||||
}
|
||||
|
||||
if let Err(err) = ensure_capability_set(&mut conn, tenant.id, readonly_capabilities()) {
|
||||
warn!(
|
||||
job_id = %job.id,
|
||||
tenant_id = %tenant.id,
|
||||
error = ?err,
|
||||
"failed to ensure readonly capability set during provisioning"
|
||||
);
|
||||
return JobExecution::Retry {
|
||||
delay: std::time::Duration::from_secs(30),
|
||||
error: "readonly capability set unavailable".into(),
|
||||
};
|
||||
}
|
||||
|
||||
if let Err(err) = ensure_capability_set(&mut conn, tenant.id, webdav_capabilities()) {
|
||||
warn!(
|
||||
job_id = %job.id,
|
||||
tenant_id = %tenant.id,
|
||||
error = ?err,
|
||||
"failed to ensure webdav capability set during provisioning"
|
||||
);
|
||||
return JobExecution::Retry {
|
||||
delay: std::time::Duration::from_secs(30),
|
||||
error: "webdav capability set unavailable".into(),
|
||||
};
|
||||
}
|
||||
|
||||
if let Some(members) = ProvisionPayload::from_job(&job) {
|
||||
for member in members {
|
||||
let new_membership = NewUserMembership {
|
||||
id: Uuid::new_v4(),
|
||||
user_id: member,
|
||||
tenant_id: tenant.id,
|
||||
capability_set_id: Some(owner_capability_set_id),
|
||||
};
|
||||
|
||||
if let Err(err) = diesel::insert_into(user_memberships::table)
|
||||
.values(&new_membership)
|
||||
.on_conflict((user_memberships::user_id, user_memberships::tenant_id))
|
||||
.do_nothing()
|
||||
.execute(&mut conn)
|
||||
{
|
||||
warn!(
|
||||
job_id = %job.id,
|
||||
tenant_id = %tenant.id,
|
||||
user_id = %member,
|
||||
error = %err,
|
||||
"failed to assign initial membership"
|
||||
);
|
||||
}
|
||||
if let Err(err) = diesel::insert_into(user_memberships::table)
|
||||
.values(&new_membership)
|
||||
.on_conflict((user_memberships::user_id, user_memberships::tenant_id))
|
||||
.do_nothing()
|
||||
.execute(&mut conn)
|
||||
{
|
||||
warn!(
|
||||
job_id = %ctx.job_id(),
|
||||
tenant_id = %tenant.id,
|
||||
user_id = %member,
|
||||
error = %err,
|
||||
"failed to assign initial membership"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(err) = diesel::update(tenants::table.find(tenant.id))
|
||||
diesel::update(tenants::table.find(tenant.id))
|
||||
.set((
|
||||
tenants::status.eq(TenantStatus::Active),
|
||||
tenants::quickwit_index.eq(Some(index_id)),
|
||||
tenants::updated_at.eq(Utc::now().naive_utc()),
|
||||
))
|
||||
.execute(&mut conn)
|
||||
{
|
||||
warn!(job_id = %job.id, error = %err, "failed to activate tenant");
|
||||
return JobExecution::Retry {
|
||||
delay: std::time::Duration::from_secs(30),
|
||||
error: format!("failed to update tenant status: {err}"),
|
||||
};
|
||||
}
|
||||
.map_err(|err| {
|
||||
TaskError::retry(
|
||||
Duration::from_secs(30),
|
||||
format!("failed to update tenant status: {err}"),
|
||||
)
|
||||
})?;
|
||||
|
||||
JobExecution::Success
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user