diff --git a/.gitignore b/.gitignore index cc482ae4..6465b353 100644 --- a/.gitignore +++ b/.gitignore @@ -27,4 +27,4 @@ node_modules tsconfig.tsbuildinfo # Ignore stored SVM keypairs -keypairs/*.json +**/keypairs/*.json diff --git a/auction-server/.sqlx/query-a569bea672a8aec19b216ea16681795eb88699af0401917b77cd32b6e9ac6a4c.json b/auction-server/.sqlx/query-a569bea672a8aec19b216ea16681795eb88699af0401917b77cd32b6e9ac6a4c.json new file mode 100644 index 00000000..b0ad3685 --- /dev/null +++ b/auction-server/.sqlx/query-a569bea672a8aec19b216ea16681795eb88699af0401917b77cd32b6e9ac6a4c.json @@ -0,0 +1,37 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE opportunity SET removal_time = $1, removal_reason = $2 WHERE removal_time IS NULL AND chain_type = $3", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Timestamp", + { + "Custom": { + "name": "opportunity_removal_reason", + "kind": { + "Enum": [ + "expired", + "invalid", + "server_restart" + ] + } + } + }, + { + "Custom": { + "name": "chain_type", + "kind": { + "Enum": [ + "evm", + "svm" + ] + } + } + } + ] + }, + "nullable": [] + }, + "hash": "a569bea672a8aec19b216ea16681795eb88699af0401917b77cd32b6e9ac6a4c" +} diff --git a/auction-server/migrations/20250226174738_removal_reason_alter_type.down.sql b/auction-server/migrations/20250226174738_removal_reason_alter_type.down.sql new file mode 100644 index 00000000..23d853d4 --- /dev/null +++ b/auction-server/migrations/20250226174738_removal_reason_alter_type.down.sql @@ -0,0 +1,7 @@ +UPDATE opportunity SET removal_reason = NULL WHERE removal_reason = 'server_restart'; +CREATE TYPE temp_opportunity_removal_reason AS ENUM ('expired', 'invalid'); +ALTER TABLE opportunity + ALTER COLUMN removal_reason TYPE temp_opportunity_removal_reason + USING removal_reason::text::temp_opportunity_removal_reason; +DROP TYPE IF EXISTS opportunity_removal_reason; +ALTER TYPE temp_opportunity_removal_reason RENAME TO opportunity_removal_reason; diff --git a/auction-server/migrations/20250226174738_removal_reason_alter_type.up.sql b/auction-server/migrations/20250226174738_removal_reason_alter_type.up.sql new file mode 100644 index 00000000..9ca581c8 --- /dev/null +++ b/auction-server/migrations/20250226174738_removal_reason_alter_type.up.sql @@ -0,0 +1 @@ +ALTER TYPE opportunity_removal_reason ADD VALUE 'server_restart'; diff --git a/auction-server/src/opportunity/entities/opportunity.rs b/auction-server/src/opportunity/entities/opportunity.rs index 4285ec58..b1de142e 100644 --- a/auction-server/src/opportunity/entities/opportunity.rs +++ b/auction-server/src/opportunity/entities/opportunity.rs @@ -102,6 +102,7 @@ pub enum OpportunityRemovalReason { // TODO use internal errors instead of RestError #[allow(dead_code)] Invalid(RestError), + ServerRestart, } pub enum OpportunityVerificationResult { @@ -114,6 +115,9 @@ impl From for repository::OpportunityRemovalReason { match reason { OpportunityRemovalReason::Expired => repository::OpportunityRemovalReason::Expired, OpportunityRemovalReason::Invalid(_) => repository::OpportunityRemovalReason::Invalid, + OpportunityRemovalReason::ServerRestart => { + repository::OpportunityRemovalReason::ServerRestart + } } } } diff --git a/auction-server/src/opportunity/repository/clear_opportunities_upon_restart.rs b/auction-server/src/opportunity/repository/clear_opportunities_upon_restart.rs new file mode 100644 index 00000000..f50054fd --- /dev/null +++ b/auction-server/src/opportunity/repository/clear_opportunities_upon_restart.rs @@ -0,0 +1,14 @@ +use { + super::{ + db::OpportunityTable, + InMemoryStore, + Repository, + }, + crate::api::RestError, +}; + +impl> Repository { + pub async fn clear_opportunities_upon_restart(&self) -> Result<(), RestError> { + self.db.clear_opportunities_upon_restart().await + } +} diff --git a/auction-server/src/opportunity/repository/db.rs b/auction-server/src/opportunity/repository/db.rs index df6ef956..d61c6cee 100644 --- a/auction-server/src/opportunity/repository/db.rs +++ b/auction-server/src/opportunity/repository/db.rs @@ -50,6 +50,7 @@ pub trait OpportunityTable { opportunity: &T::Opportunity, reason: OpportunityRemovalReason, ) -> anyhow::Result<()>; + async fn clear_opportunities_upon_restart(&self) -> Result<(), RestError>; } impl OpportunityTable for DB { @@ -167,4 +168,23 @@ impl OpportunityTable for DB { .await?; Ok(()) } + + async fn clear_opportunities_upon_restart(&self) -> Result<(), RestError> { + let now = OffsetDateTime::now_utc(); + let chain_type = + <::ModelMetadata>::get_chain_type(); + sqlx::query!("UPDATE opportunity SET removal_time = $1, removal_reason = $2 WHERE removal_time IS NULL AND chain_type = $3", + PrimitiveDateTime::new(now.date(), now.time()), + OpportunityRemovalReason::ServerRestart as _, + chain_type as _ + ) + .execute(self) + .instrument(info_span!("db_clear_opportunities_upon_restart")) + .await + .map_err(|e| { + tracing::error!("DB: Failed to clear opportunities upon restart: {}", e); + RestError::TemporarilyUnavailable + })?; + Ok(()) + } } diff --git a/auction-server/src/opportunity/repository/mod.rs b/auction-server/src/opportunity/repository/mod.rs index 51e5c6aa..ed738ec1 100644 --- a/auction-server/src/opportunity/repository/mod.rs +++ b/auction-server/src/opportunity/repository/mod.rs @@ -14,6 +14,7 @@ use { mod add_opportunity; mod add_spoof_info; +mod clear_opportunities_upon_restart; mod db; mod get_express_relay_metadata; mod get_in_memory_opportunities; diff --git a/auction-server/src/opportunity/repository/models.rs b/auction-server/src/opportunity/repository/models.rs index 6599bed0..15e28ae1 100644 --- a/auction-server/src/opportunity/repository/models.rs +++ b/auction-server/src/opportunity/repository/models.rs @@ -35,10 +35,11 @@ use { }; #[derive(Clone, Debug, PartialEq, PartialOrd, sqlx::Type)] -#[sqlx(type_name = "opportunity_removal_reason", rename_all = "lowercase")] +#[sqlx(type_name = "opportunity_removal_reason", rename_all = "snake_case")] pub enum OpportunityRemovalReason { Expired, Invalid, + ServerRestart, } #[derive(Clone, Debug, Serialize, Deserialize)] diff --git a/auction-server/src/opportunity/service/clear_opportunities_upon_restart.rs b/auction-server/src/opportunity/service/clear_opportunities_upon_restart.rs new file mode 100644 index 00000000..b7564644 --- /dev/null +++ b/auction-server/src/opportunity/service/clear_opportunities_upon_restart.rs @@ -0,0 +1,13 @@ +use { + super::{ + ChainType, + Service, + }, + crate::api::RestError, +}; + +impl Service { + pub async fn clear_opportunities_upon_restart(&self) -> Result<(), RestError> { + self.repo.clear_opportunities_upon_restart().await + } +} diff --git a/auction-server/src/opportunity/service/mod.rs b/auction-server/src/opportunity/service/mod.rs index 45466951..4c88d20e 100644 --- a/auction-server/src/opportunity/service/mod.rs +++ b/auction-server/src/opportunity/service/mod.rs @@ -50,6 +50,7 @@ use { }; pub mod add_opportunity; +pub mod clear_opportunities_upon_restart; pub mod get_config; pub mod get_live_opportunities; pub mod get_opportunities; diff --git a/auction-server/src/server.rs b/auction-server/src/server.rs index 1484d11e..4c5b1190 100644 --- a/auction-server/src/server.rs +++ b/auction-server/src/server.rs @@ -462,9 +462,14 @@ pub async fn start_server(run_options: RunOptions) -> Result<()> { let store_new = Arc::new(StoreNew::new( store.clone(), opportunity_service_evm, - opportunity_service_svm, + opportunity_service_svm.clone(), auction_services.clone(), )); + // For now, we only want to clear the SVM opportunities in the db on restart. + opportunity_service_svm + .clear_opportunities_upon_restart() + .await + .map_err(|e| anyhow!("Failed to clear opportunities on restart: {:?}", e))?; tokio::join!( async {