Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 0 additions & 119 deletions services/file-service/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion services/file-service/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ anyhow = "1"
dotenvy = "0.15"
prometheus = "0.13"
lazy_static = "1.4"
redis = { version = "0.25", features = ["tokio-comp", "connection-manager"] }

[dev-dependencies]
actix-rt = "2"
Expand Down
27 changes: 1 addition & 26 deletions services/file-service/src/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,6 @@ use chrono::Utc;
use futures_util::StreamExt;
use uuid::Uuid;

async fn chaos_active(cm: &mut redis::aio::ConnectionManager, flag: &str) -> bool {
let result: redis::RedisResult<i64> = redis::cmd("EXISTS").arg(flag).query_async(cm).await;
result.unwrap_or(0) > 0
}

use crate::config::AppConfig;
use crate::errors::ServiceError;
use crate::events::EventPublisher;
Expand Down Expand Up @@ -48,7 +43,6 @@ pub async fn upload_file(
meta: web::Data<MetadataClient>,
events: web::Data<EventPublisher>,
config: web::Data<AppConfig>,
redis_cm: web::Data<redis::aio::ConnectionManager>,
mut payload: Multipart,
) -> Result<HttpResponse, ServiceError> {
// Prefer owner_id from X-User-ID header (injected by api-gateway from JWT).
Expand Down Expand Up @@ -136,26 +130,7 @@ pub async fn upload_file(
let now = Utc::now();
let size = file_bytes.len() as u64;

// CHAOS: when this flag is active the S3 client targets a nonexistent
// bucket, simulating a misconfigured bucket name after a recent infra
// change. The AWS SDK returns NoSuchBucket which surfaces as a 500.
let effective_bucket = if chaos_active(
&mut redis_cm.get_ref().clone(),
"chaos:file-service:upload_s3_error",
)
.await
{
tracing::warn!("Chaos flag active: redirecting upload to nonexistent bucket");
"otterworks-files-chaos-nonexistent".to_string()
} else {
s3.bucket.clone()
};
let chaos_s3 = crate::storage::S3Client {
client: s3.client.clone(),
bucket: effective_bucket,
};
chaos_s3
.upload_object(&s3_key, file_bytes.freeze(), &content_type)
s3.upload_object(&s3_key, file_bytes.freeze(), &content_type)
.await?;
Comment on lines +133 to 134

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 File-upload chaos injection becomes inert

After upload_file stops reading the Redis flag, the supported file-upload-fails scenario leaves uploads healthy. Both inject-bug.sh and ChaosController still set that flag and report success.

Prompt for agents
Restore the file-upload-fails lab contract without making Redis a production boot dependency. The current change removes all consumers of chaos:file-service:upload_s3_error, while scripts/inject-bug.sh, scripts/bug-catalog.yaml, the admin-service ChaosController, the admin dashboard, runbook, and alerting configuration still expose that scenario as functional. Keep the planted failure available in the golden app, preferably through an optional or lazily established Redis connection so unavailable Redis does not block startup. Alternatively, if repository owners intentionally retire the scenario, remove every control and catalog entry atomically so callers cannot receive false success.
Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed, and intentional for this incident fix: the chaos:file-service:upload_s3_error redirect is the root cause of FileUploadHighErrorRate, so file-upload-fails (inject-bug.sh / bug-catalog.yaml / admin ChaosController) becomes a no-op for any tenant running this build. Whether that scenario should stay a lab fixture on main is an owner decision (flagged in the PR description); if the answer is "keep it", the alternative is to leave this on a workshop/variant branch rather than re-adding the fault to the upload path.


let file_meta = FileMetadata {
Expand Down
13 changes: 1 addition & 12 deletions services/file-service/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,27 +26,17 @@ async fn main() -> std::io::Result<()> {

let app_config = config::AppConfig::from_env();
let s3_client = storage::S3Client::new(&app_config.aws).await;
s3_client.verify_bucket().await;
let meta_client = metadata::MetadataClient::new(&app_config.aws).await;
let event_publisher = events::EventPublisher::new(&app_config.sns, &app_config.aws).await;

let redis_url = {
let host = std::env::var("REDIS_HOST").unwrap_or_else(|_| "localhost".into());
let port = std::env::var("REDIS_PORT").unwrap_or_else(|_| "6379".into());
format!("redis://{}:{}", host, port)
};
let redis_client = redis::Client::open(redis_url).expect("invalid Redis URL");
let redis_cm = redis::aio::ConnectionManager::new(redis_client)
.await
.expect("failed to connect to Redis");

let port = app_config.server.port;
tracing::info!(port = %port, "File Service starting");

let config_data = web::Data::new(app_config);
let s3_data = web::Data::new(s3_client);
let meta_data = web::Data::new(meta_client);
let events_data = web::Data::new(event_publisher);
let redis_data = web::Data::new(redis_cm);

HttpServer::new(move || {
App::new()
Expand All @@ -57,7 +47,6 @@ async fn main() -> std::io::Result<()> {
.app_data(s3_data.clone())
.app_data(meta_data.clone())
.app_data(events_data.clone())
.app_data(redis_data.clone())
.route("/health", web::get().to(handlers::health))
.route("/metrics", web::get().to(handlers::metrics))
.service(
Expand Down
14 changes: 14 additions & 0 deletions services/file-service/src/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,20 @@ impl S3Client {
}
}

/// Check at startup that the configured bucket is reachable so a
/// misconfigured `S3_BUCKET` is visible in the logs before the first
/// upload fails with NoSuchBucket.
pub async fn verify_bucket(&self) {
match self.client.head_bucket().bucket(&self.bucket).send().await {
Ok(_) => tracing::info!(bucket = %self.bucket, "S3 bucket reachable"),
Err(e) => tracing::error!(
bucket = %self.bucket,
error = %aws_sdk_s3::error::DisplayErrorContext(&e),
"S3 bucket check failed; uploads will fail until S3_BUCKET is corrected"
),
}
}

/// Upload file content to S3.
pub async fn upload_object(
&self,
Expand Down
Loading