chore: recreate group if it isn't exist (#1001)
This commit is contained in:
parent
7c6a706bbd
commit
dcbc84dacc
|
|
@ -16,6 +16,9 @@ pub enum WorkerError {
|
||||||
#[error("S3 service unavailable: {0}")]
|
#[error("S3 service unavailable: {0}")]
|
||||||
S3ServiceUnavailable(String),
|
S3ServiceUnavailable(String),
|
||||||
|
|
||||||
|
#[error("Redis stream group not exist: {0}")]
|
||||||
|
StreamGroupNotExist(String),
|
||||||
|
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Internal(#[from] anyhow::Error),
|
Internal(#[from] anyhow::Error),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -80,10 +80,7 @@ pub async fn run_import_worker(
|
||||||
tick_interval_secs: u64,
|
tick_interval_secs: u64,
|
||||||
) -> Result<(), ImportError> {
|
) -> Result<(), ImportError> {
|
||||||
info!("Starting importer worker");
|
info!("Starting importer worker");
|
||||||
if let Err(err) = ensure_consumer_group(stream_name, GROUP_NAME, &mut redis_client)
|
if let Err(err) = ensure_consumer_group(stream_name, GROUP_NAME, &mut redis_client).await {
|
||||||
.await
|
|
||||||
.map_err(ImportError::Internal)
|
|
||||||
{
|
|
||||||
error!("Failed to ensure consumer group: {:?}", err);
|
error!("Failed to ensure consumer group: {:?}", err);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -179,6 +176,7 @@ async fn process_upcoming_tasks(
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
interval.tick().await;
|
interval.tick().await;
|
||||||
|
|
||||||
let tasks: StreamReadReply = match redis_client
|
let tasks: StreamReadReply = match redis_client
|
||||||
.xread_options(&[stream_name], &[">"], &options)
|
.xread_options(&[stream_name], &[">"], &options)
|
||||||
.await
|
.await
|
||||||
|
|
@ -186,6 +184,17 @@ async fn process_upcoming_tasks(
|
||||||
Ok(tasks) => tasks,
|
Ok(tasks) => tasks,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!("Failed to read tasks from Redis stream: {:?}", err);
|
error!("Failed to read tasks from Redis stream: {:?}", err);
|
||||||
|
|
||||||
|
// Use command:
|
||||||
|
// docker exec -it appflowy-cloud-redis-1 redis-cli FLUSHDB to generate the error
|
||||||
|
// NOGROUP: No such key 'import_task_stream' or consumer group 'import_task_group' in XREADGROUP with GROUP option
|
||||||
|
if let Some(code) = err.code() {
|
||||||
|
if code == "NOGROUP" {
|
||||||
|
if let Err(err) = ensure_consumer_group(stream_name, GROUP_NAME, redis_client).await {
|
||||||
|
error!("Failed to ensure consumer group: {:?}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
continue;
|
continue;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
@ -198,6 +207,7 @@ async fn process_upcoming_tasks(
|
||||||
Ok(import_task) => {
|
Ok(import_task) => {
|
||||||
let stream_name = stream_name.to_string();
|
let stream_name = stream_name.to_string();
|
||||||
let group_name = group_name.to_string();
|
let group_name = group_name.to_string();
|
||||||
|
|
||||||
let context = TaskContext {
|
let context = TaskContext {
|
||||||
storage_dir: storage_dir.to_path_buf(),
|
storage_dir: storage_dir.to_path_buf(),
|
||||||
redis_client: redis_client.clone(),
|
redis_client: redis_client.clone(),
|
||||||
|
|
@ -206,7 +216,8 @@ async fn process_upcoming_tasks(
|
||||||
notifier: notifier.clone(),
|
notifier: notifier.clone(),
|
||||||
metrics: metrics.clone(),
|
metrics: metrics.clone(),
|
||||||
};
|
};
|
||||||
task_handlers.push(spawn_local(async move {
|
|
||||||
|
let handle = spawn_local(async move {
|
||||||
consume_task(
|
consume_task(
|
||||||
context,
|
context,
|
||||||
import_task,
|
import_task,
|
||||||
|
|
@ -216,7 +227,8 @@ async fn process_upcoming_tasks(
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
Ok::<(), ImportError>(())
|
Ok::<(), ImportError>(())
|
||||||
}));
|
});
|
||||||
|
task_handlers.push(handle);
|
||||||
},
|
},
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!("Failed to deserialize task: {:?}", err);
|
error!("Failed to deserialize task: {:?}", err);
|
||||||
|
|
@ -233,6 +245,7 @@ async fn process_upcoming_tasks(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
info!("[Import] stop reading tasks from stream");
|
||||||
}
|
}
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct TaskContext {
|
struct TaskContext {
|
||||||
|
|
@ -280,8 +293,12 @@ async fn consume_task(
|
||||||
if task.last_process_at.is_none() {
|
if task.last_process_at.is_none() {
|
||||||
task.last_process_at = Some(Utc::now().timestamp());
|
task.last_process_at = Some(Utc::now().timestamp());
|
||||||
}
|
}
|
||||||
|
process_and_ack_task(context, import_task, stream_name, group_name, &entry_id).await
|
||||||
} else {
|
} else {
|
||||||
trace!("[Import] {} file not found, queue task", task.workspace_id);
|
info!(
|
||||||
|
"[Import] {} zip file not found, queue task",
|
||||||
|
task.workspace_id
|
||||||
|
);
|
||||||
push_task(
|
push_task(
|
||||||
&mut context.redis_client,
|
&mut context.redis_client,
|
||||||
stream_name,
|
stream_name,
|
||||||
|
|
@ -290,12 +307,12 @@ async fn consume_task(
|
||||||
&entry_id,
|
&entry_id,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
return Ok(());
|
Ok(())
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// If the task is not a notion task, proceed directly to processing
|
||||||
|
process_and_ack_task(context, import_task, stream_name, group_name, &entry_id).await
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process and acknowledge the task
|
|
||||||
process_and_ack_task(context, import_task, stream_name, group_name, &entry_id).await
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_expired_task(
|
async fn handle_expired_task(
|
||||||
|
|
@ -308,7 +325,7 @@ async fn handle_expired_task(
|
||||||
reason: &str,
|
reason: &str,
|
||||||
) -> Result<(), ImportError> {
|
) -> Result<(), ImportError> {
|
||||||
info!(
|
info!(
|
||||||
"[Import]: {} import is expired with reason:{}, delete workspace",
|
"[Import]: {} import is expired with reason:{}",
|
||||||
task.workspace_id, reason
|
task.workspace_id, reason
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -323,6 +340,7 @@ async fn handle_expired_task(
|
||||||
ImportError::Internal(e.into())
|
ImportError::Internal(e.into())
|
||||||
})?;
|
})?;
|
||||||
remove_workspace(&import_record.workspace_id, &context.pg_pool).await;
|
remove_workspace(&import_record.workspace_id, &context.pg_pool).await;
|
||||||
|
info!("[Import]: deleted workspace {}", task.workspace_id);
|
||||||
|
|
||||||
if let Err(err) = context.s3_client.delete_blob(task.s3_key.as_str()).await {
|
if let Err(err) = context.s3_client.delete_blob(task.s3_key.as_str()).await {
|
||||||
error!(
|
error!(
|
||||||
|
|
@ -330,7 +348,12 @@ async fn handle_expired_task(
|
||||||
task.workspace_id, err
|
task.workspace_id, err
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
let _ = xack_task(&mut context.redis_client, stream_name, group_name, entry_id).await;
|
if let Err(err) = xack_task(&mut context.redis_client, stream_name, group_name, entry_id).await {
|
||||||
|
error!(
|
||||||
|
"[Import] failed to acknowledge task:{} error:{:?}",
|
||||||
|
task.workspace_id, err
|
||||||
|
);
|
||||||
|
}
|
||||||
notify_user(
|
notify_user(
|
||||||
task,
|
task,
|
||||||
Err(ImportError::UploadFileExpire),
|
Err(ImportError::UploadFileExpire),
|
||||||
|
|
@ -388,7 +411,7 @@ fn is_task_expired(created_timestamp: i64, last_process_at: Option<i64>) -> Resu
|
||||||
|
|
||||||
if elapsed.num_hours() >= hours {
|
if elapsed.num_hours() >= hours {
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
"[Import] task is expired: created_at: {}, last_process_at: {:?}, elapsed: {} hours",
|
"task is expired: created_at: {}, last_process_at: {:?}, elapsed: {} hours",
|
||||||
created_at.format("%m/%d/%y %H:%M"),
|
created_at.format("%m/%d/%y %H:%M"),
|
||||||
last_process_at,
|
last_process_at,
|
||||||
elapsed.num_hours()
|
elapsed.num_hours()
|
||||||
|
|
@ -485,10 +508,7 @@ async fn process_task(
|
||||||
.parse()
|
.parse()
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
|
|
||||||
info!(
|
info!("[Import]: Processing task: {}", import_task);
|
||||||
"[Import]: Processing task: {}, retry interval: {}, streaming: {}",
|
|
||||||
import_task, retry_interval, streaming
|
|
||||||
);
|
|
||||||
|
|
||||||
match import_task {
|
match import_task {
|
||||||
ImportTask::Notion(task) => {
|
ImportTask::Notion(task) => {
|
||||||
|
|
@ -1285,7 +1305,7 @@ async fn ensure_consumer_group(
|
||||||
stream_key: &str,
|
stream_key: &str,
|
||||||
group_name: &str,
|
group_name: &str,
|
||||||
redis_client: &mut ConnectionManager,
|
redis_client: &mut ConnectionManager,
|
||||||
) -> Result<(), anyhow::Error> {
|
) -> Result<(), WorkerError> {
|
||||||
let result: RedisResult<()> = redis_client
|
let result: RedisResult<()> = redis_client
|
||||||
.xgroup_create_mkstream(stream_key, group_name, "0")
|
.xgroup_create_mkstream(stream_key, group_name, "0")
|
||||||
.await;
|
.await;
|
||||||
|
|
@ -1293,11 +1313,15 @@ async fn ensure_consumer_group(
|
||||||
if let Err(redis_error) = result {
|
if let Err(redis_error) = result {
|
||||||
if let Some(code) = redis_error.code() {
|
if let Some(code) = redis_error.code() {
|
||||||
if code == "BUSYGROUP" {
|
if code == "BUSYGROUP" {
|
||||||
return Ok(()); // Group already exists, considered as success.
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
if code == "NOGROUP" {
|
||||||
|
return Err(WorkerError::StreamGroupNotExist(group_name.to_string()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
error!("Error when creating consumer group: {:?}", redis_error);
|
error!("Error when creating consumer group: {:?}", redis_error);
|
||||||
return Err(redis_error.into());
|
return Err(WorkerError::Internal(redis_error.into()));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue