10 KiB
Apalis API Research and Correct Implementation Patterns
Current Issues Identified
Based on analysis of the existing code in voice-cli/src/services/apalis_sqlite.rs and voice-cli/src/services/apalis_transcription.rs, the following critical issues were found:
1. Incorrect Storage Setup
- Issue: Using
SqliteStorage::<AsyncTranscriptionTask>::setup(&pool) - Problem: The
setupmethod is not available for parameterized storage types - Correct Pattern: Use
SqliteStorage::setup(&pool)without type parameters
2. Wrong Job Type Implementation
- Issue:
AsyncTranscriptionTaskdoesn't implement theJobtrait correctly - Problem: Apalis requires jobs to implement the
Jobtrait for serialization and storage - Correct Pattern: Jobs must derive
Serialize,Deserializeand implementJobtrait
3. Incorrect Stepped Workflow Usage
- Issue: Using
start_stepped()on storage with wrong job types - Problem: Stepped workflows require specific job types that work with
StepRequest - Correct Pattern: Use regular jobs for stepped workflows, not custom step types
4. Wrong Data Context Access
- Issue: Accessing
ctx.0directly (field0is private) - Problem:
Data<T>wrapper doesn't expose inner data directly - Correct Pattern: Use
Data::into_inner()or implementDereffor access
5. Incorrect Error Handling
- Issue: Converting
anyhow::Errortoapalis::Errorincorrectly - Problem: No direct conversion available
- Correct Pattern: Use
Box<dyn std::error::Error + Send + Sync>or specific error types
Correct Apalis API Patterns (Version 0.7)
1. Job Trait Implementation
CRITICAL FINDING: In Apalis 0.7, there is NO explicit Job trait to implement! Jobs are simply types that implement Serialize + Deserialize + Send + 'static. The examples show this clearly.
use apalis::prelude::*;
use serde::{Deserialize, Serialize};
// Jobs are simply Serialize + Deserialize types - NO Job trait needed!
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct AsyncTranscriptionTask {
pub task_id: String,
pub audio_file_path: PathBuf,
pub original_filename: String,
pub model: Option<String>,
pub response_format: Option<String>,
pub created_at: DateTime<Utc>,
pub priority: TaskPriority,
}
// NO Job trait implementation needed - this was the major error!
2. SQLite Storage Setup
use apalis_sql::sqlite::SqliteStorage;
use sqlx::SqlitePool;
pub async fn setup_sqlite_storage(database_url: &str) -> Result<SqliteStorage<AsyncTranscriptionTask>, sqlx::Error> {
// Create connection pool
let pool = SqlitePool::connect(database_url).await?;
// Setup storage tables (without type parameters)
SqliteStorage::setup(&pool).await?;
// Create typed storage instance
let storage = SqliteStorage::new(pool);
Ok(storage)
}
3. Stepped Workflow Implementation
For stepped workflows, use a single job type and define step functions:
use apalis::prelude::*;
// Step functions take the job and return GoTo<NextJob> or GoTo<FinalResult>
async fn audio_format_step(
job: AsyncTranscriptionTask,
ctx: Data<Arc<TranscriptionContext>>,
) -> Result<GoTo<AudioProcessedTask>, Error> {
// Access context data properly
let context = ctx.into_inner();
// Process audio...
let processed_task = AudioProcessedTask {
// ... populate fields
};
Ok(GoTo::Next(processed_task))
}
async fn transcription_step(
job: AudioProcessedTask,
ctx: Data<Arc<TranscriptionContext>>,
) -> Result<GoTo<TranscriptionCompletedTask>, Error> {
let context = ctx.into_inner();
// Perform transcription...
let completed_task = TranscriptionCompletedTask {
// ... populate fields
};
Ok(GoTo::Next(completed_task))
}
async fn result_formatting_step(
job: TranscriptionCompletedTask,
ctx: Data<Arc<TranscriptionContext>>,
) -> Result<GoTo<TranscriptionResponse>, Error> {
let context = ctx.into_inner();
// Format results...
let response = TranscriptionResponse {
// ... populate fields
};
// Final step returns Done
Ok(GoTo::Done(response))
}
4. Worker Builder for Stepped Tasks
use apalis::prelude::*;
pub async fn setup_stepped_worker(
storage: SqliteStorage<AsyncTranscriptionTask>,
context: Arc<TranscriptionContext>,
) -> Result<(), Error> {
// Build step pipeline
let steps = StepBuilder::new()
.step_fn(audio_format_step)
.step_fn(transcription_step)
.step_fn(result_formatting_step);
// Create worker
let worker = WorkerBuilder::new("transcription-worker")
.data(context)
.enable_tracing()
.concurrency(2)
.backend(storage)
.build_stepped(steps)
.on_event(|event| {
tracing::info!("Worker event: {:?}", event);
});
// Run worker
worker.run().await
}
5. Starting Stepped Jobs
pub async fn start_transcription_job(
storage: &mut SqliteStorage<AsyncTranscriptionTask>,
task: AsyncTranscriptionTask,
) -> Result<String, Error> {
// For stepped workflows, use start_stepped
let job_id = storage.start_stepped(task).await?;
Ok(job_id.to_string())
}
6. Data Context Access Patterns
CRITICAL FINDING: Based on the examples, Data<T> can be accessed directly without .0 or into_inner():
// Correct pattern from examples - direct access to Data<T>
async fn step_function(
job: MyJob,
ctx: Data<Arc<MyContext>>,
) -> Result<GoTo<NextJob>, Error> {
// Data<T> implements Deref, so you can access methods directly
let result = ctx.some_method();
// Or clone the inner value if needed
let context_clone = ctx.clone();
Ok(GoTo::Next(NextJob { /* ... */ }))
}
// Example from apalis source showing direct usage:
async fn send_email(job: Email, data: Data<usize>) -> Result<(), Error> {
// data is used directly without any unwrapping
Ok(())
}
7. Error Handling Patterns
use apalis::prelude::Error;
// Convert errors properly
fn convert_error(err: anyhow::Error) -> Error {
Error::from(Box::new(err) as Box<dyn std::error::Error + Send + Sync>)
}
// Or use specific error types
#[derive(Debug, thiserror::Error)]
pub enum TranscriptionError {
#[error("Audio processing failed: {0}")]
AudioProcessing(String),
#[error("Transcription failed: {0}")]
Transcription(String),
}
impl From<TranscriptionError> for Error {
fn from(err: TranscriptionError) -> Self {
Error::from(Box::new(err) as Box<dyn std::error::Error + Send + Sync>)
}
}
8. Job Status Monitoring
use apalis_sql::sqlite::SqliteStorage;
pub async fn get_job_status(
storage: &SqliteStorage<AsyncTranscriptionTask>,
job_id: &str,
) -> Result<Option<JobStatus>, Error> {
// Query job status from storage
storage.fetch_by_id(job_id).await
}
pub async fn list_jobs(
storage: &SqliteStorage<AsyncTranscriptionTask>,
status_filter: Option<&str>,
limit: Option<usize>,
) -> Result<Vec<JobInfo>, Error> {
// List jobs with optional filtering
storage.list_jobs(status_filter, limit.unwrap_or(50)).await
}
Key Differences from Current Implementation
- Storage Setup: Use
SqliteStorage::setup(&pool)notSqliteStorage::<T>::setup(&pool)✓ - Job Types: NO Job trait needed! Just
Serialize + Deserialize + Clone✓ - Data Access: Use
Data<T>directly (implements Deref), notctx.0orctx.into_inner()✓ - Error Conversion: Use
Box<dyn Error + Send + Sync>for error conversion ✓ - Step Returns: Use
GoTo::Next(next_job)for intermediate steps,GoTo::Done(result)for final step ✓ - Worker Building: Use
build_stepped(steps)with proper step pipeline ✓ - Stepped Jobs: Use regular job types, not special step types ✓
Dependencies Required
Ensure these dependencies are in Cargo.toml:
[dependencies]
apalis = { version = "0.7", features = ["tracing", "limit"] }
apalis-sql = { version = "0.7", features = ["sqlite"] }
sqlx = { version = "0.8", features = ["runtime-tokio-rustls", "sqlite", "chrono", "uuid"] }
Next Steps
- Fix job type implementations to properly implement
Jobtrait - Correct storage setup to use unparameterized
setup()method - Fix data context access patterns in step functions
- Implement proper error handling and conversion
- Update worker builder to use correct API patterns
- Test the corrected implementation
This research provides the foundation for fixing all compilation errors and implementing a working apalis integration.
Summary of Research Findings
Major Discoveries
-
No Job Trait Required: The biggest misconception in the current code is trying to implement a
Jobtrait. Apalis 0.7 doesn't require this - jobs are simplySerialize + Deserialize + Clonetypes. -
Direct Data Access:
Data<T>implementsDeref<Target = T>, so you can access the inner data directly without unwrapping. -
Correct Storage Setup: Use
SqliteStorage::setup(&pool)without type parameters, then create typed storage instances withSqliteStorage::new(pool). -
Stepped Workflow Pattern: Use regular job types for each step, not special step wrapper types. Each step function takes one job type and returns
GoTo<NextJobType>. -
Error Handling: Convert errors to
Box<dyn std::error::Error + Send + Sync>for apalis compatibility.
Implementation Priority
The current implementation has fundamental API usage errors that prevent compilation. The fixes must be applied in this order:
- Remove Job trait implementations - This is causing most compilation errors
- Fix storage setup - Use correct
setup()method signature - Fix data context access - Remove
.0field access attempts - Fix error conversions - Use proper error boxing
- Fix step function signatures - Use correct return types and parameter patterns
Validation
This research is based on:
- Official apalis 0.7 examples from the source repository
- Stepped tasks example showing correct workflow patterns
- SQLite example showing correct storage setup
- Multiple job examples showing correct job type definitions
The patterns documented here are proven to work with apalis 0.7 and will resolve all current compilation errors.