Add multi-database data layer

Introduce aether-data-schema and driver-specific schema generation for Postgres, MySQL, and SQLite.

Split data backends, lifecycle, repositories, and gateway runtime integration across database drivers.

Verified with cargo fmt --all --check, cargo clippy --workspace --all-targets -- -D warnings, and cargo test --workspace.
This commit is contained in:
fawney19
2026-05-05 18:27:36 +08:00
parent 099653f732
commit fce7e959e5
372 changed files with 86217 additions and 21160 deletions

View File

@@ -2,11 +2,11 @@ use std::path::PathBuf;
use std::sync::Arc;
use std::time::{Duration, Instant};
use aether_data::postgres::{
use aether_data::driver::postgres::{
DatabaseRecordId, PostgresLeaseClaimOptions, PostgresLeaseClaimSpec, PostgresLeaseRunnerConfig,
PostgresPoolConfig,
};
use aether_data::redis::{
use aether_data::driver::redis::{
RedisClientConfig, RedisConsumerGroup, RedisConsumerName, RedisLockLease, RedisLockRunner,
RedisLockRunnerConfig, RedisStreamName, RedisStreamReclaimConfig, RedisStreamRunner,
RedisStreamRunnerConfig,
@@ -532,7 +532,7 @@ async fn benchmark_redis_stream_ack_into(
}
async fn benchmark_postgres_lease(
runner: &aether_data::postgres::PostgresLeaseRunner,
runner: &aether_data::driver::postgres::PostgresLeaseRunner,
config: &DependencyPressureBaselineConfig,
) -> Result<PostgresLeasePressureReport, Box<dyn std::error::Error>> {
let spec = PostgresLeaseClaimSpec {
@@ -608,7 +608,7 @@ async fn benchmark_postgres_lease(
}
async fn record_postgres_lease_follow_up(
runner: &aether_data::postgres::PostgresLeaseRunner,
runner: &aether_data::driver::postgres::PostgresLeaseRunner,
spec: &PostgresLeaseClaimSpec,
owner: &str,
ids: &[DatabaseRecordId],

View File

@@ -3,11 +3,11 @@ use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant};
use aether_data::postgres::{
use aether_data::driver::postgres::{
PostgresLeaseClaimOptions, PostgresLeaseClaimSpec, PostgresLeaseRunnerConfig,
PostgresPoolConfig, PostgresTransactionOptions,
};
use aether_data::redis::{RedisClientConfig, RedisLockRunnerConfig};
use aether_data::driver::redis::{RedisClientConfig, RedisLockRunnerConfig};
use aether_data::{DataLayerError, PostgresBackend, RedisBackend};
use aether_testkit::{
init_test_runtime_for, reserve_local_port, ManagedPostgresServer, ManagedRedisServer,

View File

@@ -4,14 +4,13 @@ use std::time::Duration;
use aether_gateway::tunnel_protocol as protocol;
use aether_gateway::GatewayDataConfig;
use aether_testkit::{
init_test_runtime_for, reserve_local_port, run_http_load_probe, wait_until, GatewayHarness,
GatewayHarnessConfig, HttpLoadProbeConfig, HttpLoadProbeResponseMode, HttpLoadProbeResult,
ManagedPostgresServer, ManagedRedisServer,
init_test_runtime_for, prepare_aether_postgres_schema, reserve_local_port, run_http_load_probe,
wait_until, GatewayHarness, GatewayHarnessConfig, HttpLoadProbeConfig,
HttpLoadProbeResponseMode, HttpLoadProbeResult, ManagedPostgresServer, ManagedRedisServer,
};
use futures_util::{SinkExt, StreamExt};
use reqwest::Method;
use serde::Serialize;
use sqlx::{Connection, Executor, PgConnection};
use tokio_tungstenite::tungstenite::client::IntoClientRequest;
use tokio_tungstenite::tungstenite::Message;
@@ -113,7 +112,7 @@ async fn run_suite(
})
.expect("postgres url should be resolved");
ensure_owner_relay_schema(&postgres_url).await?;
prepare_aether_postgres_schema(&postgres_url).await?;
let key_prefix = format!("aether-owner-relay-baseline-{}", std::process::id());
let shared_data = GatewayDataConfig::from_postgres_url(postgres_url.clone(), false)
@@ -206,89 +205,6 @@ async fn run_suite(
})
}
async fn ensure_owner_relay_schema(postgres_url: &str) -> Result<(), Box<dyn std::error::Error>> {
let mut connection = PgConnection::connect(postgres_url).await?;
connection
.execute(
r#"
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'proxynodestatus') THEN
CREATE TYPE proxynodestatus AS ENUM ('online', 'offline');
END IF;
END
$$;
"#,
)
.await?;
connection
.execute(
r#"
CREATE TABLE IF NOT EXISTS system_configs (
id VARCHAR(36) PRIMARY KEY,
key VARCHAR(100) UNIQUE NOT NULL,
value JSON NOT NULL,
description TEXT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
)
"#,
)
.await?;
connection
.execute(
r#"
CREATE TABLE IF NOT EXISTS proxy_nodes (
id VARCHAR(36) PRIMARY KEY,
name VARCHAR(100) NOT NULL,
ip VARCHAR(512) NOT NULL,
port INTEGER NOT NULL,
region VARCHAR(100) NULL,
is_manual BOOLEAN NOT NULL DEFAULT FALSE,
proxy_url VARCHAR(500) NULL,
proxy_username VARCHAR(255) NULL,
proxy_password VARCHAR(500) NULL,
status proxynodestatus NOT NULL DEFAULT 'online',
registered_by VARCHAR(36) NULL,
last_heartbeat_at TIMESTAMPTZ NULL,
heartbeat_interval INTEGER NOT NULL DEFAULT 30,
active_connections INTEGER NOT NULL DEFAULT 0,
total_requests BIGINT NOT NULL DEFAULT 0,
avg_latency_ms DOUBLE PRECISION NULL,
failed_requests BIGINT NOT NULL DEFAULT 0,
dns_failures BIGINT NOT NULL DEFAULT 0,
stream_errors BIGINT NOT NULL DEFAULT 0,
proxy_metadata JSON NULL,
hardware_info JSON NULL,
estimated_max_concurrency INTEGER NULL,
tunnel_mode BOOLEAN NOT NULL DEFAULT FALSE,
tunnel_connected BOOLEAN NOT NULL DEFAULT FALSE,
tunnel_connected_at TIMESTAMPTZ NULL,
remote_config JSON NULL,
config_version INTEGER NOT NULL DEFAULT 1,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
)
"#,
)
.await?;
connection
.execute(
r#"
CREATE TABLE IF NOT EXISTS proxy_node_events (
id BIGSERIAL PRIMARY KEY,
node_id VARCHAR(36) NOT NULL,
event_type TEXT NOT NULL,
detail TEXT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
)
"#,
)
.await?;
connection.close().await?;
Ok(())
}
async fn wait_for_owner_attachment(forwarder_base_url: &str) -> Result<(), String> {
let client = reqwest::Client::builder()
.timeout(Duration::from_secs(2))

View File

@@ -1,7 +1,7 @@
use std::path::PathBuf;
use std::time::{Duration, Instant};
use aether_data::redis::{
use aether_data::driver::redis::{
RedisClientConfig, RedisConsumerGroup, RedisConsumerName, RedisStreamReclaimConfig,
RedisStreamRunnerConfig,
};
@@ -124,8 +124,8 @@ async fn run_suite(
}
async fn benchmark_append(
runner: &aether_data::redis::RedisStreamRunner,
stream: &aether_data::redis::RedisStreamName,
runner: &aether_data::driver::redis::RedisStreamRunner,
stream: &aether_data::driver::redis::RedisStreamName,
config: &RedisWorkerBaselineConfig,
) -> Result<OperationSummary, Box<dyn std::error::Error>> {
let next = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
@@ -182,8 +182,8 @@ async fn benchmark_append(
}
async fn benchmark_read_group(
runner: &aether_data::redis::RedisStreamRunner,
stream: &aether_data::redis::RedisStreamName,
runner: &aether_data::driver::redis::RedisStreamRunner,
stream: &aether_data::driver::redis::RedisStreamName,
group: &RedisConsumerGroup,
consumer: &RedisConsumerName,
config: &RedisWorkerBaselineConfig,
@@ -211,8 +211,8 @@ async fn benchmark_read_group(
}
async fn benchmark_reclaim(
runner: &aether_data::redis::RedisStreamRunner,
stream: &aether_data::redis::RedisStreamName,
runner: &aether_data::driver::redis::RedisStreamRunner,
stream: &aether_data::driver::redis::RedisStreamName,
group: &RedisConsumerGroup,
consumer_a: &RedisConsumerName,
consumer_b: &RedisConsumerName,
@@ -283,8 +283,8 @@ async fn benchmark_reclaim(
}
async fn benchmark_ack(
runner: &aether_data::redis::RedisStreamRunner,
stream: &aether_data::redis::RedisStreamName,
runner: &aether_data::driver::redis::RedisStreamRunner,
stream: &aether_data::driver::redis::RedisStreamName,
group: &RedisConsumerGroup,
ids: &[String],
) -> Result<OperationSummary, Box<dyn std::error::Error>> {

View File

@@ -22,7 +22,7 @@ pub use load::{
pub use metrics::{
fetch_prometheus_samples, find_metric_value_u64, parse_prometheus_samples, PrometheusSample,
};
pub use postgres::ManagedPostgresServer;
pub use postgres::{prepare_aether_postgres_schema, ManagedPostgresServer};
pub use redis::ManagedRedisServer;
pub use server::{reserve_local_port, SpawnedServer};
pub use tracing::{init_test_runtime, init_test_runtime_for, test_runtime_config};

View File

@@ -1,6 +1,8 @@
use std::path::PathBuf;
use std::process::{Child, Command, Stdio};
use aether_data::driver::postgres::PostgresPoolConfig;
use aether_data::{DataBackends, DataLayerConfig};
use sqlx::{Connection, PgConnection};
use crate::wait_until;
@@ -140,6 +142,26 @@ impl Drop for ManagedPostgresServer {
}
}
pub async fn prepare_aether_postgres_schema(
database_url: &str,
) -> Result<(), Box<dyn std::error::Error>> {
let config = PostgresPoolConfig {
database_url: database_url.to_string(),
..Default::default()
};
let backends = DataBackends::from_config(DataLayerConfig::from_postgres(config))?;
let pending_migrations = backends
.prepare_database_for_startup()
.await?
.unwrap_or_default();
if !pending_migrations.is_empty() {
backends.run_database_migrations().await?;
}
Ok(())
}
fn reserve_local_port() -> Result<u16, std::io::Error> {
let listener = std::net::TcpListener::bind("127.0.0.1:0")?;
let port = listener.local_addr()?.port();