Implement FlareDB SQL routing and service CRUD
This commit is contained in:
parent
23ec8b5edb
commit
c17e5a6130
15 changed files with 1290 additions and 218 deletions
|
|
@ -156,7 +156,9 @@ impl LbMetadataStore {
|
||||||
)
|
)
|
||||||
.execute(pool)
|
.execute(pool)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| MetadataError::Storage(format!("Failed to initialize Postgres schema: {}", e)))?;
|
.map_err(|e| {
|
||||||
|
MetadataError::Storage(format!("Failed to initialize Postgres schema: {}", e))
|
||||||
|
})?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -169,7 +171,9 @@ impl LbMetadataStore {
|
||||||
)
|
)
|
||||||
.execute(pool)
|
.execute(pool)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| MetadataError::Storage(format!("Failed to initialize SQLite schema: {}", e)))?;
|
.map_err(|e| {
|
||||||
|
MetadataError::Storage(format!("Failed to initialize SQLite schema: {}", e))
|
||||||
|
})?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -196,9 +200,7 @@ impl LbMetadataStore {
|
||||||
.bind(value)
|
.bind(value)
|
||||||
.execute(pool.as_ref())
|
.execute(pool.as_ref())
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| MetadataError::Storage(format!("Postgres put failed: {}", e)))?;
|
||||||
MetadataError::Storage(format!("Postgres put failed: {}", e))
|
|
||||||
})?;
|
|
||||||
}
|
}
|
||||||
SqlStorageBackend::Sqlite(pool) => {
|
SqlStorageBackend::Sqlite(pool) => {
|
||||||
sqlx::query(
|
sqlx::query(
|
||||||
|
|
@ -439,6 +441,10 @@ impl LbMetadataStore {
|
||||||
format!("/fiberlb/healthchecks/{}/{}", pool_id, hc_id)
|
format!("/fiberlb/healthchecks/{}/{}", pool_id, hc_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn health_check_id_key(hc_id: &HealthCheckId) -> String {
|
||||||
|
format!("/fiberlb/healthcheck_ids/{}", hc_id)
|
||||||
|
}
|
||||||
|
|
||||||
fn health_check_prefix(pool_id: &PoolId) -> String {
|
fn health_check_prefix(pool_id: &PoolId) -> String {
|
||||||
format!("/fiberlb/healthchecks/{}/", pool_id)
|
format!("/fiberlb/healthchecks/{}/", pool_id)
|
||||||
}
|
}
|
||||||
|
|
@ -865,7 +871,8 @@ impl LbMetadataStore {
|
||||||
MetadataError::Serialization(format!("Failed to serialize health check: {}", e))
|
MetadataError::Serialization(format!("Failed to serialize health check: {}", e))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
self.put(&key, &value).await
|
self.put(&key, &value).await?;
|
||||||
|
self.put(&Self::health_check_id_key(&hc.id), &key).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load health check
|
/// Load health check
|
||||||
|
|
@ -886,6 +893,30 @@ impl LbMetadataStore {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Load health check by ID using the global ID index.
|
||||||
|
pub async fn load_health_check_by_id(
|
||||||
|
&self,
|
||||||
|
hc_id: &HealthCheckId,
|
||||||
|
) -> Result<Option<HealthCheck>> {
|
||||||
|
let id_key = Self::health_check_id_key(hc_id);
|
||||||
|
|
||||||
|
if let Some(hc_key) = self.get(&id_key).await? {
|
||||||
|
if let Some(value) = self.get(&hc_key).await? {
|
||||||
|
let hc: HealthCheck = serde_json::from_str(&value).map_err(|e| {
|
||||||
|
MetadataError::Serialization(format!(
|
||||||
|
"Failed to deserialize health check: {}",
|
||||||
|
e
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
Ok(Some(hc))
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// List health checks for a pool
|
/// List health checks for a pool
|
||||||
pub async fn list_health_checks(&self, pool_id: &PoolId) -> Result<Vec<HealthCheck>> {
|
pub async fn list_health_checks(&self, pool_id: &PoolId) -> Result<Vec<HealthCheck>> {
|
||||||
let prefix = Self::health_check_prefix(pool_id);
|
let prefix = Self::health_check_prefix(pool_id);
|
||||||
|
|
@ -907,7 +938,8 @@ impl LbMetadataStore {
|
||||||
/// Delete health check
|
/// Delete health check
|
||||||
pub async fn delete_health_check(&self, hc: &HealthCheck) -> Result<()> {
|
pub async fn delete_health_check(&self, hc: &HealthCheck) -> Result<()> {
|
||||||
let key = Self::health_check_key(&hc.pool_id, &hc.id);
|
let key = Self::health_check_key(&hc.pool_id, &hc.id);
|
||||||
self.delete_key(&key).await
|
self.delete_key(&key).await?;
|
||||||
|
self.delete_key(&Self::health_check_id_key(&hc.id)).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Delete all health checks for a pool
|
/// Delete all health checks for a pool
|
||||||
|
|
@ -1215,7 +1247,7 @@ fn normalize_transport_addr(endpoint: &str) -> String {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use fiberlb_types::{ListenerProtocol, PoolAlgorithm, PoolProtocol};
|
use fiberlb_types::{HealthCheck, ListenerProtocol, PoolAlgorithm, PoolProtocol};
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_lb_crud() {
|
async fn test_lb_crud() {
|
||||||
|
|
@ -1355,6 +1387,55 @@ mod tests {
|
||||||
assert!(deleted.is_none());
|
assert!(deleted.is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_health_check_crud() {
|
||||||
|
let store = LbMetadataStore::new_in_memory();
|
||||||
|
|
||||||
|
let lb = LoadBalancer::new("test-lb", "test-org", "test-project");
|
||||||
|
store.save_lb(&lb).await.unwrap();
|
||||||
|
|
||||||
|
let pool = Pool::new(
|
||||||
|
"web-pool",
|
||||||
|
lb.id,
|
||||||
|
PoolAlgorithm::RoundRobin,
|
||||||
|
PoolProtocol::Http,
|
||||||
|
);
|
||||||
|
store.save_pool(&pool).await.unwrap();
|
||||||
|
|
||||||
|
let hc = HealthCheck::new_http("http-check", pool.id, "/healthz");
|
||||||
|
store.save_health_check(&hc).await.unwrap();
|
||||||
|
|
||||||
|
let loaded = store
|
||||||
|
.load_health_check(&pool.id, &hc.id)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(loaded.id, hc.id);
|
||||||
|
assert_eq!(loaded.name, "http-check");
|
||||||
|
|
||||||
|
let loaded_by_id = store
|
||||||
|
.load_health_check_by_id(&hc.id)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(loaded_by_id.pool_id, pool.id);
|
||||||
|
|
||||||
|
let checks = store.list_health_checks(&pool.id).await.unwrap();
|
||||||
|
assert_eq!(checks.len(), 1);
|
||||||
|
|
||||||
|
store.delete_health_check(&hc).await.unwrap();
|
||||||
|
assert!(store
|
||||||
|
.load_health_check(&pool.id, &hc.id)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.is_none());
|
||||||
|
assert!(store
|
||||||
|
.load_health_check_by_id(&hc.id)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_cascade_delete() {
|
async fn test_cascade_delete() {
|
||||||
let store = LbMetadataStore::new_in_memory();
|
let store = LbMetadataStore::new_in_memory();
|
||||||
|
|
|
||||||
|
|
@ -2,17 +2,15 @@
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use base64::Engine as _;
|
|
||||||
use crate::metadata::LbMetadataStore;
|
use crate::metadata::LbMetadataStore;
|
||||||
|
use base64::Engine as _;
|
||||||
use fiberlb_api::{
|
use fiberlb_api::{
|
||||||
health_check_service_server::HealthCheckService,
|
health_check_service_server::HealthCheckService, CreateHealthCheckRequest,
|
||||||
CreateHealthCheckRequest, CreateHealthCheckResponse,
|
CreateHealthCheckResponse, DeleteHealthCheckRequest, DeleteHealthCheckResponse,
|
||||||
DeleteHealthCheckRequest, DeleteHealthCheckResponse,
|
GetHealthCheckRequest, GetHealthCheckResponse, HealthCheck as ProtoHealthCheck,
|
||||||
GetHealthCheckRequest, GetHealthCheckResponse,
|
HealthCheckType as ProtoHealthCheckType, HttpHealthConfig as ProtoHttpHealthConfig,
|
||||||
ListHealthChecksRequest, ListHealthChecksResponse,
|
ListHealthChecksRequest, ListHealthChecksResponse, UpdateHealthCheckRequest,
|
||||||
UpdateHealthCheckRequest, UpdateHealthCheckResponse,
|
UpdateHealthCheckResponse,
|
||||||
HealthCheck as ProtoHealthCheck, HealthCheckType as ProtoHealthCheckType,
|
|
||||||
HttpHealthConfig as ProtoHttpHealthConfig,
|
|
||||||
};
|
};
|
||||||
use fiberlb_types::{HealthCheck, HealthCheckId, HealthCheckType, HttpHealthConfig, PoolId};
|
use fiberlb_types::{HealthCheck, HealthCheckId, HealthCheckType, HttpHealthConfig, PoolId};
|
||||||
use iam_service_auth::{get_tenant_context, resource_for_tenant, AuthService};
|
use iam_service_auth::{get_tenant_context, resource_for_tenant, AuthService};
|
||||||
|
|
@ -33,7 +31,10 @@ impl HealthCheckServiceImpl {
|
||||||
}
|
}
|
||||||
|
|
||||||
const ACTION_HEALTHCHECKS_CREATE: &str = "network:healthchecks:create";
|
const ACTION_HEALTHCHECKS_CREATE: &str = "network:healthchecks:create";
|
||||||
|
const ACTION_HEALTHCHECKS_READ: &str = "network:healthchecks:read";
|
||||||
const ACTION_HEALTHCHECKS_LIST: &str = "network:healthchecks:list";
|
const ACTION_HEALTHCHECKS_LIST: &str = "network:healthchecks:list";
|
||||||
|
const ACTION_HEALTHCHECKS_UPDATE: &str = "network:healthchecks:update";
|
||||||
|
const ACTION_HEALTHCHECKS_DELETE: &str = "network:healthchecks:delete";
|
||||||
|
|
||||||
/// Convert domain HealthCheck to proto
|
/// Convert domain HealthCheck to proto
|
||||||
fn health_check_to_proto(hc: &HealthCheck) -> ProtoHealthCheck {
|
fn health_check_to_proto(hc: &HealthCheck) -> ProtoHealthCheck {
|
||||||
|
|
@ -52,13 +53,11 @@ fn health_check_to_proto(hc: &HealthCheck) -> ProtoHealthCheck {
|
||||||
timeout_seconds: hc.timeout_seconds,
|
timeout_seconds: hc.timeout_seconds,
|
||||||
healthy_threshold: hc.healthy_threshold,
|
healthy_threshold: hc.healthy_threshold,
|
||||||
unhealthy_threshold: hc.unhealthy_threshold,
|
unhealthy_threshold: hc.unhealthy_threshold,
|
||||||
http_config: hc.http_config.as_ref().map(|cfg| {
|
http_config: hc.http_config.as_ref().map(|cfg| ProtoHttpHealthConfig {
|
||||||
ProtoHttpHealthConfig {
|
|
||||||
method: cfg.method.clone(),
|
method: cfg.method.clone(),
|
||||||
path: cfg.path.clone(),
|
path: cfg.path.clone(),
|
||||||
expected_codes: cfg.expected_codes.iter().map(|&c| c as u32).collect(),
|
expected_codes: cfg.expected_codes.iter().map(|&c| c as u32).collect(),
|
||||||
host: cfg.host.clone().unwrap_or_default(),
|
host: cfg.host.clone().unwrap_or_default(),
|
||||||
}
|
|
||||||
}),
|
}),
|
||||||
enabled: hc.enabled,
|
enabled: hc.enabled,
|
||||||
created_at: hc.created_at,
|
created_at: hc.created_at,
|
||||||
|
|
@ -100,7 +99,11 @@ fn proto_to_http_config(cfg: Option<ProtoHttpHealthConfig>) -> Option<HttpHealth
|
||||||
method: c.method,
|
method: c.method,
|
||||||
path: c.path,
|
path: c.path,
|
||||||
expected_codes: c.expected_codes.iter().map(|&c| c as u16).collect(),
|
expected_codes: c.expected_codes.iter().map(|&c| c as u16).collect(),
|
||||||
host: if c.host.is_empty() { None } else { Some(c.host) },
|
host: if c.host.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(c.host)
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -123,40 +126,39 @@ impl HealthCheckService for HealthCheckServiceImpl {
|
||||||
|
|
||||||
let pool_id = parse_pool_id(&req.pool_id)?;
|
let pool_id = parse_pool_id(&req.pool_id)?;
|
||||||
|
|
||||||
let lbs = self
|
let pool = self
|
||||||
.metadata
|
.metadata
|
||||||
.list_lbs(&tenant.org_id, Some(tenant.project_id.as_str()))
|
.load_pool_by_id(&pool_id)
|
||||||
.await
|
|
||||||
.map_err(|e| Status::internal(format!("metadata error: {}", e)))?;
|
|
||||||
|
|
||||||
let mut scope: Option<(String, String)> = None;
|
|
||||||
for lb in &lbs {
|
|
||||||
if let Some(_) = self
|
|
||||||
.metadata
|
|
||||||
.load_pool(&lb.id, &pool_id)
|
|
||||||
.await
|
.await
|
||||||
.map_err(|e| Status::internal(format!("metadata error: {}", e)))?
|
.map_err(|e| Status::internal(format!("metadata error: {}", e)))?
|
||||||
{
|
.ok_or_else(|| Status::not_found("pool not found"))?;
|
||||||
scope = Some((lb.org_id.clone(), lb.project_id.clone()));
|
let lb = self
|
||||||
break;
|
.metadata
|
||||||
|
.load_lb_by_id(&pool.loadbalancer_id)
|
||||||
|
.await
|
||||||
|
.map_err(|e| Status::internal(format!("metadata error: {}", e)))?
|
||||||
|
.ok_or_else(|| Status::not_found("load balancer not found"))?;
|
||||||
|
if lb.org_id != tenant.org_id || lb.project_id != tenant.project_id {
|
||||||
|
return Err(Status::permission_denied("pool not in tenant scope"));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
let (lb_org_id, lb_project_id) =
|
|
||||||
scope.ok_or_else(|| Status::not_found("pool not found"))?;
|
|
||||||
|
|
||||||
self.auth
|
self.auth
|
||||||
.authorize(
|
.authorize(
|
||||||
&tenant,
|
&tenant,
|
||||||
ACTION_HEALTHCHECKS_CREATE,
|
ACTION_HEALTHCHECKS_CREATE,
|
||||||
&resource_for_tenant("health-check", "*", &lb_org_id, &lb_project_id),
|
&resource_for_tenant("health-check", "*", &lb.org_id, &lb.project_id),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
let check_type = proto_to_check_type(req.r#type);
|
let check_type = proto_to_check_type(req.r#type);
|
||||||
|
|
||||||
// Create health check based on type
|
// Create health check based on type
|
||||||
let mut hc = if check_type == HealthCheckType::Http || check_type == HealthCheckType::Https {
|
let mut hc = if check_type == HealthCheckType::Http || check_type == HealthCheckType::Https
|
||||||
let path = req.http_config.as_ref().map(|c| c.path.as_str()).unwrap_or("/health");
|
{
|
||||||
|
let path = req
|
||||||
|
.http_config
|
||||||
|
.as_ref()
|
||||||
|
.map(|c| c.path.as_str())
|
||||||
|
.unwrap_or("/health");
|
||||||
HealthCheck::new_http(&req.name, pool_id, path)
|
HealthCheck::new_http(&req.name, pool_id, path)
|
||||||
} else {
|
} else {
|
||||||
HealthCheck::new_tcp(&req.name, pool_id)
|
HealthCheck::new_tcp(&req.name, pool_id)
|
||||||
|
|
@ -195,18 +197,55 @@ impl HealthCheckService for HealthCheckServiceImpl {
|
||||||
&self,
|
&self,
|
||||||
request: Request<GetHealthCheckRequest>,
|
request: Request<GetHealthCheckRequest>,
|
||||||
) -> Result<Response<GetHealthCheckResponse>, Status> {
|
) -> Result<Response<GetHealthCheckResponse>, Status> {
|
||||||
|
let tenant = get_tenant_context(&request)?;
|
||||||
let req = request.into_inner();
|
let req = request.into_inner();
|
||||||
|
|
||||||
if req.id.is_empty() {
|
if req.id.is_empty() {
|
||||||
return Err(Status::invalid_argument("id is required"));
|
return Err(Status::invalid_argument("id is required"));
|
||||||
}
|
}
|
||||||
|
|
||||||
let _hc_id = parse_hc_id(&req.id)?;
|
let hc_id = parse_hc_id(&req.id)?;
|
||||||
|
let hc = self
|
||||||
|
.metadata
|
||||||
|
.load_health_check_by_id(&hc_id)
|
||||||
|
.await
|
||||||
|
.map_err(|e| Status::internal(format!("metadata error: {}", e)))?
|
||||||
|
.ok_or_else(|| Status::not_found("health check not found"))?;
|
||||||
|
let pool = self
|
||||||
|
.metadata
|
||||||
|
.load_pool_by_id(&hc.pool_id)
|
||||||
|
.await
|
||||||
|
.map_err(|e| Status::internal(format!("metadata error: {}", e)))?
|
||||||
|
.ok_or_else(|| Status::not_found("pool not found"))?;
|
||||||
|
let lb = self
|
||||||
|
.metadata
|
||||||
|
.load_lb_by_id(&pool.loadbalancer_id)
|
||||||
|
.await
|
||||||
|
.map_err(|e| Status::internal(format!("metadata error: {}", e)))?
|
||||||
|
.ok_or_else(|| Status::not_found("load balancer not found"))?;
|
||||||
|
|
||||||
// Need pool_id context to efficiently look up health check
|
if lb.org_id != tenant.org_id || lb.project_id != tenant.project_id {
|
||||||
Err(Status::unimplemented(
|
return Err(Status::permission_denied(
|
||||||
"get_health_check by ID requires pool_id context; use list_health_checks instead",
|
"health check not in tenant scope",
|
||||||
))
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
self.auth
|
||||||
|
.authorize(
|
||||||
|
&tenant,
|
||||||
|
ACTION_HEALTHCHECKS_READ,
|
||||||
|
&resource_for_tenant(
|
||||||
|
"health-check",
|
||||||
|
&hc.id.to_string(),
|
||||||
|
&lb.org_id,
|
||||||
|
&lb.project_id,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(Response::new(GetHealthCheckResponse {
|
||||||
|
health_check: Some(health_check_to_proto(&hc)),
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn list_health_checks(
|
async fn list_health_checks(
|
||||||
|
|
@ -222,33 +261,27 @@ impl HealthCheckService for HealthCheckServiceImpl {
|
||||||
|
|
||||||
let pool_id = parse_pool_id(&req.pool_id)?;
|
let pool_id = parse_pool_id(&req.pool_id)?;
|
||||||
|
|
||||||
let lbs = self
|
let pool = self
|
||||||
.metadata
|
.metadata
|
||||||
.list_lbs(&tenant.org_id, Some(tenant.project_id.as_str()))
|
.load_pool_by_id(&pool_id)
|
||||||
.await
|
|
||||||
.map_err(|e| Status::internal(format!("metadata error: {}", e)))?;
|
|
||||||
|
|
||||||
let mut scope: Option<(String, String)> = None;
|
|
||||||
for lb in &lbs {
|
|
||||||
if let Some(_) = self
|
|
||||||
.metadata
|
|
||||||
.load_pool(&lb.id, &pool_id)
|
|
||||||
.await
|
.await
|
||||||
.map_err(|e| Status::internal(format!("metadata error: {}", e)))?
|
.map_err(|e| Status::internal(format!("metadata error: {}", e)))?
|
||||||
{
|
.ok_or_else(|| Status::not_found("pool not found"))?;
|
||||||
scope = Some((lb.org_id.clone(), lb.project_id.clone()));
|
let lb = self
|
||||||
break;
|
.metadata
|
||||||
|
.load_lb_by_id(&pool.loadbalancer_id)
|
||||||
|
.await
|
||||||
|
.map_err(|e| Status::internal(format!("metadata error: {}", e)))?
|
||||||
|
.ok_or_else(|| Status::not_found("load balancer not found"))?;
|
||||||
|
if lb.org_id != tenant.org_id || lb.project_id != tenant.project_id {
|
||||||
|
return Err(Status::permission_denied("pool not in tenant scope"));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
let (lb_org_id, lb_project_id) =
|
|
||||||
scope.ok_or_else(|| Status::not_found("pool not found"))?;
|
|
||||||
|
|
||||||
self.auth
|
self.auth
|
||||||
.authorize(
|
.authorize(
|
||||||
&tenant,
|
&tenant,
|
||||||
ACTION_HEALTHCHECKS_LIST,
|
ACTION_HEALTHCHECKS_LIST,
|
||||||
&resource_for_tenant("health-check", "*", &lb_org_id, &lb_project_id),
|
&resource_for_tenant("health-check", "*", &lb.org_id, &lb.project_id),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
|
@ -299,31 +332,141 @@ impl HealthCheckService for HealthCheckServiceImpl {
|
||||||
&self,
|
&self,
|
||||||
request: Request<UpdateHealthCheckRequest>,
|
request: Request<UpdateHealthCheckRequest>,
|
||||||
) -> Result<Response<UpdateHealthCheckResponse>, Status> {
|
) -> Result<Response<UpdateHealthCheckResponse>, Status> {
|
||||||
|
let tenant = get_tenant_context(&request)?;
|
||||||
let req = request.into_inner();
|
let req = request.into_inner();
|
||||||
|
|
||||||
if req.id.is_empty() {
|
if req.id.is_empty() {
|
||||||
return Err(Status::invalid_argument("id is required"));
|
return Err(Status::invalid_argument("id is required"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Need pool_id context for update
|
let hc_id = parse_hc_id(&req.id)?;
|
||||||
Err(Status::unimplemented(
|
let mut hc = self
|
||||||
"update_health_check requires pool_id context; include pool_id in request",
|
.metadata
|
||||||
))
|
.load_health_check_by_id(&hc_id)
|
||||||
|
.await
|
||||||
|
.map_err(|e| Status::internal(format!("metadata error: {}", e)))?
|
||||||
|
.ok_or_else(|| Status::not_found("health check not found"))?;
|
||||||
|
let pool = self
|
||||||
|
.metadata
|
||||||
|
.load_pool_by_id(&hc.pool_id)
|
||||||
|
.await
|
||||||
|
.map_err(|e| Status::internal(format!("metadata error: {}", e)))?
|
||||||
|
.ok_or_else(|| Status::not_found("pool not found"))?;
|
||||||
|
let lb = self
|
||||||
|
.metadata
|
||||||
|
.load_lb_by_id(&pool.loadbalancer_id)
|
||||||
|
.await
|
||||||
|
.map_err(|e| Status::internal(format!("metadata error: {}", e)))?
|
||||||
|
.ok_or_else(|| Status::not_found("load balancer not found"))?;
|
||||||
|
|
||||||
|
if lb.org_id != tenant.org_id || lb.project_id != tenant.project_id {
|
||||||
|
return Err(Status::permission_denied(
|
||||||
|
"health check not in tenant scope",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
self.auth
|
||||||
|
.authorize(
|
||||||
|
&tenant,
|
||||||
|
ACTION_HEALTHCHECKS_UPDATE,
|
||||||
|
&resource_for_tenant(
|
||||||
|
"health-check",
|
||||||
|
&hc.id.to_string(),
|
||||||
|
&lb.org_id,
|
||||||
|
&lb.project_id,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if !req.name.is_empty() {
|
||||||
|
hc.name = req.name;
|
||||||
|
}
|
||||||
|
if req.interval_seconds > 0 {
|
||||||
|
hc.interval_seconds = req.interval_seconds;
|
||||||
|
}
|
||||||
|
if req.timeout_seconds > 0 {
|
||||||
|
hc.timeout_seconds = req.timeout_seconds;
|
||||||
|
}
|
||||||
|
if req.healthy_threshold > 0 {
|
||||||
|
hc.healthy_threshold = req.healthy_threshold;
|
||||||
|
}
|
||||||
|
if req.unhealthy_threshold > 0 {
|
||||||
|
hc.unhealthy_threshold = req.unhealthy_threshold;
|
||||||
|
}
|
||||||
|
if req.http_config.is_some() {
|
||||||
|
hc.http_config = proto_to_http_config(req.http_config);
|
||||||
|
}
|
||||||
|
hc.enabled = req.enabled;
|
||||||
|
hc.updated_at = std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.as_secs();
|
||||||
|
|
||||||
|
self.metadata
|
||||||
|
.save_health_check(&hc)
|
||||||
|
.await
|
||||||
|
.map_err(|e| Status::internal(format!("failed to save health check: {}", e)))?;
|
||||||
|
|
||||||
|
Ok(Response::new(UpdateHealthCheckResponse {
|
||||||
|
health_check: Some(health_check_to_proto(&hc)),
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn delete_health_check(
|
async fn delete_health_check(
|
||||||
&self,
|
&self,
|
||||||
request: Request<DeleteHealthCheckRequest>,
|
request: Request<DeleteHealthCheckRequest>,
|
||||||
) -> Result<Response<DeleteHealthCheckResponse>, Status> {
|
) -> Result<Response<DeleteHealthCheckResponse>, Status> {
|
||||||
|
let tenant = get_tenant_context(&request)?;
|
||||||
let req = request.into_inner();
|
let req = request.into_inner();
|
||||||
|
|
||||||
if req.id.is_empty() {
|
if req.id.is_empty() {
|
||||||
return Err(Status::invalid_argument("id is required"));
|
return Err(Status::invalid_argument("id is required"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Need pool_id context for delete
|
let hc_id = parse_hc_id(&req.id)?;
|
||||||
Err(Status::unimplemented(
|
let hc = self
|
||||||
"delete_health_check requires pool_id context; include pool_id in request",
|
.metadata
|
||||||
))
|
.load_health_check_by_id(&hc_id)
|
||||||
|
.await
|
||||||
|
.map_err(|e| Status::internal(format!("metadata error: {}", e)))?
|
||||||
|
.ok_or_else(|| Status::not_found("health check not found"))?;
|
||||||
|
let pool = self
|
||||||
|
.metadata
|
||||||
|
.load_pool_by_id(&hc.pool_id)
|
||||||
|
.await
|
||||||
|
.map_err(|e| Status::internal(format!("metadata error: {}", e)))?
|
||||||
|
.ok_or_else(|| Status::not_found("pool not found"))?;
|
||||||
|
let lb = self
|
||||||
|
.metadata
|
||||||
|
.load_lb_by_id(&pool.loadbalancer_id)
|
||||||
|
.await
|
||||||
|
.map_err(|e| Status::internal(format!("metadata error: {}", e)))?
|
||||||
|
.ok_or_else(|| Status::not_found("load balancer not found"))?;
|
||||||
|
|
||||||
|
if lb.org_id != tenant.org_id || lb.project_id != tenant.project_id {
|
||||||
|
return Err(Status::permission_denied(
|
||||||
|
"health check not in tenant scope",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
self.auth
|
||||||
|
.authorize(
|
||||||
|
&tenant,
|
||||||
|
ACTION_HEALTHCHECKS_DELETE,
|
||||||
|
&resource_for_tenant(
|
||||||
|
"health-check",
|
||||||
|
&hc.id.to_string(),
|
||||||
|
&lb.org_id,
|
||||||
|
&lb.project_id,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
self.metadata
|
||||||
|
.delete_health_check(&hc)
|
||||||
|
.await
|
||||||
|
.map_err(|e| Status::internal(format!("failed to delete health check: {}", e)))?;
|
||||||
|
|
||||||
|
Ok(Response::new(DeleteHealthCheckResponse {}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -585,7 +585,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
info!("FlareDB server starting with health checks enabled");
|
info!("FlareDB server starting with health checks enabled");
|
||||||
|
|
||||||
// Create SQL service
|
// Create SQL service
|
||||||
let sql_service = sql_service::SqlServiceImpl::new(server_config.addr.to_string());
|
let sql_service =
|
||||||
|
sql_service::SqlServiceImpl::new(server_config.addr.to_string(), pd_endpoints.clone());
|
||||||
|
|
||||||
// Configure TLS if enabled
|
// Configure TLS if enabled
|
||||||
let mut server = Server::builder();
|
let mut server = Server::builder();
|
||||||
|
|
|
||||||
|
|
@ -33,8 +33,7 @@ pub fn build_merkle(
|
||||||
let mut start_key = None;
|
let mut start_key = None;
|
||||||
|
|
||||||
while let Some(Ok((k, v))) = iter.next() {
|
while let Some(Ok((k, v))) = iter.next() {
|
||||||
if (k.len() < 4 || k[..4] != prefix)
|
if (k.len() < 4 || k[..4] != prefix) && !k.starts_with(&prefix) {
|
||||||
&& !k.starts_with(&prefix) {
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if start_key.is_none() {
|
if start_key.is_none() {
|
||||||
|
|
|
||||||
|
|
@ -199,9 +199,7 @@ impl PdClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Start watching for metadata changes in the background
|
/// Start watching for metadata changes in the background
|
||||||
pub async fn start_watch(
|
pub async fn start_watch(&mut self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
&mut self,
|
|
||||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|
||||||
let (tx, rx) = mpsc::channel::<WatchRequest>(32);
|
let (tx, rx) = mpsc::channel::<WatchRequest>(32);
|
||||||
|
|
||||||
// Create watch requests for stores and regions prefixes
|
// Create watch requests for stores and regions prefixes
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
|
use crate::config::NamespaceManager;
|
||||||
use crate::store::Store;
|
use crate::store::Store;
|
||||||
use flaredb_raft::{FlareNode, FlareNodeId, FlareTypeConfig};
|
|
||||||
use flaredb_proto::raft_server::raft_service_server::RaftService;
|
use flaredb_proto::raft_server::raft_service_server::RaftService;
|
||||||
use openraft::raft::{AppendEntriesRequest, VoteRequest};
|
|
||||||
use flaredb_proto::raft_server::{
|
use flaredb_proto::raft_server::{
|
||||||
FetchRangeRequest, FetchRangeResponse, ForwardEventualRequest, GetMerkleRequest,
|
FetchRangeRequest, FetchRangeResponse, ForwardEventualRequest, GetMerkleRequest,
|
||||||
GetMerkleResponse, GetModeRequest, GetModeResponse, ListNamespaceModesRequest,
|
GetMerkleResponse, GetModeRequest, GetModeResponse, ListNamespaceModesRequest,
|
||||||
|
|
@ -10,9 +9,10 @@ use flaredb_proto::raft_server::{
|
||||||
OpenRaftVoteRequest, OpenRaftVoteResponse, RaftMessage, RaftResponse,
|
OpenRaftVoteRequest, OpenRaftVoteResponse, RaftMessage, RaftResponse,
|
||||||
UpdateNamespaceModeRequest, UpdateNamespaceModeResponse,
|
UpdateNamespaceModeRequest, UpdateNamespaceModeResponse,
|
||||||
};
|
};
|
||||||
|
use flaredb_raft::{FlareNode, FlareNodeId, FlareTypeConfig};
|
||||||
|
use openraft::raft::{AppendEntriesRequest, VoteRequest};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tonic::{Request, Response, Status};
|
use tonic::{Request, Response, Status};
|
||||||
use crate::config::NamespaceManager;
|
|
||||||
|
|
||||||
pub struct RaftServiceImpl {
|
pub struct RaftServiceImpl {
|
||||||
store: Arc<Store>,
|
store: Arc<Store>,
|
||||||
|
|
@ -204,8 +204,8 @@ impl RaftService for RaftServiceImpl {
|
||||||
.await
|
.await
|
||||||
.ok_or_else(|| Status::failed_precondition("region not found"))?;
|
.ok_or_else(|| Status::failed_precondition("region not found"))?;
|
||||||
|
|
||||||
let append_req: AppendEntriesRequest<FlareTypeConfig> =
|
let append_req: AppendEntriesRequest<FlareTypeConfig> = serde_json::from_slice(&req.data)
|
||||||
serde_json::from_slice(&req.data).map_err(|e| {
|
.map_err(|e| {
|
||||||
Status::invalid_argument(format!("invalid append_entries request: {}", e))
|
Status::invalid_argument(format!("invalid append_entries request: {}", e))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,13 +8,13 @@
|
||||||
//! - GET /api/v1/scan - Range scan
|
//! - GET /api/v1/scan - Range scan
|
||||||
//! - GET /health - Health check
|
//! - GET /health - Health check
|
||||||
|
|
||||||
|
use crate::pd_client::PdClient;
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::{Path, Query, State},
|
extract::{Path, Query, State},
|
||||||
http::StatusCode,
|
http::StatusCode,
|
||||||
routing::{get, post, put},
|
routing::{get, post, put},
|
||||||
Json, Router,
|
Json, Router,
|
||||||
};
|
};
|
||||||
use crate::pd_client::PdClient;
|
|
||||||
use flaredb_client::RdbClient;
|
use flaredb_client::RdbClient;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
@ -172,7 +172,9 @@ pub fn build_router(state: RestApiState) -> Router {
|
||||||
async fn health_check() -> (StatusCode, Json<SuccessResponse<serde_json::Value>>) {
|
async fn health_check() -> (StatusCode, Json<SuccessResponse<serde_json::Value>>) {
|
||||||
(
|
(
|
||||||
StatusCode::OK,
|
StatusCode::OK,
|
||||||
Json(SuccessResponse::new(serde_json::json!({ "status": "healthy" }))),
|
Json(SuccessResponse::new(
|
||||||
|
serde_json::json!({ "status": "healthy" }),
|
||||||
|
)),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -211,12 +213,24 @@ async fn get_kv(
|
||||||
) -> Result<Json<SuccessResponse<GetResponse>>, (StatusCode, Json<ErrorResponse>)> {
|
) -> Result<Json<SuccessResponse<GetResponse>>, (StatusCode, Json<ErrorResponse>)> {
|
||||||
let mut client = RdbClient::connect_direct(state.server_addr.clone(), "default")
|
let mut client = RdbClient::connect_direct(state.server_addr.clone(), "default")
|
||||||
.await
|
.await
|
||||||
.map_err(|e| error_response(StatusCode::SERVICE_UNAVAILABLE, "SERVICE_UNAVAILABLE", &format!("Failed to connect: {}", e)))?;
|
.map_err(|e| {
|
||||||
|
error_response(
|
||||||
|
StatusCode::SERVICE_UNAVAILABLE,
|
||||||
|
"SERVICE_UNAVAILABLE",
|
||||||
|
&format!("Failed to connect: {}", e),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
let value = client
|
let value = client
|
||||||
.raw_get(key.as_bytes().to_vec())
|
.raw_get(key.as_bytes().to_vec())
|
||||||
.await
|
.await
|
||||||
.map_err(|e| error_response(StatusCode::INTERNAL_SERVER_ERROR, "INTERNAL_ERROR", &e.to_string()))?
|
.map_err(|e| {
|
||||||
|
error_response(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
"INTERNAL_ERROR",
|
||||||
|
&e.to_string(),
|
||||||
|
)
|
||||||
|
})?
|
||||||
.ok_or_else(|| error_response(StatusCode::NOT_FOUND, "NOT_FOUND", "Key not found"))?;
|
.ok_or_else(|| error_response(StatusCode::NOT_FOUND, "NOT_FOUND", "Key not found"))?;
|
||||||
|
|
||||||
Ok(Json(SuccessResponse::new(GetResponse {
|
Ok(Json(SuccessResponse::new(GetResponse {
|
||||||
|
|
@ -230,19 +244,34 @@ async fn put_kv(
|
||||||
State(state): State<RestApiState>,
|
State(state): State<RestApiState>,
|
||||||
Path(key): Path<String>,
|
Path(key): Path<String>,
|
||||||
Json(req): Json<PutRequest>,
|
Json(req): Json<PutRequest>,
|
||||||
) -> Result<(StatusCode, Json<SuccessResponse<serde_json::Value>>), (StatusCode, Json<ErrorResponse>)> {
|
) -> Result<(StatusCode, Json<SuccessResponse<serde_json::Value>>), (StatusCode, Json<ErrorResponse>)>
|
||||||
|
{
|
||||||
let mut client = RdbClient::connect_direct(state.server_addr.clone(), &req.namespace)
|
let mut client = RdbClient::connect_direct(state.server_addr.clone(), &req.namespace)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| error_response(StatusCode::SERVICE_UNAVAILABLE, "SERVICE_UNAVAILABLE", &format!("Failed to connect: {}", e)))?;
|
.map_err(|e| {
|
||||||
|
error_response(
|
||||||
|
StatusCode::SERVICE_UNAVAILABLE,
|
||||||
|
"SERVICE_UNAVAILABLE",
|
||||||
|
&format!("Failed to connect: {}", e),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
client
|
client
|
||||||
.raw_put(key.as_bytes().to_vec(), req.value.as_bytes().to_vec())
|
.raw_put(key.as_bytes().to_vec(), req.value.as_bytes().to_vec())
|
||||||
.await
|
.await
|
||||||
.map_err(|e| error_response(StatusCode::INTERNAL_SERVER_ERROR, "INTERNAL_ERROR", &e.to_string()))?;
|
.map_err(|e| {
|
||||||
|
error_response(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
"INTERNAL_ERROR",
|
||||||
|
&e.to_string(),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
Ok((
|
Ok((
|
||||||
StatusCode::OK,
|
StatusCode::OK,
|
||||||
Json(SuccessResponse::new(serde_json::json!({ "key": key, "success": true }))),
|
Json(SuccessResponse::new(
|
||||||
|
serde_json::json!({ "key": key, "success": true }),
|
||||||
|
)),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -253,15 +282,31 @@ async fn scan_kv(
|
||||||
) -> Result<Json<SuccessResponse<ScanResponse>>, (StatusCode, Json<ErrorResponse>)> {
|
) -> Result<Json<SuccessResponse<ScanResponse>>, (StatusCode, Json<ErrorResponse>)> {
|
||||||
let mut client = RdbClient::connect_direct(state.server_addr.clone(), ¶ms.namespace)
|
let mut client = RdbClient::connect_direct(state.server_addr.clone(), ¶ms.namespace)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| error_response(StatusCode::SERVICE_UNAVAILABLE, "SERVICE_UNAVAILABLE", &format!("Failed to connect: {}", e)))?;
|
.map_err(|e| {
|
||||||
|
error_response(
|
||||||
|
StatusCode::SERVICE_UNAVAILABLE,
|
||||||
|
"SERVICE_UNAVAILABLE",
|
||||||
|
&format!("Failed to connect: {}", e),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
let start_key = params.start.unwrap_or_default();
|
let start_key = params.start.unwrap_or_default();
|
||||||
let end_key = params.end.unwrap_or_else(|| format!("{}~", start_key));
|
let end_key = params.end.unwrap_or_else(|| format!("{}~", start_key));
|
||||||
|
|
||||||
let (keys, values, _next) = client
|
let (keys, values, _next) = client
|
||||||
.raw_scan(start_key.as_bytes().to_vec(), end_key.as_bytes().to_vec(), 100)
|
.raw_scan(
|
||||||
|
start_key.as_bytes().to_vec(),
|
||||||
|
end_key.as_bytes().to_vec(),
|
||||||
|
100,
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| error_response(StatusCode::INTERNAL_SERVER_ERROR, "INTERNAL_ERROR", &e.to_string()))?;
|
.map_err(|e| {
|
||||||
|
error_response(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
"INTERNAL_ERROR",
|
||||||
|
&e.to_string(),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
let items: Vec<KvItem> = keys
|
let items: Vec<KvItem> = keys
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
|
@ -282,13 +327,31 @@ async fn get_region(
|
||||||
) -> Result<Json<SuccessResponse<RegionResponse>>, (StatusCode, Json<ErrorResponse>)> {
|
) -> Result<Json<SuccessResponse<RegionResponse>>, (StatusCode, Json<ErrorResponse>)> {
|
||||||
let mut pd_client = PdClient::connect_any(&state.pd_endpoints)
|
let mut pd_client = PdClient::connect_any(&state.pd_endpoints)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| error_response(StatusCode::SERVICE_UNAVAILABLE, "PD_UNAVAILABLE", &format!("Failed to connect to PD: {}", e)))?;
|
.map_err(|e| {
|
||||||
|
error_response(
|
||||||
|
StatusCode::SERVICE_UNAVAILABLE,
|
||||||
|
"PD_UNAVAILABLE",
|
||||||
|
&format!("Failed to connect to PD: {}", e),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
let region = pd_client
|
let region = pd_client
|
||||||
.get_region(id)
|
.get_region(id)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| error_response(StatusCode::INTERNAL_SERVER_ERROR, "INTERNAL_ERROR", &e.to_string()))?
|
.map_err(|e| {
|
||||||
.ok_or_else(|| error_response(StatusCode::NOT_FOUND, "NOT_FOUND", &format!("Region {} not found", id)))?;
|
error_response(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
"INTERNAL_ERROR",
|
||||||
|
&e.to_string(),
|
||||||
|
)
|
||||||
|
})?
|
||||||
|
.ok_or_else(|| {
|
||||||
|
error_response(
|
||||||
|
StatusCode::NOT_FOUND,
|
||||||
|
"NOT_FOUND",
|
||||||
|
&format!("Region {} not found", id),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
Ok(Json(SuccessResponse::new(RegionResponse {
|
Ok(Json(SuccessResponse::new(RegionResponse {
|
||||||
id: region.id,
|
id: region.id,
|
||||||
|
|
@ -305,23 +368,44 @@ async fn add_peer_to_region(
|
||||||
) -> Result<Json<SuccessResponse<RegionResponse>>, (StatusCode, Json<ErrorResponse>)> {
|
) -> Result<Json<SuccessResponse<RegionResponse>>, (StatusCode, Json<ErrorResponse>)> {
|
||||||
let mut pd_client = PdClient::connect_any(&state.pd_endpoints)
|
let mut pd_client = PdClient::connect_any(&state.pd_endpoints)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| error_response(StatusCode::SERVICE_UNAVAILABLE, "PD_UNAVAILABLE", &format!("Failed to connect to PD: {}", e)))?;
|
.map_err(|e| {
|
||||||
|
error_response(
|
||||||
|
StatusCode::SERVICE_UNAVAILABLE,
|
||||||
|
"PD_UNAVAILABLE",
|
||||||
|
&format!("Failed to connect to PD: {}", e),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
let mut region = pd_client
|
let mut region = pd_client
|
||||||
.get_region(id)
|
.get_region(id)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| error_response(StatusCode::INTERNAL_SERVER_ERROR, "INTERNAL_ERROR", &e.to_string()))?
|
.map_err(|e| {
|
||||||
.ok_or_else(|| error_response(StatusCode::NOT_FOUND, "NOT_FOUND", &format!("Region {} not found", id)))?;
|
error_response(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
"INTERNAL_ERROR",
|
||||||
|
&e.to_string(),
|
||||||
|
)
|
||||||
|
})?
|
||||||
|
.ok_or_else(|| {
|
||||||
|
error_response(
|
||||||
|
StatusCode::NOT_FOUND,
|
||||||
|
"NOT_FOUND",
|
||||||
|
&format!("Region {} not found", id),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
// Add peer if not already present
|
// Add peer if not already present
|
||||||
if !region.peers.contains(&req.peer_id) {
|
if !region.peers.contains(&req.peer_id) {
|
||||||
region.peers.push(req.peer_id);
|
region.peers.push(req.peer_id);
|
||||||
region.peers.sort();
|
region.peers.sort();
|
||||||
|
|
||||||
pd_client
|
pd_client.put_region(region.clone()).await.map_err(|e| {
|
||||||
.put_region(region.clone())
|
error_response(
|
||||||
.await
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
.map_err(|e| error_response(StatusCode::INTERNAL_SERVER_ERROR, "INTERNAL_ERROR", &e.to_string()))?;
|
"INTERNAL_ERROR",
|
||||||
|
&e.to_string(),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Json(SuccessResponse::new(RegionResponse {
|
Ok(Json(SuccessResponse::new(RegionResponse {
|
||||||
|
|
@ -335,7 +419,8 @@ async fn add_peer_to_region(
|
||||||
async fn add_member_legacy(
|
async fn add_member_legacy(
|
||||||
State(state): State<RestApiState>,
|
State(state): State<RestApiState>,
|
||||||
Json(req): Json<AddMemberRequestLegacy>,
|
Json(req): Json<AddMemberRequestLegacy>,
|
||||||
) -> Result<(StatusCode, Json<SuccessResponse<serde_json::Value>>), (StatusCode, Json<ErrorResponse>)> {
|
) -> Result<(StatusCode, Json<SuccessResponse<serde_json::Value>>), (StatusCode, Json<ErrorResponse>)>
|
||||||
|
{
|
||||||
let (peer_id, peer_addr) = resolve_join_peer(&state, &req).ok_or_else(|| {
|
let (peer_id, peer_addr) = resolve_join_peer(&state, &req).ok_or_else(|| {
|
||||||
error_response(
|
error_response(
|
||||||
StatusCode::BAD_REQUEST,
|
StatusCode::BAD_REQUEST,
|
||||||
|
|
@ -346,7 +431,13 @@ async fn add_member_legacy(
|
||||||
|
|
||||||
let mut pd_client = PdClient::connect_any(&state.pd_endpoints)
|
let mut pd_client = PdClient::connect_any(&state.pd_endpoints)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| error_response(StatusCode::SERVICE_UNAVAILABLE, "PD_UNAVAILABLE", &format!("Failed to connect to PD: {}", e)))?;
|
.map_err(|e| {
|
||||||
|
error_response(
|
||||||
|
StatusCode::SERVICE_UNAVAILABLE,
|
||||||
|
"PD_UNAVAILABLE",
|
||||||
|
&format!("Failed to connect to PD: {}", e),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
let stores = pd_client.list_stores().await;
|
let stores = pd_client.list_stores().await;
|
||||||
let already_registered = stores.iter().any(|store| store.id == peer_id);
|
let already_registered = stores.iter().any(|store| store.id == peer_id);
|
||||||
|
|
@ -354,14 +445,26 @@ async fn add_member_legacy(
|
||||||
pd_client
|
pd_client
|
||||||
.register_store(peer_id, peer_addr.clone())
|
.register_store(peer_id, peer_addr.clone())
|
||||||
.await
|
.await
|
||||||
.map_err(|e| error_response(StatusCode::INTERNAL_SERVER_ERROR, "INTERNAL_ERROR", &e.to_string()))?;
|
.map_err(|e| {
|
||||||
|
error_response(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
"INTERNAL_ERROR",
|
||||||
|
&e.to_string(),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
let mut regions = pd_client.list_regions().await;
|
let mut regions = pd_client.list_regions().await;
|
||||||
if regions.is_empty() {
|
if regions.is_empty() {
|
||||||
pd_client
|
pd_client
|
||||||
.init_default_region(vec![state.store_id, peer_id])
|
.init_default_region(vec![state.store_id, peer_id])
|
||||||
.await
|
.await
|
||||||
.map_err(|e| error_response(StatusCode::INTERNAL_SERVER_ERROR, "INTERNAL_ERROR", &e.to_string()))?;
|
.map_err(|e| {
|
||||||
|
error_response(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
"INTERNAL_ERROR",
|
||||||
|
&e.to_string(),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
regions = vec![crate::pd_client::RegionInfo {
|
regions = vec![crate::pd_client::RegionInfo {
|
||||||
id: 1,
|
id: 1,
|
||||||
start_key: Vec::new(),
|
start_key: Vec::new(),
|
||||||
|
|
@ -376,10 +479,13 @@ async fn add_member_legacy(
|
||||||
if !region.peers.contains(&peer_id) {
|
if !region.peers.contains(&peer_id) {
|
||||||
region.peers.push(peer_id);
|
region.peers.push(peer_id);
|
||||||
region.peers.sort_unstable();
|
region.peers.sort_unstable();
|
||||||
pd_client
|
pd_client.put_region(region.clone()).await.map_err(|e| {
|
||||||
.put_region(region.clone())
|
error_response(
|
||||||
.await
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
.map_err(|e| error_response(StatusCode::INTERNAL_SERVER_ERROR, "INTERNAL_ERROR", &e.to_string()))?;
|
"INTERNAL_ERROR",
|
||||||
|
&e.to_string(),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
updated_regions.push(region.id);
|
updated_regions.push(region.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -403,10 +509,7 @@ async fn add_member_legacy(
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resolve_join_peer(
|
fn resolve_join_peer(state: &RestApiState, req: &AddMemberRequestLegacy) -> Option<(u64, String)> {
|
||||||
state: &RestApiState,
|
|
||||||
req: &AddMemberRequestLegacy,
|
|
||||||
) -> Option<(u64, String)> {
|
|
||||||
if let Ok(peer_id) = req.id.parse::<u64>() {
|
if let Ok(peer_id) = req.id.parse::<u64>() {
|
||||||
if let Some(addr) = req
|
if let Some(addr) = req
|
||||||
.addr
|
.addr
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,19 @@
|
||||||
use crate::config::{
|
use crate::config::{
|
||||||
decode_value_with_ts, encode_namespaced_key, encode_value_with_ts, ConsistencyMode,
|
decode_value_with_ts,
|
||||||
|
encode_namespaced_key,
|
||||||
|
encode_value_with_ts,
|
||||||
|
ConsistencyMode,
|
||||||
NamespaceManager, // Renamed from ServerConfig
|
NamespaceManager, // Renamed from ServerConfig
|
||||||
};
|
};
|
||||||
use crate::store::Store;
|
use crate::store::Store;
|
||||||
use flaredb_raft::FlareRaftNode;
|
|
||||||
use flaredb_proto::kvrpc::kv_cas_server::KvCas;
|
use flaredb_proto::kvrpc::kv_cas_server::KvCas;
|
||||||
use flaredb_proto::kvrpc::kv_raw_server::KvRaw;
|
use flaredb_proto::kvrpc::kv_raw_server::KvRaw;
|
||||||
use flaredb_proto::kvrpc::{
|
use flaredb_proto::kvrpc::{
|
||||||
CasRequest, CasResponse, DeleteRequest, DeleteResponse, GetRequest, GetResponse, RawDeleteRequest,
|
CasRequest, CasResponse, DeleteRequest, DeleteResponse, GetRequest, GetResponse,
|
||||||
RawDeleteResponse, RawGetRequest, RawGetResponse, RawPutRequest, RawPutResponse, RawScanRequest,
|
RawDeleteRequest, RawDeleteResponse, RawGetRequest, RawGetResponse, RawPutRequest,
|
||||||
RawScanResponse, ScanRequest, ScanResponse, VersionedKv,
|
RawPutResponse, RawScanRequest, RawScanResponse, ScanRequest, ScanResponse, VersionedKv,
|
||||||
};
|
};
|
||||||
|
use flaredb_raft::FlareRaftNode;
|
||||||
use flaredb_storage::rocks_engine::RocksEngine;
|
use flaredb_storage::rocks_engine::RocksEngine;
|
||||||
use flaredb_storage::StorageEngine;
|
use flaredb_storage::StorageEngine;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
@ -30,7 +33,11 @@ pub struct KvServiceImpl {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl KvServiceImpl {
|
impl KvServiceImpl {
|
||||||
pub fn new(engine: Arc<RocksEngine>, namespace_manager: Arc<NamespaceManager>, store: Arc<Store>) -> Self {
|
pub fn new(
|
||||||
|
engine: Arc<RocksEngine>,
|
||||||
|
namespace_manager: Arc<NamespaceManager>,
|
||||||
|
store: Arc<Store>,
|
||||||
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
engine,
|
engine,
|
||||||
namespace_manager,
|
namespace_manager,
|
||||||
|
|
@ -245,9 +252,9 @@ impl KvRaw for KvServiceImpl {
|
||||||
let ts = Self::now_millis();
|
let ts = Self::now_millis();
|
||||||
if let Some(node) = self.route_raft_node(&encoded).await? {
|
if let Some(node) = self.route_raft_node(&encoded).await? {
|
||||||
let existed = node.read_kv(ns_id, &req.key).await.is_some();
|
let existed = node.read_kv(ns_id, &req.key).await.is_some();
|
||||||
node.delete_kv(ns_id, req.key, ts)
|
node.delete_kv(ns_id, req.key, ts).await.map_err(|e| {
|
||||||
.await
|
Status::failed_precondition(format!("raft raw_delete failed: {}", e))
|
||||||
.map_err(|e| Status::failed_precondition(format!("raft raw_delete failed: {}", e)))?;
|
})?;
|
||||||
Ok(Response::new(RawDeleteResponse {
|
Ok(Response::new(RawDeleteResponse {
|
||||||
success: true,
|
success: true,
|
||||||
existed,
|
existed,
|
||||||
|
|
@ -573,7 +580,10 @@ mod tests {
|
||||||
|
|
||||||
let service = KvServiceImpl::new(
|
let service = KvServiceImpl::new(
|
||||||
engine,
|
engine,
|
||||||
Arc::new(NamespaceManager::new(ConsistencyMode::Strong, HashMap::new())), // Use NamespaceManager directly
|
Arc::new(NamespaceManager::new(
|
||||||
|
ConsistencyMode::Strong,
|
||||||
|
HashMap::new(),
|
||||||
|
)), // Use NamespaceManager directly
|
||||||
store,
|
store,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -635,11 +645,17 @@ mod tests {
|
||||||
|
|
||||||
let service = KvServiceImpl::new(
|
let service = KvServiceImpl::new(
|
||||||
engine,
|
engine,
|
||||||
Arc::new(NamespaceManager::new(ConsistencyMode::Strong, HashMap::new())),
|
Arc::new(NamespaceManager::new(
|
||||||
|
ConsistencyMode::Strong,
|
||||||
|
HashMap::new(),
|
||||||
|
)),
|
||||||
store,
|
store,
|
||||||
);
|
);
|
||||||
|
|
||||||
for (key, value) in [(b"k1".to_vec(), b"v1".to_vec()), (b"k2".to_vec(), b"v2".to_vec())] {
|
for (key, value) in [
|
||||||
|
(b"k1".to_vec(), b"v1".to_vec()),
|
||||||
|
(b"k2".to_vec(), b"v2".to_vec()),
|
||||||
|
] {
|
||||||
service
|
service
|
||||||
.compare_and_swap(Request::new(CasRequest {
|
.compare_and_swap(Request::new(CasRequest {
|
||||||
key,
|
key,
|
||||||
|
|
|
||||||
|
|
@ -13,11 +13,16 @@ use tonic::{Request, Response, Status};
|
||||||
pub struct SqlServiceImpl {
|
pub struct SqlServiceImpl {
|
||||||
/// Address of the local FlareDB server
|
/// Address of the local FlareDB server
|
||||||
server_addr: String,
|
server_addr: String,
|
||||||
|
/// ChainFire/PD endpoints used for region-aware routing
|
||||||
|
pd_endpoints: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SqlServiceImpl {
|
impl SqlServiceImpl {
|
||||||
pub fn new(server_addr: String) -> Self {
|
pub fn new(server_addr: String, pd_endpoints: Vec<String>) -> Self {
|
||||||
Self { server_addr }
|
Self {
|
||||||
|
server_addr,
|
||||||
|
pd_endpoints,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn value_to_proto(value: &Value) -> ProtoValue {
|
fn value_to_proto(value: &Value) -> ProtoValue {
|
||||||
|
|
@ -41,9 +46,7 @@ impl SqlServiceImpl {
|
||||||
is_null: false,
|
is_null: false,
|
||||||
},
|
},
|
||||||
Value::Timestamp(ts) => ProtoValue {
|
Value::Timestamp(ts) => ProtoValue {
|
||||||
value: Some(flaredb_proto::sqlrpc::sql_value::Value::TimestampValue(
|
value: Some(flaredb_proto::sqlrpc::sql_value::Value::TimestampValue(*ts)),
|
||||||
*ts,
|
|
||||||
)),
|
|
||||||
is_null: false,
|
is_null: false,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
@ -52,14 +55,15 @@ impl SqlServiceImpl {
|
||||||
|
|
||||||
#[tonic::async_trait]
|
#[tonic::async_trait]
|
||||||
impl SqlServiceTrait for SqlServiceImpl {
|
impl SqlServiceTrait for SqlServiceImpl {
|
||||||
async fn execute(
|
async fn execute(&self, request: Request<SqlRequest>) -> Result<Response<SqlResponse>, Status> {
|
||||||
&self,
|
|
||||||
request: Request<SqlRequest>,
|
|
||||||
) -> Result<Response<SqlResponse>, Status> {
|
|
||||||
let req = request.into_inner();
|
let req = request.into_inner();
|
||||||
|
|
||||||
// Connect to the local FlareDB server with the requested namespace
|
// Use PD-backed routing so SQL works for both strong and eventual namespaces.
|
||||||
let client = RdbClient::connect_direct(self.server_addr.clone(), req.namespace.clone())
|
let client = RdbClient::connect_with_pd_namespace(
|
||||||
|
self.server_addr.clone(),
|
||||||
|
self.pd_endpoints.join(","),
|
||||||
|
req.namespace.clone(),
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| Status::internal(format!("Failed to connect to FlareDB: {}", e)))?;
|
.map_err(|e| Status::internal(format!("Failed to connect to FlareDB: {}", e)))?;
|
||||||
|
|
||||||
|
|
@ -81,9 +85,7 @@ impl SqlServiceTrait for SqlServiceImpl {
|
||||||
},
|
},
|
||||||
ExecutionResult::DmlSuccess(rows_affected) => SqlResponse {
|
ExecutionResult::DmlSuccess(rows_affected) => SqlResponse {
|
||||||
result: Some(flaredb_proto::sqlrpc::sql_response::Result::DmlResult(
|
result: Some(flaredb_proto::sqlrpc::sql_response::Result::DmlResult(
|
||||||
DmlResult {
|
DmlResult { rows_affected },
|
||||||
rows_affected,
|
|
||||||
},
|
|
||||||
)),
|
)),
|
||||||
},
|
},
|
||||||
ExecutionResult::Query(query_result) => {
|
ExecutionResult::Query(query_result) => {
|
||||||
|
|
@ -103,8 +105,7 @@ impl SqlServiceTrait for SqlServiceImpl {
|
||||||
},
|
},
|
||||||
)),
|
)),
|
||||||
}
|
}
|
||||||
}
|
} // Errors are returned via Result::Err and handled by .map_err() above
|
||||||
// Errors are returned via Result::Err and handled by .map_err() above
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Response::new(response))
|
Ok(Response::new(response))
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
use crate::error::{Result, SqlError};
|
use crate::error::Result;
|
||||||
use crate::metadata::MetadataManager;
|
use crate::metadata::MetadataManager;
|
||||||
use crate::parser::{SqlStatement, parse_sql};
|
use crate::parser::{parse_sql, SqlStatement};
|
||||||
use crate::storage::StorageManager;
|
use crate::storage::StorageManager;
|
||||||
use crate::types::QueryResult;
|
use crate::types::QueryResult;
|
||||||
use flaredb_client::RdbClient;
|
use flaredb_client::RdbClient;
|
||||||
|
|
@ -93,12 +93,31 @@ impl SqlExecutor {
|
||||||
Ok(ExecutionResult::Query(result))
|
Ok(ExecutionResult::Query(result))
|
||||||
}
|
}
|
||||||
|
|
||||||
SqlStatement::Update { .. } => {
|
SqlStatement::Update {
|
||||||
Err(SqlError::InvalidOperation("UPDATE not yet implemented".to_string()))
|
table_name,
|
||||||
|
assignments,
|
||||||
|
where_clause,
|
||||||
|
} => {
|
||||||
|
let table = self.metadata_manager.get_table_metadata(table_name).await?;
|
||||||
|
let updated = self
|
||||||
|
.storage_manager
|
||||||
|
.update_rows(&table, assignments, where_clause.as_ref())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(ExecutionResult::DmlSuccess(updated))
|
||||||
}
|
}
|
||||||
|
|
||||||
SqlStatement::Delete { .. } => {
|
SqlStatement::Delete {
|
||||||
Err(SqlError::InvalidOperation("DELETE not yet implemented".to_string()))
|
table_name,
|
||||||
|
where_clause,
|
||||||
|
} => {
|
||||||
|
let table = self.metadata_manager.get_table_metadata(table_name).await?;
|
||||||
|
let deleted = self
|
||||||
|
.storage_manager
|
||||||
|
.delete_rows(&table, where_clause.as_ref())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(ExecutionResult::DmlSuccess(deleted))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -111,7 +130,9 @@ mod tests {
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
#[ignore] // Requires FlareDB server
|
#[ignore] // Requires FlareDB server
|
||||||
async fn test_create_and_query_table() {
|
async fn test_create_and_query_table() {
|
||||||
let client = RdbClient::connect_direct("127.0.0.1:8001".to_string(), "sqltest".to_string()).await.unwrap();
|
let client = RdbClient::connect_direct("127.0.0.1:8001".to_string(), "sqltest".to_string())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
let executor = SqlExecutor::new(Arc::new(Mutex::new(client)));
|
let executor = SqlExecutor::new(Arc::new(Mutex::new(client)));
|
||||||
|
|
||||||
// Create table
|
// Create table
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
use crate::error::{Result, SqlError};
|
use crate::error::{Result, SqlError};
|
||||||
use crate::types::{ColumnDef, DataType, Value};
|
use crate::types::{ColumnDef, DataType, Value};
|
||||||
use sqlparser::ast::{
|
use sqlparser::ast::{ColumnDef as AstColumnDef, DataType as AstDataType, Expr, Statement};
|
||||||
ColumnDef as AstColumnDef, DataType as AstDataType, Expr, Statement,
|
|
||||||
};
|
|
||||||
use sqlparser::dialect::GenericDialect;
|
use sqlparser::dialect::GenericDialect;
|
||||||
use sqlparser::parser::Parser;
|
use sqlparser::parser::Parser;
|
||||||
|
|
||||||
|
|
@ -85,7 +83,9 @@ fn parse_statement(stmt: &Statement) -> Result<SqlStatement> {
|
||||||
Statement::CreateTable { .. } => parse_create_table(stmt),
|
Statement::CreateTable { .. } => parse_create_table(stmt),
|
||||||
Statement::Drop { names, .. } => {
|
Statement::Drop { names, .. } => {
|
||||||
if names.len() != 1 {
|
if names.len() != 1 {
|
||||||
return Err(SqlError::ParseError("Expected single table name".to_string()));
|
return Err(SqlError::ParseError(
|
||||||
|
"Expected single table name".to_string(),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
Ok(SqlStatement::DropTable {
|
Ok(SqlStatement::DropTable {
|
||||||
table_name: names[0].to_string(),
|
table_name: names[0].to_string(),
|
||||||
|
|
@ -93,12 +93,8 @@ fn parse_statement(stmt: &Statement) -> Result<SqlStatement> {
|
||||||
}
|
}
|
||||||
Statement::Insert { .. } => parse_insert(stmt),
|
Statement::Insert { .. } => parse_insert(stmt),
|
||||||
Statement::Query(query) => parse_select(query),
|
Statement::Query(query) => parse_select(query),
|
||||||
Statement::Update { .. } => {
|
Statement::Update { .. } => parse_update(stmt),
|
||||||
Err(SqlError::ParseError("UPDATE not yet implemented".to_string()))
|
Statement::Delete { .. } => parse_delete(stmt),
|
||||||
}
|
|
||||||
Statement::Delete { .. } => {
|
|
||||||
Err(SqlError::ParseError("DELETE not yet implemented".to_string()))
|
|
||||||
}
|
|
||||||
_ => Err(SqlError::ParseError(format!(
|
_ => Err(SqlError::ParseError(format!(
|
||||||
"Unsupported statement: {:?}",
|
"Unsupported statement: {:?}",
|
||||||
stmt
|
stmt
|
||||||
|
|
@ -107,8 +103,16 @@ fn parse_statement(stmt: &Statement) -> Result<SqlStatement> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_create_table(stmt: &Statement) -> Result<SqlStatement> {
|
fn parse_create_table(stmt: &Statement) -> Result<SqlStatement> {
|
||||||
let Statement::CreateTable { name, columns: col_defs, constraints, .. } = stmt else {
|
let Statement::CreateTable {
|
||||||
return Err(SqlError::ParseError("Expected CREATE TABLE statement".to_string()));
|
name,
|
||||||
|
columns: col_defs,
|
||||||
|
constraints,
|
||||||
|
..
|
||||||
|
} = stmt
|
||||||
|
else {
|
||||||
|
return Err(SqlError::ParseError(
|
||||||
|
"Expected CREATE TABLE statement".to_string(),
|
||||||
|
));
|
||||||
};
|
};
|
||||||
|
|
||||||
let table_name = name.to_string();
|
let table_name = name.to_string();
|
||||||
|
|
@ -122,7 +126,12 @@ fn parse_create_table(stmt: &Statement) -> Result<SqlStatement> {
|
||||||
|
|
||||||
// Extract primary key from constraints
|
// Extract primary key from constraints
|
||||||
for constraint in constraints {
|
for constraint in constraints {
|
||||||
if let sqlparser::ast::TableConstraint::Unique { columns: pk_cols, is_primary: true, .. } = constraint {
|
if let sqlparser::ast::TableConstraint::Unique {
|
||||||
|
columns: pk_cols,
|
||||||
|
is_primary: true,
|
||||||
|
..
|
||||||
|
} = constraint
|
||||||
|
{
|
||||||
for pk_col in pk_cols {
|
for pk_col in pk_cols {
|
||||||
primary_key.push(pk_col.value.to_string());
|
primary_key.push(pk_col.value.to_string());
|
||||||
}
|
}
|
||||||
|
|
@ -133,7 +142,10 @@ fn parse_create_table(stmt: &Statement) -> Result<SqlStatement> {
|
||||||
if primary_key.is_empty() {
|
if primary_key.is_empty() {
|
||||||
for column in col_defs {
|
for column in col_defs {
|
||||||
for option in &column.options {
|
for option in &column.options {
|
||||||
if matches!(option.option, sqlparser::ast::ColumnOption::Unique { is_primary: true }) {
|
if matches!(
|
||||||
|
option.option,
|
||||||
|
sqlparser::ast::ColumnOption::Unique { is_primary: true }
|
||||||
|
) {
|
||||||
primary_key.push(column.name.value.to_string());
|
primary_key.push(column.name.value.to_string());
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
@ -142,9 +154,7 @@ fn parse_create_table(stmt: &Statement) -> Result<SqlStatement> {
|
||||||
}
|
}
|
||||||
|
|
||||||
if primary_key.is_empty() {
|
if primary_key.is_empty() {
|
||||||
return Err(SqlError::ParseError(
|
return Err(SqlError::ParseError("PRIMARY KEY is required".to_string()));
|
||||||
"PRIMARY KEY is required".to_string(),
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(SqlStatement::CreateTable {
|
Ok(SqlStatement::CreateTable {
|
||||||
|
|
@ -196,8 +206,16 @@ fn parse_data_type(dt: &AstDataType) -> Result<DataType> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_insert(stmt: &Statement) -> Result<SqlStatement> {
|
fn parse_insert(stmt: &Statement) -> Result<SqlStatement> {
|
||||||
let Statement::Insert { table_name, columns: col_idents, source, .. } = stmt else {
|
let Statement::Insert {
|
||||||
return Err(SqlError::ParseError("Expected INSERT statement".to_string()));
|
table_name,
|
||||||
|
columns: col_idents,
|
||||||
|
source,
|
||||||
|
..
|
||||||
|
} = stmt
|
||||||
|
else {
|
||||||
|
return Err(SqlError::ParseError(
|
||||||
|
"Expected INSERT statement".to_string(),
|
||||||
|
));
|
||||||
};
|
};
|
||||||
|
|
||||||
let table_name = table_name.to_string();
|
let table_name = table_name.to_string();
|
||||||
|
|
@ -276,6 +294,145 @@ fn parse_select(query: &sqlparser::ast::Query) -> Result<SqlStatement> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse_update(stmt: &Statement) -> Result<SqlStatement> {
|
||||||
|
let Statement::Update {
|
||||||
|
table,
|
||||||
|
assignments,
|
||||||
|
from,
|
||||||
|
selection,
|
||||||
|
returning,
|
||||||
|
} = stmt
|
||||||
|
else {
|
||||||
|
return Err(SqlError::ParseError(
|
||||||
|
"Expected UPDATE statement".to_string(),
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
if from.is_some() {
|
||||||
|
return Err(SqlError::ParseError(
|
||||||
|
"UPDATE ... FROM is not supported".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if returning.is_some() {
|
||||||
|
return Err(SqlError::ParseError(
|
||||||
|
"UPDATE ... RETURNING is not supported".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if !table.joins.is_empty() {
|
||||||
|
return Err(SqlError::ParseError(
|
||||||
|
"UPDATE with joins is not supported".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let table_name = match &table.relation {
|
||||||
|
sqlparser::ast::TableFactor::Table { name, .. } => name.to_string(),
|
||||||
|
_ => {
|
||||||
|
return Err(SqlError::ParseError(
|
||||||
|
"Complex UPDATE targets are not supported".to_string(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut parsed_assignments = Vec::with_capacity(assignments.len());
|
||||||
|
for assignment in assignments {
|
||||||
|
if assignment.id.len() != 1 {
|
||||||
|
return Err(SqlError::ParseError(
|
||||||
|
"Only simple column assignments are supported".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let column = assignment.id[0].value.to_string();
|
||||||
|
let value = parse_expr_as_value(&assignment.value)?;
|
||||||
|
parsed_assignments.push((column, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
let where_clause = if let Some(expr) = selection {
|
||||||
|
Some(parse_where_expr(expr)?)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(SqlStatement::Update {
|
||||||
|
table_name,
|
||||||
|
assignments: parsed_assignments,
|
||||||
|
where_clause,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_delete(stmt: &Statement) -> Result<SqlStatement> {
|
||||||
|
let Statement::Delete {
|
||||||
|
tables,
|
||||||
|
from,
|
||||||
|
using,
|
||||||
|
selection,
|
||||||
|
returning,
|
||||||
|
order_by,
|
||||||
|
limit,
|
||||||
|
} = stmt
|
||||||
|
else {
|
||||||
|
return Err(SqlError::ParseError(
|
||||||
|
"Expected DELETE statement".to_string(),
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
if !tables.is_empty() {
|
||||||
|
return Err(SqlError::ParseError(
|
||||||
|
"Multi-table DELETE is not supported".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if using.is_some() {
|
||||||
|
return Err(SqlError::ParseError(
|
||||||
|
"DELETE ... USING is not supported".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if returning.is_some() {
|
||||||
|
return Err(SqlError::ParseError(
|
||||||
|
"DELETE ... RETURNING is not supported".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if !order_by.is_empty() {
|
||||||
|
return Err(SqlError::ParseError(
|
||||||
|
"DELETE ... ORDER BY is not supported".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if limit.is_some() {
|
||||||
|
return Err(SqlError::ParseError(
|
||||||
|
"DELETE ... LIMIT is not supported".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if from.len() != 1 {
|
||||||
|
return Err(SqlError::ParseError(
|
||||||
|
"DELETE must target exactly one table".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let table = &from[0];
|
||||||
|
if !table.joins.is_empty() {
|
||||||
|
return Err(SqlError::ParseError(
|
||||||
|
"DELETE with joins is not supported".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let table_name = match &table.relation {
|
||||||
|
sqlparser::ast::TableFactor::Table { name, .. } => name.to_string(),
|
||||||
|
_ => {
|
||||||
|
return Err(SqlError::ParseError(
|
||||||
|
"Complex DELETE targets are not supported".to_string(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let where_clause = if let Some(expr) = selection {
|
||||||
|
Some(parse_where_expr(expr)?)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(SqlStatement::Delete {
|
||||||
|
table_name,
|
||||||
|
where_clause,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn parse_where_expr(expr: &Expr) -> Result<WhereClause> {
|
fn parse_where_expr(expr: &Expr) -> Result<WhereClause> {
|
||||||
match expr {
|
match expr {
|
||||||
Expr::BinaryOp { left, op, right } => {
|
Expr::BinaryOp { left, op, right } => {
|
||||||
|
|
@ -356,3 +513,64 @@ fn parse_expr_as_value(expr: &Expr) -> Result<Value> {
|
||||||
))),
|
))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_update_statement() {
|
||||||
|
let parsed = parse_sql("UPDATE users SET name = 'Bob', active = true WHERE id = 7")
|
||||||
|
.expect("update should parse");
|
||||||
|
|
||||||
|
match parsed {
|
||||||
|
SqlStatement::Update {
|
||||||
|
table_name,
|
||||||
|
assignments,
|
||||||
|
where_clause,
|
||||||
|
} => {
|
||||||
|
assert_eq!(table_name, "users");
|
||||||
|
assert_eq!(
|
||||||
|
assignments,
|
||||||
|
vec![
|
||||||
|
("name".to_string(), Value::Text("Bob".to_string())),
|
||||||
|
("active".to_string(), Value::Boolean(true)),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
assert!(matches!(
|
||||||
|
where_clause,
|
||||||
|
Some(WhereClause::Comparison {
|
||||||
|
column,
|
||||||
|
op: ComparisonOp::Eq,
|
||||||
|
value: Value::Integer(7),
|
||||||
|
}) if column == "id"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
other => panic!("expected update statement, got {:?}", other),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_delete_statement() {
|
||||||
|
let parsed =
|
||||||
|
parse_sql("DELETE FROM users WHERE name = 'Alice'").expect("delete should parse");
|
||||||
|
|
||||||
|
match parsed {
|
||||||
|
SqlStatement::Delete {
|
||||||
|
table_name,
|
||||||
|
where_clause,
|
||||||
|
} => {
|
||||||
|
assert_eq!(table_name, "users");
|
||||||
|
assert!(matches!(
|
||||||
|
where_clause,
|
||||||
|
Some(WhereClause::Comparison {
|
||||||
|
column,
|
||||||
|
op: ComparisonOp::Eq,
|
||||||
|
value: Value::Text(value),
|
||||||
|
}) if column == "name" && value == "Alice"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
other => panic!("expected delete statement, got {:?}", other),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,12 @@ pub struct StorageManager {
|
||||||
client: Arc<Mutex<RdbClient>>,
|
client: Arc<Mutex<RdbClient>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct ScannedRow {
|
||||||
|
key: Vec<u8>,
|
||||||
|
row: RowData,
|
||||||
|
version: u64,
|
||||||
|
}
|
||||||
|
|
||||||
impl StorageManager {
|
impl StorageManager {
|
||||||
pub fn new(client: Arc<Mutex<RdbClient>>) -> Self {
|
pub fn new(client: Arc<Mutex<RdbClient>>) -> Self {
|
||||||
Self { client }
|
Self { client }
|
||||||
|
|
@ -52,9 +58,9 @@ impl StorageManager {
|
||||||
.primary_key
|
.primary_key
|
||||||
.iter()
|
.iter()
|
||||||
.map(|pk_col| {
|
.map(|pk_col| {
|
||||||
row_data
|
row_data.get(pk_col).ok_or_else(|| {
|
||||||
.get(pk_col)
|
SqlError::PrimaryKeyViolation(format!("Missing primary key column: {}", pk_col))
|
||||||
.ok_or_else(|| SqlError::PrimaryKeyViolation(format!("Missing primary key column: {}", pk_col)))
|
})
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
let pk_values = pk_values?;
|
let pk_values = pk_values?;
|
||||||
|
|
@ -66,8 +72,8 @@ impl StorageManager {
|
||||||
let row = RowData::new(row_data);
|
let row = RowData::new(row_data);
|
||||||
|
|
||||||
// Serialize row
|
// Serialize row
|
||||||
let value = bincode::serialize(&row)
|
let value =
|
||||||
.map_err(|e| SqlError::SerializationError(e.to_string()))?;
|
bincode::serialize(&row).map_err(|e| SqlError::SerializationError(e.to_string()))?;
|
||||||
|
|
||||||
// Store in KVS using CAS (version 0 for new row)
|
// Store in KVS using CAS (version 0 for new row)
|
||||||
let mut client = self.client.lock().await;
|
let mut client = self.client.lock().await;
|
||||||
|
|
@ -77,7 +83,9 @@ impl StorageManager {
|
||||||
.map_err(|e| SqlError::KvsError(e.to_string()))?;
|
.map_err(|e| SqlError::KvsError(e.to_string()))?;
|
||||||
|
|
||||||
if !success {
|
if !success {
|
||||||
return Err(SqlError::PrimaryKeyViolation("Row with this primary key already exists".to_string()));
|
return Err(SqlError::PrimaryKeyViolation(
|
||||||
|
"Row with this primary key already exists".to_string(),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -109,21 +117,9 @@ impl StorageManager {
|
||||||
|
|
||||||
let mut result = QueryResult::new(result_columns.clone());
|
let mut result = QueryResult::new(result_columns.clone());
|
||||||
|
|
||||||
// Scan all rows for this table
|
|
||||||
let start_key = Self::encode_table_prefix(table.table_id);
|
|
||||||
let end_key = Self::encode_table_prefix(table.table_id + 1);
|
|
||||||
|
|
||||||
let mut client = self.client.lock().await;
|
|
||||||
let (entries, _next_key) = client
|
|
||||||
.cas_scan(start_key, end_key, 1000)
|
|
||||||
.await
|
|
||||||
.map_err(|e| SqlError::KvsError(e.to_string()))?;
|
|
||||||
|
|
||||||
// Process each row
|
// Process each row
|
||||||
for (_key, value_bytes, _version) in entries {
|
for scanned in self.scan_table_rows(table).await? {
|
||||||
let row: RowData = bincode::deserialize(&value_bytes)
|
let row = scanned.row;
|
||||||
.map_err(|e| SqlError::SerializationError(e.to_string()))?;
|
|
||||||
|
|
||||||
// Apply WHERE filter
|
// Apply WHERE filter
|
||||||
if let Some(where_clause) = where_clause {
|
if let Some(where_clause) = where_clause {
|
||||||
if !Self::evaluate_where(&row, where_clause)? {
|
if !Self::evaluate_where(&row, where_clause)? {
|
||||||
|
|
@ -134,10 +130,7 @@ impl StorageManager {
|
||||||
// Extract requested columns
|
// Extract requested columns
|
||||||
let mut row_values = Vec::new();
|
let mut row_values = Vec::new();
|
||||||
for col_name in &result_columns {
|
for col_name in &result_columns {
|
||||||
let value = row
|
let value = row.get(col_name).cloned().unwrap_or(Value::Null);
|
||||||
.get(col_name)
|
|
||||||
.cloned()
|
|
||||||
.unwrap_or(Value::Null);
|
|
||||||
row_values.push(value);
|
row_values.push(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -147,6 +140,138 @@ impl StorageManager {
|
||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Update rows in a table
|
||||||
|
pub async fn update_rows(
|
||||||
|
&self,
|
||||||
|
table: &TableMetadata,
|
||||||
|
assignments: &[(String, Value)],
|
||||||
|
where_clause: Option<&WhereClause>,
|
||||||
|
) -> Result<u64> {
|
||||||
|
for (column, _) in assignments {
|
||||||
|
if table.get_column(column).is_none() {
|
||||||
|
return Err(SqlError::ColumnNotFound(
|
||||||
|
column.clone(),
|
||||||
|
table.table_name.clone(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut updated = 0u64;
|
||||||
|
for scanned in self.scan_table_rows(table).await? {
|
||||||
|
if let Some(where_clause) = where_clause {
|
||||||
|
if !Self::evaluate_where(&scanned.row, where_clause)? {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut row = scanned.row;
|
||||||
|
for (column, value) in assignments {
|
||||||
|
row.set(column.clone(), value.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
let pk_values = Self::primary_key_values(table, &row)?;
|
||||||
|
let new_key = Self::encode_row_key(table.table_id, &pk_values)?;
|
||||||
|
let value = Self::serialize_row(&row)?;
|
||||||
|
|
||||||
|
if new_key == scanned.key {
|
||||||
|
let mut client = self.client.lock().await;
|
||||||
|
let (success, _current_version, _new_version) = client
|
||||||
|
.cas(scanned.key.clone(), value, scanned.version)
|
||||||
|
.await
|
||||||
|
.map_err(|e| SqlError::KvsError(e.to_string()))?;
|
||||||
|
|
||||||
|
if !success {
|
||||||
|
return Err(SqlError::InvalidOperation(
|
||||||
|
"Concurrent row update detected".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let mut client = self.client.lock().await;
|
||||||
|
let (created, _current_version, new_version) = client
|
||||||
|
.cas(new_key.clone(), value, 0)
|
||||||
|
.await
|
||||||
|
.map_err(|e| SqlError::KvsError(e.to_string()))?;
|
||||||
|
if !created {
|
||||||
|
return Err(SqlError::PrimaryKeyViolation(
|
||||||
|
"Row with this primary key already exists".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let (deleted, _current_version, _existed) = client
|
||||||
|
.cas_delete(scanned.key.clone(), scanned.version)
|
||||||
|
.await
|
||||||
|
.map_err(|e| SqlError::KvsError(e.to_string()))?;
|
||||||
|
if !deleted {
|
||||||
|
let _ = client.cas_delete(new_key, new_version).await;
|
||||||
|
return Err(SqlError::InvalidOperation(
|
||||||
|
"Concurrent row update detected".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updated += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(updated)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Delete rows from a table
|
||||||
|
pub async fn delete_rows(
|
||||||
|
&self,
|
||||||
|
table: &TableMetadata,
|
||||||
|
where_clause: Option<&WhereClause>,
|
||||||
|
) -> Result<u64> {
|
||||||
|
let mut deleted = 0u64;
|
||||||
|
for scanned in self.scan_table_rows(table).await? {
|
||||||
|
if let Some(where_clause) = where_clause {
|
||||||
|
if !Self::evaluate_where(&scanned.row, where_clause)? {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut client = self.client.lock().await;
|
||||||
|
let (success, _current_version, _existed) = client
|
||||||
|
.cas_delete(scanned.key, scanned.version)
|
||||||
|
.await
|
||||||
|
.map_err(|e| SqlError::KvsError(e.to_string()))?;
|
||||||
|
if !success {
|
||||||
|
return Err(SqlError::InvalidOperation(
|
||||||
|
"Concurrent row delete detected".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
deleted += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(deleted)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn scan_table_rows(&self, table: &TableMetadata) -> Result<Vec<ScannedRow>> {
|
||||||
|
let (mut next_key, end_key) = Self::encode_table_scan_range(table.table_id);
|
||||||
|
let mut rows = Vec::new();
|
||||||
|
let mut client = self.client.lock().await;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let (entries, continuation) = client
|
||||||
|
.cas_scan(next_key.clone(), end_key.clone(), 1000)
|
||||||
|
.await
|
||||||
|
.map_err(|e| SqlError::KvsError(e.to_string()))?;
|
||||||
|
|
||||||
|
for (key, value_bytes, version) in entries {
|
||||||
|
let row: RowData = bincode::deserialize(&value_bytes)
|
||||||
|
.map_err(|e| SqlError::SerializationError(e.to_string()))?;
|
||||||
|
rows.push(ScannedRow { key, row, version });
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(continuation) = continuation {
|
||||||
|
next_key = continuation;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(rows)
|
||||||
|
}
|
||||||
|
|
||||||
/// Evaluate WHERE clause against a row
|
/// Evaluate WHERE clause against a row
|
||||||
fn evaluate_where(row: &RowData, where_clause: &WhereClause) -> Result<bool> {
|
fn evaluate_where(row: &RowData, where_clause: &WhereClause) -> Result<bool> {
|
||||||
match where_clause {
|
match where_clause {
|
||||||
|
|
@ -213,4 +338,57 @@ impl StorageManager {
|
||||||
fn encode_table_prefix(table_id: u32) -> Vec<u8> {
|
fn encode_table_prefix(table_id: u32) -> Vec<u8> {
|
||||||
format!("{}:{}:", DATA_PREFIX, table_id).into_bytes()
|
format!("{}:{}:", DATA_PREFIX, table_id).into_bytes()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn encode_table_scan_range(table_id: u32) -> (Vec<u8>, Vec<u8>) {
|
||||||
|
let start = Self::encode_table_prefix(table_id);
|
||||||
|
let end = Self::prefix_end(&start);
|
||||||
|
(start, end)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prefix_end(prefix: &[u8]) -> Vec<u8> {
|
||||||
|
let mut end_key = prefix.to_vec();
|
||||||
|
if let Some(last) = end_key.last_mut() {
|
||||||
|
if *last == 0xff {
|
||||||
|
end_key.push(0x00);
|
||||||
|
} else {
|
||||||
|
*last += 1;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
end_key.push(0xff);
|
||||||
|
}
|
||||||
|
end_key
|
||||||
|
}
|
||||||
|
|
||||||
|
fn primary_key_values<'a>(
|
||||||
|
table: &'a TableMetadata,
|
||||||
|
row: &'a RowData,
|
||||||
|
) -> Result<Vec<&'a Value>> {
|
||||||
|
table
|
||||||
|
.primary_key
|
||||||
|
.iter()
|
||||||
|
.map(|pk_col| {
|
||||||
|
row.get(pk_col).ok_or_else(|| {
|
||||||
|
SqlError::PrimaryKeyViolation(format!("Missing primary key column: {}", pk_col))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn serialize_row(row: &RowData) -> Result<Vec<u8>> {
|
||||||
|
bincode::serialize(row).map_err(|e| SqlError::SerializationError(e.to_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn table_scan_range_uses_prefix_successor() {
|
||||||
|
let (start, end) = StorageManager::encode_table_scan_range(9);
|
||||||
|
assert_eq!(start, b"__sql_data:9:".to_vec());
|
||||||
|
assert_eq!(end, b"__sql_data:9;".to_vec());
|
||||||
|
assert!(b"__sql_data:9:1".to_vec() < end);
|
||||||
|
assert!(b"__sql_data:10:1".to_vec() < start);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -170,7 +170,13 @@ impl ObjectServiceImpl {
|
||||||
end: usize,
|
end: usize,
|
||||||
) -> <Self as ObjectService>::GetObjectStream {
|
) -> <Self as ObjectService>::GetObjectStream {
|
||||||
let storage = self.storage.clone();
|
let storage = self.storage.clone();
|
||||||
let state = (storage, upload, Some(self.object_to_proto(object)), 0usize, 0u64);
|
let state = (
|
||||||
|
storage,
|
||||||
|
upload,
|
||||||
|
Some(self.object_to_proto(object)),
|
||||||
|
0usize,
|
||||||
|
0u64,
|
||||||
|
);
|
||||||
let range_start = start as u64;
|
let range_start = start as u64;
|
||||||
let range_end = end as u64;
|
let range_end = end as u64;
|
||||||
let object_size = object.size;
|
let object_size = object.size;
|
||||||
|
|
@ -327,6 +333,41 @@ impl ObjectServiceImpl {
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn load_full_object_bytes(&self, object: &Object) -> Result<Bytes, Status> {
|
||||||
|
if let Some(upload) = self
|
||||||
|
.metadata
|
||||||
|
.load_object_multipart_upload(&object.id)
|
||||||
|
.await
|
||||||
|
.map_err(Self::to_status)?
|
||||||
|
{
|
||||||
|
let mut body = BytesMut::new();
|
||||||
|
for part in &upload.parts {
|
||||||
|
let bytes = self
|
||||||
|
.storage
|
||||||
|
.get_part(upload.upload_id.as_str(), part.part_number.as_u32())
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
Status::internal(format!("Failed to retrieve multipart object part: {}", e))
|
||||||
|
})?;
|
||||||
|
body.extend_from_slice(bytes.as_ref());
|
||||||
|
}
|
||||||
|
if body.len() as u64 != object.size {
|
||||||
|
return Err(Status::internal(format!(
|
||||||
|
"Multipart object {} has inconsistent size: expected {}, got {}",
|
||||||
|
object.id,
|
||||||
|
object.size,
|
||||||
|
body.len()
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
Ok(body.freeze())
|
||||||
|
} else {
|
||||||
|
self.storage
|
||||||
|
.get_object(&object.id)
|
||||||
|
.await
|
||||||
|
.map_err(|e| Status::internal(format!("Failed to retrieve object: {}", e)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn multipart_lock(&self, upload_id: &str) -> Arc<Mutex<()>> {
|
fn multipart_lock(&self, upload_id: &str) -> Arc<Mutex<()>> {
|
||||||
self.multipart_locks
|
self.multipart_locks
|
||||||
.entry(upload_id.to_string())
|
.entry(upload_id.to_string())
|
||||||
|
|
@ -613,9 +654,99 @@ impl ObjectService for ObjectServiceImpl {
|
||||||
|
|
||||||
async fn copy_object(
|
async fn copy_object(
|
||||||
&self,
|
&self,
|
||||||
_request: Request<CopyObjectRequest>,
|
request: Request<CopyObjectRequest>,
|
||||||
) -> Result<Response<CopyObjectResponse>, Status> {
|
) -> Result<Response<CopyObjectResponse>, Status> {
|
||||||
Err(Status::unimplemented("CopyObject not yet implemented"))
|
let tenant = get_tenant_context(&request)?;
|
||||||
|
let req = request.into_inner();
|
||||||
|
|
||||||
|
if req.source_bucket.is_empty() {
|
||||||
|
return Err(Status::invalid_argument("source_bucket is required"));
|
||||||
|
}
|
||||||
|
if req.source_key.is_empty() {
|
||||||
|
return Err(Status::invalid_argument("source_key is required"));
|
||||||
|
}
|
||||||
|
if req.dest_bucket.is_empty() {
|
||||||
|
return Err(Status::invalid_argument("dest_bucket is required"));
|
||||||
|
}
|
||||||
|
if req.dest_key.is_empty() {
|
||||||
|
return Err(Status::invalid_argument("dest_key is required"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let source_bucket = self
|
||||||
|
.load_bucket_for_tenant(&tenant, &req.source_bucket)
|
||||||
|
.await?;
|
||||||
|
self.authorize_object_action(
|
||||||
|
&tenant,
|
||||||
|
ACTION_OBJECTS_READ,
|
||||||
|
&source_bucket,
|
||||||
|
&req.source_key,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let dest_bucket = self
|
||||||
|
.load_bucket_for_tenant(&tenant, &req.dest_bucket)
|
||||||
|
.await?;
|
||||||
|
self.authorize_object_action(&tenant, ACTION_OBJECTS_CREATE, &dest_bucket, &req.dest_key)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let source_bucket_id: BucketId = BucketId::from_str(&source_bucket.id.to_string())
|
||||||
|
.map_err(|_| Status::internal("Invalid source bucket ID"))?;
|
||||||
|
let dest_key = ObjectKey::new(&req.dest_key)
|
||||||
|
.map_err(|e| Status::invalid_argument(format!("Invalid destination key: {}", e)))?;
|
||||||
|
|
||||||
|
let source_version_id = if req.source_version_id.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(req.source_version_id.as_str())
|
||||||
|
};
|
||||||
|
let source_object = self
|
||||||
|
.metadata
|
||||||
|
.load_object(&source_bucket_id, &req.source_key, source_version_id)
|
||||||
|
.await
|
||||||
|
.map_err(Self::to_status)?
|
||||||
|
.ok_or_else(|| Status::not_found(format!("Object {} not found", req.source_key)))?;
|
||||||
|
|
||||||
|
if source_object.is_delete_marker {
|
||||||
|
return Err(Status::not_found("Source object is a delete marker"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let data = self.load_full_object_bytes(&source_object).await?;
|
||||||
|
let object_metadata = if req.metadata_directive_replace {
|
||||||
|
Self::proto_metadata_to_object_metadata(req.metadata)
|
||||||
|
} else {
|
||||||
|
source_object.metadata.clone()
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut dest_object = Object::new(
|
||||||
|
dest_bucket.id.to_string(),
|
||||||
|
dest_key,
|
||||||
|
source_object.etag.clone(),
|
||||||
|
source_object.size,
|
||||||
|
object_metadata.content_type.clone(),
|
||||||
|
);
|
||||||
|
dest_object.metadata = object_metadata;
|
||||||
|
dest_object.storage_class = source_object.storage_class.clone();
|
||||||
|
if dest_bucket.versioning == lightningstor_types::Versioning::Enabled {
|
||||||
|
dest_object.version = ObjectVersion::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.storage
|
||||||
|
.put_object(&dest_object.id, data)
|
||||||
|
.await
|
||||||
|
.map_err(|e| Status::internal(format!("Failed to store copied object: {}", e)))?;
|
||||||
|
self.metadata
|
||||||
|
.save_object(&dest_object)
|
||||||
|
.await
|
||||||
|
.map_err(Self::to_status)?;
|
||||||
|
|
||||||
|
Ok(Response::new(CopyObjectResponse {
|
||||||
|
etag: dest_object.etag.as_str().to_string(),
|
||||||
|
version_id: dest_object.version.as_str().to_string(),
|
||||||
|
last_modified: Some(prost_types::Timestamp {
|
||||||
|
seconds: dest_object.last_modified.timestamp(),
|
||||||
|
nanos: dest_object.last_modified.timestamp_subsec_nanos() as i32,
|
||||||
|
}),
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn list_objects(
|
async fn list_objects(
|
||||||
|
|
@ -801,10 +932,14 @@ impl ObjectService for ObjectServiceImpl {
|
||||||
|
|
||||||
while let Some(chunk) = stream.message().await? {
|
while let Some(chunk) = stream.message().await? {
|
||||||
if !chunk.bucket.is_empty() && chunk.bucket != first.bucket {
|
if !chunk.bucket.is_empty() && chunk.bucket != first.bucket {
|
||||||
return Err(Status::invalid_argument("bucket changed within UploadPart stream"));
|
return Err(Status::invalid_argument(
|
||||||
|
"bucket changed within UploadPart stream",
|
||||||
|
));
|
||||||
}
|
}
|
||||||
if !chunk.key.is_empty() && chunk.key != first.key {
|
if !chunk.key.is_empty() && chunk.key != first.key {
|
||||||
return Err(Status::invalid_argument("key changed within UploadPart stream"));
|
return Err(Status::invalid_argument(
|
||||||
|
"key changed within UploadPart stream",
|
||||||
|
));
|
||||||
}
|
}
|
||||||
if !chunk.upload_id.is_empty() && chunk.upload_id != first.upload_id {
|
if !chunk.upload_id.is_empty() && chunk.upload_id != first.upload_id {
|
||||||
return Err(Status::invalid_argument(
|
return Err(Status::invalid_argument(
|
||||||
|
|
@ -922,7 +1057,10 @@ impl ObjectService for ObjectServiceImpl {
|
||||||
selected_parts.push(part.clone());
|
selected_parts.push(part.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
let etags: Vec<ETag> = selected_parts.iter().map(|part| part.etag.clone()).collect();
|
let etags: Vec<ETag> = selected_parts
|
||||||
|
.iter()
|
||||||
|
.map(|part| part.etag.clone())
|
||||||
|
.collect();
|
||||||
let multipart_etag = ETag::multipart(&etags, selected_parts.len());
|
let multipart_etag = ETag::multipart(&etags, selected_parts.len());
|
||||||
upload.parts = selected_parts;
|
upload.parts = selected_parts;
|
||||||
|
|
||||||
|
|
@ -1018,7 +1156,11 @@ impl ObjectService for ObjectServiceImpl {
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let max_parts = if req.max_parts > 0 { req.max_parts } else { 1000 };
|
let max_parts = if req.max_parts > 0 {
|
||||||
|
req.max_parts
|
||||||
|
} else {
|
||||||
|
1000
|
||||||
|
};
|
||||||
let remaining_count = upload
|
let remaining_count = upload
|
||||||
.parts
|
.parts
|
||||||
.iter()
|
.iter()
|
||||||
|
|
@ -1064,7 +1206,11 @@ impl ObjectService for ObjectServiceImpl {
|
||||||
|
|
||||||
let bucket_id: BucketId = BucketId::from_str(&bucket.id.to_string())
|
let bucket_id: BucketId = BucketId::from_str(&bucket.id.to_string())
|
||||||
.map_err(|_| Status::internal("Invalid bucket ID"))?;
|
.map_err(|_| Status::internal("Invalid bucket ID"))?;
|
||||||
let max_uploads = if req.max_uploads > 0 { req.max_uploads } else { 1000 };
|
let max_uploads = if req.max_uploads > 0 {
|
||||||
|
req.max_uploads
|
||||||
|
} else {
|
||||||
|
1000
|
||||||
|
};
|
||||||
let uploads = self
|
let uploads = self
|
||||||
.metadata
|
.metadata
|
||||||
.list_multipart_uploads(&bucket_id, &req.prefix, max_uploads)
|
.list_multipart_uploads(&bucket_id, &req.prefix, max_uploads)
|
||||||
|
|
|
||||||
|
|
@ -82,6 +82,7 @@ PLASMAVMC_PROTO_DIR="${REPO_ROOT}/plasmavmc/proto"
|
||||||
PLASMAVMC_PROTO="${PLASMAVMC_PROTO_DIR}/plasmavmc.proto"
|
PLASMAVMC_PROTO="${PLASMAVMC_PROTO_DIR}/plasmavmc.proto"
|
||||||
FLAREDB_PROTO_DIR="${REPO_ROOT}/flaredb/crates/flaredb-proto/src"
|
FLAREDB_PROTO_DIR="${REPO_ROOT}/flaredb/crates/flaredb-proto/src"
|
||||||
FLAREDB_PROTO="${FLAREDB_PROTO_DIR}/kvrpc.proto"
|
FLAREDB_PROTO="${FLAREDB_PROTO_DIR}/kvrpc.proto"
|
||||||
|
FLAREDB_SQL_PROTO="${FLAREDB_PROTO_DIR}/sqlrpc.proto"
|
||||||
|
|
||||||
# shellcheck disable=SC2034
|
# shellcheck disable=SC2034
|
||||||
NODE_PHASES=(
|
NODE_PHASES=(
|
||||||
|
|
@ -2507,6 +2508,7 @@ ensure_flaredb_proto_on_node() {
|
||||||
|
|
||||||
ssh_node "${node}" "install -d -m 0755 ${proto_root}"
|
ssh_node "${node}" "install -d -m 0755 ${proto_root}"
|
||||||
scp_to_node "${node}" "${FLAREDB_PROTO}" "${proto_root}/kvrpc.proto"
|
scp_to_node "${node}" "${FLAREDB_PROTO}" "${proto_root}/kvrpc.proto"
|
||||||
|
scp_to_node "${node}" "${FLAREDB_SQL_PROTO}" "${proto_root}/sqlrpc.proto"
|
||||||
}
|
}
|
||||||
|
|
||||||
vm_runtime_dir_path() {
|
vm_runtime_dir_path() {
|
||||||
|
|
@ -3072,6 +3074,74 @@ while true; do
|
||||||
fi
|
fi
|
||||||
sleep 1
|
sleep 1
|
||||||
done
|
done
|
||||||
|
EOS
|
||||||
|
|
||||||
|
log "Validating FlareDB SQL DDL/DML execution"
|
||||||
|
ssh_node_script node01 "${flaredb_proto_root}" "10.100.0.11:2479" <<'EOS'
|
||||||
|
set -euo pipefail
|
||||||
|
proto_root="$1"
|
||||||
|
sql_target="$2"
|
||||||
|
sql_namespace="validation-sql-$(date +%s)"
|
||||||
|
|
||||||
|
grpcurl -plaintext \
|
||||||
|
-import-path "${proto_root}" \
|
||||||
|
-proto "${proto_root}/sqlrpc.proto" \
|
||||||
|
-d "$(jq -cn --arg namespace "${sql_namespace}" --arg sql "CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT NOT NULL, active BOOLEAN)" '{namespace:$namespace, sql:$sql}')" \
|
||||||
|
"${sql_target}" sqlrpc.SqlService/Execute \
|
||||||
|
| jq -e '.ddlResult.message | contains("created")' >/dev/null
|
||||||
|
|
||||||
|
grpcurl -plaintext \
|
||||||
|
-import-path "${proto_root}" \
|
||||||
|
-proto "${proto_root}/sqlrpc.proto" \
|
||||||
|
-d "$(jq -cn --arg namespace "${sql_namespace}" --arg sql "INSERT INTO users (id, name, active) VALUES (1, 'Alice', true)" '{namespace:$namespace, sql:$sql}')" \
|
||||||
|
"${sql_target}" sqlrpc.SqlService/Execute \
|
||||||
|
| jq -e '(.dmlResult.rowsAffected | tonumber) == 1' >/dev/null
|
||||||
|
|
||||||
|
grpcurl -plaintext \
|
||||||
|
-import-path "${proto_root}" \
|
||||||
|
-proto "${proto_root}/sqlrpc.proto" \
|
||||||
|
-d "$(jq -cn --arg namespace "${sql_namespace}" --arg sql "INSERT INTO users (id, name, active) VALUES (2, 'Bob', false)" '{namespace:$namespace, sql:$sql}')" \
|
||||||
|
"${sql_target}" sqlrpc.SqlService/Execute \
|
||||||
|
| jq -e '(.dmlResult.rowsAffected | tonumber) == 1' >/dev/null
|
||||||
|
|
||||||
|
grpcurl -plaintext \
|
||||||
|
-import-path "${proto_root}" \
|
||||||
|
-proto "${proto_root}/sqlrpc.proto" \
|
||||||
|
-d "$(jq -cn --arg namespace "${sql_namespace}" --arg sql "UPDATE users SET name = 'Carol', active = false WHERE id = 1" '{namespace:$namespace, sql:$sql}')" \
|
||||||
|
"${sql_target}" sqlrpc.SqlService/Execute \
|
||||||
|
| jq -e '(.dmlResult.rowsAffected | tonumber) == 1' >/dev/null
|
||||||
|
|
||||||
|
grpcurl -plaintext \
|
||||||
|
-import-path "${proto_root}" \
|
||||||
|
-proto "${proto_root}/sqlrpc.proto" \
|
||||||
|
-d "$(jq -cn --arg namespace "${sql_namespace}" --arg sql "SELECT id, name, active FROM users WHERE id = 1" '{namespace:$namespace, sql:$sql}')" \
|
||||||
|
"${sql_target}" sqlrpc.SqlService/Execute \
|
||||||
|
| jq -e '
|
||||||
|
.queryResult.columns == ["id", "name", "active"] and
|
||||||
|
(.queryResult.rows | length) == 1 and
|
||||||
|
.queryResult.rows[0].values[0].intValue == "1" and
|
||||||
|
.queryResult.rows[0].values[1].textValue == "Carol" and
|
||||||
|
((.queryResult.rows[0].values[2].boolValue // false) == false)
|
||||||
|
' >/dev/null
|
||||||
|
|
||||||
|
grpcurl -plaintext \
|
||||||
|
-import-path "${proto_root}" \
|
||||||
|
-proto "${proto_root}/sqlrpc.proto" \
|
||||||
|
-d "$(jq -cn --arg namespace "${sql_namespace}" --arg sql "DELETE FROM users WHERE id = 2" '{namespace:$namespace, sql:$sql}')" \
|
||||||
|
"${sql_target}" sqlrpc.SqlService/Execute \
|
||||||
|
| jq -e '(.dmlResult.rowsAffected | tonumber) == 1' >/dev/null
|
||||||
|
|
||||||
|
grpcurl -plaintext \
|
||||||
|
-import-path "${proto_root}" \
|
||||||
|
-proto "${proto_root}/sqlrpc.proto" \
|
||||||
|
-d "$(jq -cn --arg namespace "${sql_namespace}" --arg sql "SELECT id, name FROM users" '{namespace:$namespace, sql:$sql}')" \
|
||||||
|
"${sql_target}" sqlrpc.SqlService/Execute \
|
||||||
|
| jq -e '
|
||||||
|
.queryResult.columns == ["id", "name"] and
|
||||||
|
(.queryResult.rows | length) == 1 and
|
||||||
|
.queryResult.rows[0].values[0].intValue == "1" and
|
||||||
|
.queryResult.rows[0].values[1].textValue == "Carol"
|
||||||
|
' >/dev/null
|
||||||
EOS
|
EOS
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3328,7 +3398,7 @@ validate_fiberlb_flow() {
|
||||||
local org_id="fiberlb-smoke-org"
|
local org_id="fiberlb-smoke-org"
|
||||||
local project_id="fiberlb-smoke-project"
|
local project_id="fiberlb-smoke-project"
|
||||||
local principal_id="fiberlb-smoke-$(date +%s)"
|
local principal_id="fiberlb-smoke-$(date +%s)"
|
||||||
local token lb_id pool_id backend_id listener_id listener_port
|
local token lb_id pool_id health_check_id backend_id listener_id listener_port
|
||||||
token="$(issue_project_admin_token 15080 "${org_id}" "${project_id}" "${principal_id}")"
|
token="$(issue_project_admin_token 15080 "${org_id}" "${project_id}" "${principal_id}")"
|
||||||
listener_port=$((18080 + (RANDOM % 100)))
|
listener_port=$((18080 + (RANDOM % 100)))
|
||||||
|
|
||||||
|
|
@ -3350,6 +3420,39 @@ validate_fiberlb_flow() {
|
||||||
| jq -r '.pool.id')"
|
| jq -r '.pool.id')"
|
||||||
[[ -n "${pool_id}" && "${pool_id}" != "null" ]] || die "FiberLB CreatePool did not return an ID"
|
[[ -n "${pool_id}" && "${pool_id}" != "null" ]] || die "FiberLB CreatePool did not return an ID"
|
||||||
|
|
||||||
|
health_check_id="$(grpcurl -plaintext \
|
||||||
|
-H "authorization: Bearer ${token}" \
|
||||||
|
-import-path "${FIBERLB_PROTO_DIR}" \
|
||||||
|
-proto "${FIBERLB_PROTO}" \
|
||||||
|
-d "$(jq -cn --arg name "fiberlb-smoke-health" --arg pool "${pool_id}" '{name:$name, poolId:$pool, type:"HEALTH_CHECK_TYPE_HTTP", intervalSeconds:10, timeoutSeconds:3, healthyThreshold:2, unhealthyThreshold:2, httpConfig:{method:"GET", path:"/health", expectedCodes:[200]}}')" \
|
||||||
|
127.0.0.1:15085 fiberlb.v1.HealthCheckService/CreateHealthCheck \
|
||||||
|
| jq -r '.healthCheck.id')"
|
||||||
|
[[ -n "${health_check_id}" && "${health_check_id}" != "null" ]] || die "FiberLB CreateHealthCheck did not return an ID"
|
||||||
|
|
||||||
|
grpcurl -plaintext \
|
||||||
|
-H "authorization: Bearer ${token}" \
|
||||||
|
-import-path "${FIBERLB_PROTO_DIR}" \
|
||||||
|
-proto "${FIBERLB_PROTO}" \
|
||||||
|
-d "$(jq -cn --arg id "${health_check_id}" '{id:$id}')" \
|
||||||
|
127.0.0.1:15085 fiberlb.v1.HealthCheckService/GetHealthCheck \
|
||||||
|
| jq -e --arg id "${health_check_id}" '.healthCheck.id == $id and .healthCheck.httpConfig.path == "/health"' >/dev/null
|
||||||
|
|
||||||
|
grpcurl -plaintext \
|
||||||
|
-H "authorization: Bearer ${token}" \
|
||||||
|
-import-path "${FIBERLB_PROTO_DIR}" \
|
||||||
|
-proto "${FIBERLB_PROTO}" \
|
||||||
|
-d "$(jq -cn --arg pool "${pool_id}" '{poolId:$pool}')" \
|
||||||
|
127.0.0.1:15085 fiberlb.v1.HealthCheckService/ListHealthChecks \
|
||||||
|
| jq -e --arg id "${health_check_id}" '(.healthChecks | length) == 1 and .healthChecks[0].id == $id' >/dev/null
|
||||||
|
|
||||||
|
grpcurl -plaintext \
|
||||||
|
-H "authorization: Bearer ${token}" \
|
||||||
|
-import-path "${FIBERLB_PROTO_DIR}" \
|
||||||
|
-proto "${FIBERLB_PROTO}" \
|
||||||
|
-d "$(jq -cn --arg id "${health_check_id}" '{id:$id, name:"fiberlb-smoke-health-updated", intervalSeconds:15, timeoutSeconds:4, healthyThreshold:3, unhealthyThreshold:4, httpConfig:{method:"GET", path:"/readyz", expectedCodes:[200,204]}, enabled:false}')" \
|
||||||
|
127.0.0.1:15085 fiberlb.v1.HealthCheckService/UpdateHealthCheck \
|
||||||
|
| jq -e '.healthCheck.name == "fiberlb-smoke-health-updated" and .healthCheck.httpConfig.path == "/readyz" and ((.healthCheck.enabled // false) == false)' >/dev/null
|
||||||
|
|
||||||
backend_id="$(grpcurl -plaintext \
|
backend_id="$(grpcurl -plaintext \
|
||||||
-H "authorization: Bearer ${token}" \
|
-H "authorization: Bearer ${token}" \
|
||||||
-import-path "${FIBERLB_PROTO_DIR}" \
|
-import-path "${FIBERLB_PROTO_DIR}" \
|
||||||
|
|
@ -3442,6 +3545,12 @@ validate_fiberlb_flow() {
|
||||||
-proto "${FIBERLB_PROTO}" \
|
-proto "${FIBERLB_PROTO}" \
|
||||||
-d "$(jq -cn --arg id "${backend_id}" '{id:$id}')" \
|
-d "$(jq -cn --arg id "${backend_id}" '{id:$id}')" \
|
||||||
127.0.0.1:15085 fiberlb.v1.BackendService/DeleteBackend >/dev/null
|
127.0.0.1:15085 fiberlb.v1.BackendService/DeleteBackend >/dev/null
|
||||||
|
grpcurl -plaintext \
|
||||||
|
-H "authorization: Bearer ${token}" \
|
||||||
|
-import-path "${FIBERLB_PROTO_DIR}" \
|
||||||
|
-proto "${FIBERLB_PROTO}" \
|
||||||
|
-d "$(jq -cn --arg id "${health_check_id}" '{id:$id}')" \
|
||||||
|
127.0.0.1:15085 fiberlb.v1.HealthCheckService/DeleteHealthCheck >/dev/null
|
||||||
grpcurl -plaintext \
|
grpcurl -plaintext \
|
||||||
-H "authorization: Bearer ${token}" \
|
-H "authorization: Bearer ${token}" \
|
||||||
-import-path "${FIBERLB_PROTO_DIR}" \
|
-import-path "${FIBERLB_PROTO_DIR}" \
|
||||||
|
|
@ -3798,11 +3907,12 @@ validate_lightningstor_distributed_storage() {
|
||||||
read -r before_node01 before_node04 before_node05 < <(lightningstor_count_triplet)
|
read -r before_node01 before_node04 before_node05 < <(lightningstor_count_triplet)
|
||||||
|
|
||||||
local key="replication-check-$(date +%s)"
|
local key="replication-check-$(date +%s)"
|
||||||
|
local copy_key="${key}-copy"
|
||||||
local body="distributed-object-${key}"
|
local body="distributed-object-${key}"
|
||||||
local body_b64
|
local body_b64
|
||||||
body_b64="$(printf '%s' "${body}" | base64 -w0)"
|
body_b64="$(printf '%s' "${body}" | base64 -w0)"
|
||||||
|
|
||||||
local put_json head_json delete_json output
|
local put_json head_json copy_head_json delete_json copy_delete_json output
|
||||||
put_json="$(
|
put_json="$(
|
||||||
jq -cn \
|
jq -cn \
|
||||||
--arg bucket "${bucket}" \
|
--arg bucket "${bucket}" \
|
||||||
|
|
@ -3849,7 +3959,54 @@ validate_lightningstor_distributed_storage() {
|
||||||
|
|
||||||
wait_for_lightningstor_counts_greater_than "${before_node01}" "${before_node04}" "${before_node05}" "generic object replication"
|
wait_for_lightningstor_counts_greater_than "${before_node01}" "${before_node04}" "${before_node05}" "generic object replication"
|
||||||
|
|
||||||
|
local copy_json copied_body
|
||||||
|
copy_json="$(
|
||||||
|
jq -cn \
|
||||||
|
--arg source_bucket "${bucket}" \
|
||||||
|
--arg source_key "${key}" \
|
||||||
|
--arg dest_bucket "${bucket}" \
|
||||||
|
--arg dest_key "${copy_key}" \
|
||||||
|
'{sourceBucket:$source_bucket, sourceKey:$source_key, destBucket:$dest_bucket, destKey:$dest_key, metadataDirectiveReplace:true, metadata:{contentType:"text/copied", userMetadata:{copied:"yes"}}}'
|
||||||
|
)"
|
||||||
|
log "LightningStor distributed replication: COPY ${bucket}/${key} -> ${bucket}/${copy_key}"
|
||||||
|
output="$(
|
||||||
|
grpcurl_capture -plaintext \
|
||||||
|
-H "authorization: Bearer ${token}" \
|
||||||
|
-import-path "${LIGHTNINGSTOR_PROTO_DIR}" \
|
||||||
|
-proto "${LIGHTNINGSTOR_PROTO}" \
|
||||||
|
-d "${copy_json}" \
|
||||||
|
127.0.0.1:15086 lightningstor.v1.ObjectService/CopyObject
|
||||||
|
)" || die "failed to copy LightningStor distributed replication probe ${bucket}/${key}: ${output}"
|
||||||
|
printf '%s\n' "${output}" | jq -e '.etag != "" and .versionId != ""' >/dev/null \
|
||||||
|
|| die "LightningStor copy response was incomplete: ${output}"
|
||||||
|
|
||||||
|
copy_head_json="$(jq -cn --arg bucket "${bucket}" --arg key "${copy_key}" '{bucket:$bucket, key:$key}')"
|
||||||
|
output="$(
|
||||||
|
grpcurl_capture -plaintext \
|
||||||
|
-H "authorization: Bearer ${token}" \
|
||||||
|
-import-path "${LIGHTNINGSTOR_PROTO_DIR}" \
|
||||||
|
-proto "${LIGHTNINGSTOR_PROTO}" \
|
||||||
|
-d "${copy_head_json}" \
|
||||||
|
127.0.0.1:15086 lightningstor.v1.ObjectService/HeadObject
|
||||||
|
)" || die "failed to head copied LightningStor object ${bucket}/${copy_key}: ${output}"
|
||||||
|
printf '%s\n' "${output}" \
|
||||||
|
| jq -e --arg size "$(printf '%s' "${body}" | wc -c | awk '{print $1}')" '.object.metadata.contentType == "text/copied" and (.object.size | tonumber) == ($size | tonumber)' >/dev/null \
|
||||||
|
|| die "LightningStor copied object returned unexpected metadata: ${output}"
|
||||||
|
|
||||||
|
output="$(
|
||||||
|
grpcurl_capture -plaintext \
|
||||||
|
-H "authorization: Bearer ${token}" \
|
||||||
|
-import-path "${LIGHTNINGSTOR_PROTO_DIR}" \
|
||||||
|
-proto "${LIGHTNINGSTOR_PROTO}" \
|
||||||
|
-d "${copy_head_json}" \
|
||||||
|
127.0.0.1:15086 lightningstor.v1.ObjectService/GetObject
|
||||||
|
)" || die "failed to fetch copied LightningStor object ${bucket}/${copy_key}: ${output}"
|
||||||
|
copied_body="$(printf '%s\n' "${output}" | jq -rsr '[.[] | .bodyChunk? | select(. != null) | @base64d] | join("")')" \
|
||||||
|
|| die "failed to decode copied LightningStor object ${bucket}/${copy_key}: ${output}"
|
||||||
|
[[ "${copied_body}" == "${body}" ]] || die "copied LightningStor object payload did not match source"
|
||||||
|
|
||||||
delete_json="$(jq -cn --arg bucket "${bucket}" --arg key "${key}" '{bucket:$bucket, key:$key}')"
|
delete_json="$(jq -cn --arg bucket "${bucket}" --arg key "${key}" '{bucket:$bucket, key:$key}')"
|
||||||
|
copy_delete_json="$(jq -cn --arg bucket "${bucket}" --arg key "${copy_key}" '{bucket:$bucket, key:$key}')"
|
||||||
log "LightningStor distributed replication: DELETE ${bucket}/${key}"
|
log "LightningStor distributed replication: DELETE ${bucket}/${key}"
|
||||||
output="$(
|
output="$(
|
||||||
grpcurl_capture -plaintext \
|
grpcurl_capture -plaintext \
|
||||||
|
|
@ -3860,6 +4017,16 @@ validate_lightningstor_distributed_storage() {
|
||||||
127.0.0.1:15086 lightningstor.v1.ObjectService/DeleteObject
|
127.0.0.1:15086 lightningstor.v1.ObjectService/DeleteObject
|
||||||
)" || die "failed to delete LightningStor distributed replication probe ${bucket}/${key}: ${output}"
|
)" || die "failed to delete LightningStor distributed replication probe ${bucket}/${key}: ${output}"
|
||||||
|
|
||||||
|
log "LightningStor distributed replication: DELETE ${bucket}/${copy_key}"
|
||||||
|
output="$(
|
||||||
|
grpcurl_capture -plaintext \
|
||||||
|
-H "authorization: Bearer ${token}" \
|
||||||
|
-import-path "${LIGHTNINGSTOR_PROTO_DIR}" \
|
||||||
|
-proto "${LIGHTNINGSTOR_PROTO}" \
|
||||||
|
-d "${copy_delete_json}" \
|
||||||
|
127.0.0.1:15086 lightningstor.v1.ObjectService/DeleteObject
|
||||||
|
)" || die "failed to delete copied LightningStor object ${bucket}/${copy_key}: ${output}"
|
||||||
|
|
||||||
wait_for_lightningstor_counts_equal "${before_node01}" "${before_node04}" "${before_node05}" "generic object cleanup"
|
wait_for_lightningstor_counts_equal "${before_node01}" "${before_node04}" "${before_node05}" "generic object cleanup"
|
||||||
|
|
||||||
trap - RETURN
|
trap - RETURN
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue