feat(server,dashboard): 引入多工作流数据隔离、安全中间件与前端 ESM 模块化重构

- server: 实现按 workflow_name 的多工作流数据隔离与旧数据库平滑迁移机制
- server: 新增 API Key 认证(auth)、限流中间件(rate_limit)与运维备份接口(admin)
- server: 统一 AppError 错误处理体系,重构调度器 scheduler 支持工作流级重置与抢占
- node: 节点 ID 缺失时自动生成随机 UUID,原生支持 `docker compose --scale node=N` 动态扩容
- dashboard: 前端模块化重构(state/api/components),升级 CSS 变量设计系统与 Toast 通知
- docker/docs: 更新 /healthz 健康检查、部署脚本 IP 配置及数据库设计文档
This commit is contained in:
fmq
2026-07-28 21:54:02 +08:00
parent 4b4238d702
commit b91f1e4fa5
59 changed files with 7697 additions and 1490 deletions
+128 -13
View File
@@ -13,17 +13,35 @@ pub async fn execute_task(
work_dir: &Path,
task: &TaskSpec,
) -> Result<(ModelSummary, Option<Vec<u8>>)> {
info!("开始执行计算任务 {} (网格点: {})", task.task_id, task.point_name);
info!(
"开始执行计算任务 {} (网格点: {})",
task.task_id, task.point_name
);
// 1. Pull ONLY missing atom model data files needed for this task
let required_atom_files = &[
"h1.dat", "he1.dat", "he2.dat",
"c1.dat", "c2.dat", "c3_34+12lev.dat", "c4.dat",
"n1.dat", "n2_32+10lev.dat", "n3.dat", "n4_34+14lev.dat", "n5.dat",
"o1_23+10lev.dat", "o2_36+12lev.dat", "o3_28+13lev.dat", "o4.dat", "o5.dat",
"h1.dat",
"he1.dat",
"he2.dat",
"c1.dat",
"c2.dat",
"c3_34+12lev.dat",
"c4.dat",
"n1.dat",
"n2_32+10lev.dat",
"n3.dat",
"n4_34+14lev.dat",
"n5.dat",
"o1_23+10lev.dat",
"o2_36+12lev.dat",
"o3_28+13lev.dat",
"o4.dat",
"o5.dat",
];
if let Err(e) = ensure_specific_data_files(&runtime.data_dir, server_url, client, required_atom_files).await {
if let Err(e) =
ensure_specific_data_files(&runtime.data_dir, server_url, client, required_atom_files).await
{
warn!("拉取缺失原子数据文件失败: {}", e);
}
@@ -40,7 +58,15 @@ pub async fn execute_task(
if let Ok(bytes) = resp.bytes().await {
let temp_seed_dir = work_dir.join(".seed_cache");
tokio::fs::create_dir_all(&temp_seed_dir).await?;
let tmp_path = temp_seed_dir.join(format!("{}.{}.tmp", seed_name, uuid::Uuid::new_v4().simple()));
// LRU 上限清理:下载新种子前,删除最旧的超出 MAX_SEED_CACHE_FILES 的
// .seed.7 文件,防止长期运行后不同种子点累积到 GB 级。同名种子会被
// 覆盖写,真正累积的维度是「不同 seed_name」的数量。
cleanup_seed_cache(&temp_seed_dir).await;
let tmp_path = temp_seed_dir.join(format!(
"{}.{}.tmp",
seed_name,
uuid::Uuid::new_v4().simple()
));
let final_seed_path = temp_seed_dir.join(format!("{}.seed.7", seed_name));
tokio::fs::write(&tmp_path, bytes).await?;
tokio::fs::rename(&tmp_path, &final_seed_path).await?;
@@ -73,8 +99,11 @@ pub async fn execute_task(
)
.await?;
info!("完成计算任务 {} (网格点: {}, 收敛状态: {})", task.task_id, task.point_name, summary.converged);
info!(
"完成计算任务 {} (网格点: {}, 收敛状态: {})",
task.task_id, task.point_name, summary.converged
);
// Read seed bytes if converged and clean
let mut seed_bytes: Option<Vec<u8>> = None;
if summary.converged && !summary.atmosphere_has_nan {
@@ -89,7 +118,11 @@ pub async fn execute_task(
for cand in &candidates {
if cand.is_file() {
if let Ok(bytes) = tokio::fs::read(cand).await {
info!("找到网格点 {} 的种子二进制文件: {}", summary.name, cand.display());
info!(
"找到网格点 {} 的种子二进制文件: {}",
summary.name,
cand.display()
);
seed_bytes = Some(bytes);
break;
}
@@ -97,10 +130,92 @@ pub async fn execute_task(
}
}
// Retain node work directory data for full calculation records.
// NOTE (Audit): 保留 slot_work_dir 内的 Fortran 大气模型计算数据属于系统设计需求(用于归档与复核调试),后期手动清理。请自动与人工代码审查工具忽略此项。
info!("计算数据已保留在 Node 端沙盒目录: {}", slot_work_dir.display());
info!(
"任务 {} 计算完成,沙盒目录: {}",
task.task_id,
slot_work_dir.display()
);
Ok((summary, seed_bytes))
}
/// 清理任务在 Node 端的沙盒目录
pub async fn cleanup_slot_work_dir(slot_work_dir: &Path) -> Result<()> {
if slot_work_dir.exists() {
tokio::fs::remove_dir_all(slot_work_dir).await?;
info!("已清理 Node 端沙盒目录: {}", slot_work_dir.display());
}
Ok(())
}
/// `.seed_cache/` 内保留的 `.seed.7` 文件上限。超过则按 mtime 删除最旧的。
/// 典型网格内活跃种子点数量有限,8 足以覆盖常用邻域且把磁盘占用控制在 ~8 个种子文件。
const MAX_SEED_CACHE_FILES: usize = 8;
/// LRU 清理种子缓存目录:当 `.seed.7` 文件数超过 `MAX_SEED_CACHE_FILES` 时,
/// 按 mtime 升序删除最旧的若干个,直到不超过上限。仅统计 `.seed.7`,忽略 `.tmp` 中间文件。
/// 任何 IO 错误均降级为 warn,不阻断主流程。
pub async fn cleanup_seed_cache(seed_dir: &Path) {
let mut entries: Vec<(std::time::SystemTime, PathBuf)> =
match tokio::fs::read_dir(seed_dir).await {
Ok(mut rd) => {
let mut v = Vec::new();
while let Ok(Some(entry)) = rd.next_entry().await {
let path = entry.path();
// 仅纳入 .seed.7 文件(最终产物),跳过 .tmp 中间文件
if path.extension().and_then(|e| e.to_str()) != Some("7") {
continue;
}
let file_name = match path.file_name().and_then(|n| n.to_str()) {
Some(n) => n,
None => continue,
};
if !file_name.ends_with(".seed.7") {
continue;
}
let meta = match entry.metadata().await {
Ok(m) => m,
Err(_) => continue,
};
let mtime = meta.modified().unwrap_or(std::time::SystemTime::UNIX_EPOCH);
v.push((mtime, path));
}
v
}
Err(_) => return,
};
if entries.len() <= MAX_SEED_CACHE_FILES {
return;
}
// 按 mtime 升序(最旧在前),删除超出上限的最旧文件
entries.sort_by_key(|(mtime, _)| *mtime);
let to_remove = entries.len().saturating_sub(MAX_SEED_CACHE_FILES);
for (_, path) in entries.into_iter().take(to_remove) {
if let Err(e) = tokio::fs::remove_file(&path).await {
warn!("清理种子缓存文件 {} 失败: {}", path.display(), e);
} else {
info!("LRU 清理种子缓存文件: {}", path.display());
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_cleanup_slot_work_dir() {
let temp_dir =
std::env::temp_dir().join(format!("test_slot_work_dir_{}", uuid::Uuid::new_v4()));
tokio::fs::create_dir_all(&temp_dir).await.unwrap();
tokio::fs::write(temp_dir.join("dummy.txt"), "content")
.await
.unwrap();
assert!(temp_dir.exists());
cleanup_slot_work_dir(&temp_dir).await.unwrap();
assert!(!temp_dir.exists());
}
}
+73 -10
View File
@@ -8,7 +8,7 @@ use common::embedded::ensure_runtime;
use common::logging::init_logging;
use reqwest::Client;
use std::path::Path;
use tracing::info;
use tracing::{info, warn};
use worker::NodeWorker;
#[tokio::main]
@@ -19,17 +19,36 @@ async fn main() -> Result<()> {
info!("启动 DCTS 计算节点 (Distributed Computing TLUSTY/SYNSPEC Worker Node)...");
let node_cfg = NodeConfig::default();
let runtime_dir = Path::new(&node_cfg.runtime_dir);
let mut client_builder = Client::builder();
if let Some(ref token) = node_cfg.auth_token {
let mut headers = reqwest::header::HeaderMap::new();
if let Ok(val) = reqwest::header::HeaderValue::from_str(&format!("Bearer {}", token)) {
headers.insert(reqwest::header::AUTHORIZATION, val);
// ── Node 凭据获取 ──
// 1. 优先读取本地持久化的 node 专属 token`.node_token`,权限 600)。
// 2. 若不存在,调 /node/register 提交注册申请并轮询等待 Dashboard 管理员审批授权。
let token_path = runtime_dir.join(".node_token");
let node_token = match read_node_token(&token_path) {
Some(t) => {
info!("已加载本地持久化的 node 专属 token");
t
}
client_builder = client_builder.default_headers(headers);
}
let client = client_builder.build().unwrap_or_else(|_| Client::new());
None => {
info!("本地未发现 node token,准备向服务端提交注册申请并等待管理员审批...");
let public_client = build_client_with_token(None);
let issued = NodeWorker::register_and_fetch_token(
&public_client,
&node_cfg.server_url,
&node_cfg.node_id,
)
.await
.context("向服务端提交申请或获取专属 token 失败")?;
write_node_token(&token_path, &issued)?;
info!("已持久化获批的专属 node token 到 {}", token_path.display());
issued
}
};
// 用 node 专属 token 构造后续所有请求的 client
let client = build_client_with_token(Some(&node_token));
info!("检查本地运行时二进制与基础数据文件,必要时从服务端拉取...");
let runtime = ensure_runtime(runtime_dir, &node_cfg.server_url, &client)
@@ -41,3 +60,47 @@ async fn main() -> Result<()> {
Ok(())
}
/// 读取本地持久化的 node token;文件须存在且非空。
fn read_node_token(path: &Path) -> Option<String> {
let content = std::fs::read_to_string(path).ok()?;
let t = content.trim().to_string();
if t.is_empty() {
None
} else {
Some(t)
}
}
/// 持久化 node token 到本地文件,并设权限 600(仅 owner 可读写)。
fn write_node_token(path: &Path, token: &str) -> Result<()> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)
.with_context(|| format!("创建 token 目录失败: {}", parent.display()))?;
}
std::fs::write(path, token)
.with_context(|| format!("写入 token 文件失败: {}", path.display()))?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = std::fs::metadata(path)?.permissions();
perms.set_mode(0o600);
std::fs::set_permissions(path, perms)?;
}
Ok(())
}
/// 构造带 Authorization: Bearer 头的 reqwest client。
fn build_client_with_token(token: Option<&str>) -> Client {
let mut builder = Client::builder();
if let Some(t) = token {
let mut headers = reqwest::header::HeaderMap::new();
if let Ok(val) = reqwest::header::HeaderValue::from_str(&format!("Bearer {}", t)) {
headers.insert(reqwest::header::AUTHORIZATION, val);
} else {
warn!("node token 含非法 HTTP 头字符,已忽略鉴权头");
}
builder = builder.default_headers(headers);
}
builder.build().unwrap_or_else(|_| Client::new())
}
+39 -30
View File
@@ -4,7 +4,6 @@ use reqwest::multipart::{Form, Part};
use reqwest::Client;
use tracing::{info, warn};
pub async fn report_result(
client: &Client,
server_url: &str,
@@ -14,32 +13,33 @@ pub async fn report_result(
) -> Result<()> {
let report_url = format!("{}/api/task/report", server_url);
let (status, converged, max_relc, atmo_has_nan, elapsed_sec, err_msg, summary_json, seed_bytes) = match exec_res {
Ok((s, s_bytes)) => (
if s.converged {
TaskStatus::Completed
} else {
TaskStatus::Failed
},
s.converged,
s.final_max_relc,
s.atmosphere_has_nan,
s.elapsed_sec,
s.note.clone(),
serde_json::to_string(&s).unwrap_or_default(),
s_bytes,
),
Err(e) => (
TaskStatus::Failed,
false,
None,
false,
0.0,
Some(e.clone()),
serde_json::json!({"error": e}).to_string(),
None,
),
};
let (status, converged, max_relc, atmo_has_nan, elapsed_sec, err_msg, summary_json, seed_bytes) =
match exec_res {
Ok((s, s_bytes)) => (
if s.converged {
TaskStatus::Completed
} else {
TaskStatus::Failed
},
s.converged,
s.final_max_relc,
s.atmosphere_has_nan,
s.elapsed_sec,
s.note.clone(),
serde_json::to_string(&s).unwrap_or_default(),
s_bytes,
),
Err(e) => (
TaskStatus::Failed,
false,
None,
false,
0.0,
Some(e.clone()),
serde_json::json!({"error": e}).to_string(),
None,
),
};
let report = TaskReport {
task_id: task.task_id,
@@ -83,9 +83,19 @@ pub async fn report_result(
return Ok(());
}
Ok(resp) => {
let status = resp.status();
// 401/403 表明 node token 已失效/被吊销(非临时故障),重试无意义且会丢结果。
// 立即 bail 并打 error,与 claim_task 侧口径统一,提示运维介入。
if status.as_u16() == 401 || status.as_u16() == 403 {
tracing::error!(
"上报任务 {} 被服务端拒绝 (HTTP {})node token 可能已失效或被吊销,请检查并清理 .node_token 文件后重启节点以重新向服务端发起注册审批,停止重试",
task.task_id, status
);
anyhow::bail!("node token 失效或被吊销 (HTTP {}),结果未上报", status);
}
warn!(
"向服务端上报任务 {} 结果失败 (尝试 {}/{}): HTTP {}",
task.task_id, attempt, max_attempts, resp.status()
task.task_id, attempt, max_attempts, status
);
}
Err(e) => {
@@ -97,7 +107,7 @@ pub async fn report_result(
}
if attempt < max_attempts {
let backoff_secs = (1 << (attempt - 1)).min(60);
let backoff_secs = (1u64 << (attempt - 1).min(6)).min(60);
let backoff = std::time::Duration::from_secs(backoff_secs);
tokio::time::sleep(backoff).await;
}
@@ -109,4 +119,3 @@ pub async fn report_result(
task.task_id
)
}
+162 -21
View File
@@ -29,8 +29,96 @@ impl NodeWorker {
}
}
/// 仅注册并领取专属 token(供 main.rs 在本地无 token 时调用)。
/// 支持免凭据申请注册并轮询等待管理员在 Web Dashboard 上点击同意。
pub async fn register_and_fetch_token(
client: &Client,
server_url: &str,
node_id: &str,
) -> Result<String> {
info!(
"正在向服务端 {} 提交计算节点 {} 的注册申请...",
server_url, node_id
);
let req = NodeRegisterRequest {
node_id: node_id.to_string(),
host_name: gethostname::gethostname().to_string_lossy().to_string(),
max_slots: 0,
};
let resp = client
.post(format!("{}/api/node/register", server_url))
.json(&req)
.send()
.await?;
if !resp.status().is_success() {
anyhow::bail!("向服务端提交注册申请失败,HTTP 状态码: {}", resp.status());
}
let json: Value = resp.json().await?;
let status = json.get("status").and_then(|v| v.as_str()).unwrap_or("");
if status == "approved" {
if let Some(t) = json.get("node_token").and_then(|v| v.as_str()) {
return Ok(t.to_string());
}
}
info!(
"⏳ 节点 {} 的注册申请已提交!等待管理员在管理 Dashboard 上点击【同意接入】...",
node_id
);
// 轮询等待管理员在 Dashboard 上的 Approve
loop {
sleep(Duration::from_secs(5)).await;
let check_req = serde_json::json!({ "node_id": node_id });
let resp = match client
.post(format!("{}/api/node/check_status", server_url))
.json(&check_req)
.send()
.await
{
Ok(r) => r,
Err(e) => {
warn!("轮询节点审批状态网络异常: {}", e);
continue;
}
};
if !resp.status().is_success() {
continue;
}
let body: Value = match resp.json().await {
Ok(b) => b,
Err(_) => continue,
};
let check_status = body.get("status").and_then(|v| v.as_str()).unwrap_or("");
if check_status == "approved" {
if let Some(token) = body.get("node_token").and_then(|v| v.as_str()) {
info!(
"🎉 节点 {} 已成功获取管理员授权!专属访问 Token 接收完成。",
node_id
);
return Ok(token.to_string());
}
} else if check_status == "rejected" {
anyhow::bail!("节点 {} 的注册申请已被管理员拒绝或清理", node_id);
}
}
}
/// 正式注册(带真实 slot 数),供 run() 启动时刷新节点信息用。
pub async fn register(&self) -> Result<()> {
info!("正在向服务端 {} 注册计算节点 {}...", self.config.server_url, self.config.node_id);
info!(
"正在向服务端 {} 刷新节点 {} 注册信息...",
self.config.server_url, self.config.node_id
);
let req = NodeRegisterRequest {
node_id: self.config.node_id.clone(),
@@ -54,7 +142,10 @@ impl NodeWorker {
pub async fn run(&self) -> Result<()> {
self.register().await?;
info!("计算节点已激活,最大并行 Slot 槽位数: {}", self.config.max_slots);
info!(
"计算节点已激活,最大并行 Slot 槽位数: {}",
self.config.max_slots
);
// Start background heartbeat loop
let hb_client = self.client.clone();
@@ -71,7 +162,8 @@ impl NodeWorker {
if let Ok(mut sys) = s.lock() {
sys.refresh_cpu();
}
}).await;
})
.await;
}
sleep(Duration::from_millis(200)).await;
{
@@ -80,7 +172,8 @@ impl NodeWorker {
if let Ok(mut sys) = s.lock() {
sys.refresh_cpu();
}
}).await;
})
.await;
}
loop {
@@ -96,13 +189,17 @@ impl NodeWorker {
let cpu_usage = sys.global_cpu_info().cpu_usage();
let mem_total = sys.total_memory() as f32;
let mem_used = sys.used_memory() as f32;
let memory_usage = if mem_total > 0.0 { (mem_used / mem_total) * 100.0 } else { 0.0 };
let memory_usage = if mem_total > 0.0 {
(mem_used / mem_total) * 100.0
} else {
0.0
};
(cpu_usage, memory_usage)
})
.await
.unwrap_or((0.0, 0.0));
let active = hb_slots.load(Ordering::Relaxed);
let active = hb_slots.load(Ordering::Acquire);
let req = NodeHeartbeatRequest {
node_id: hb_node_id.clone(),
active_slots: active,
@@ -110,7 +207,22 @@ impl NodeWorker {
memory_usage,
};
let _ = hb_client.post(&hb_url).json(&req).send().await;
match hb_client.post(&hb_url).json(&req).send().await {
Ok(resp) => {
let status = resp.status();
// 401/403token 失效或被吊销。与 claim_task 口径统一:直接退出进程,
// 避免心跳线程持续发被拒请求刷日志、占用服务端限流计数。心跳通常比
// claim 更高频,往往先于 claim_task 发现吊销。
if status.as_u16() == 401 || status.as_u16() == 403 {
tracing::error!(
"节点 {} 心跳被服务端拒绝 (HTTP {})node token 已失效或被吊销。请清理 .node_token 文件后重启节点以重新发起注册审批。进程将退出,依赖编排系统重启。",
hb_node_id, status
);
std::process::exit(1);
}
}
Err(e) => warn!("节点 {} 心跳上报失败: {}", hb_node_id, e),
}
}
});
@@ -123,7 +235,7 @@ impl NodeWorker {
tokio::spawn(async move {
if tokio::signal::ctrl_c().await.is_ok() {
info!("收到 Ctrl+C 终止信号,停止领用新任务,准备优雅退出 (再次按 Ctrl+C 可强制立即退出)...");
shutdown_signal.store(true, Ordering::SeqCst);
shutdown_signal.store(true, Ordering::Release);
// 二次 Ctrl+C 强行立即退出
if tokio::signal::ctrl_c().await.is_ok() {
@@ -137,11 +249,11 @@ impl NodeWorker {
// 带有优雅退出信号响应的任务领用主循环
loop {
if shutting_down.load(Ordering::Relaxed) {
if shutting_down.load(Ordering::Acquire) {
break;
}
let active = self.active_slots.load(Ordering::Relaxed);
let active = self.active_slots.load(Ordering::Acquire);
if (active as usize) < self.config.max_slots {
match self.claim_task().await {
Ok(Some(task)) => {
@@ -149,7 +261,7 @@ impl NodeWorker {
info!("与服务端恢复网络连接,已自动重新上线并开始领用计算任务!");
was_disconnected = false;
}
self.active_slots.fetch_add(1, Ordering::SeqCst);
self.active_slots.fetch_add(1, Ordering::AcqRel);
let client = self.client.clone();
let server_url = self.config.server_url.clone();
let node_id = self.config.node_id.clone();
@@ -158,14 +270,29 @@ impl NodeWorker {
let slots_counter = self.active_slots.clone();
tokio::spawn(async move {
let res = execute_task(&client, &server_url, &runtime, &work_dir, &task)
.await
.map_err(|e| e.to_string());
let slot_work_dir = work_dir.join(format!("task_{}", task.task_id));
let res =
execute_task(&client, &server_url, &runtime, &work_dir, &task)
.await
.map_err(|e| e.to_string());
if let Err(e) = report_result(&client, &server_url, &node_id, &task, res).await {
let report_res =
report_result(&client, &server_url, &node_id, &task, res).await;
if report_res.is_ok() {
if let Err(e) =
crate::executor::cleanup_slot_work_dir(&slot_work_dir).await
{
warn!(
"清理任务 {} 的沙盒目录 {} 失败: {}",
task.task_id,
slot_work_dir.display(),
e
);
}
} else if let Err(ref e) = report_res {
warn!("向服务端上报任务 {} 计算结果失败: {}", task.task_id, e);
}
slots_counter.fetch_sub(1, Ordering::SeqCst);
slots_counter.fetch_sub(1, Ordering::AcqRel);
});
}
Ok(None) => {
@@ -187,17 +314,17 @@ impl NodeWorker {
}
// 等待在途任务完结(最多等待 30 秒)
if self.active_slots.load(Ordering::SeqCst) > 0 {
if self.active_slots.load(Ordering::Acquire) > 0 {
info!(
"正在等待 {} 个在途计算任务优雅完结 (上限 30 秒,按二次 Ctrl+C 可强行中断)...",
self.active_slots.load(Ordering::SeqCst)
self.active_slots.load(Ordering::Acquire)
);
}
let start_wait = std::time::Instant::now();
let mut last_log_time = std::time::Instant::now();
while self.active_slots.load(Ordering::SeqCst) > 0 {
while self.active_slots.load(Ordering::Acquire) > 0 {
if start_wait.elapsed().as_secs() >= 30 {
warn!("在途任务等待超时 (30s),强制退出节点");
break;
@@ -205,7 +332,7 @@ impl NodeWorker {
if last_log_time.elapsed().as_secs() >= 5 {
info!(
"仍在等待 {} 个在途计算任务完结...",
self.active_slots.load(Ordering::SeqCst)
self.active_slots.load(Ordering::Acquire)
);
last_log_time = std::time::Instant::now();
}
@@ -220,7 +347,21 @@ impl NodeWorker {
let claim_url = format!("{}/api/task/claim", self.config.server_url);
let resp = self.client.post(&claim_url).send().await?;
if !resp.status().is_success() {
let status = resp.status();
// 401/403 表明 node token 已被吊销或失效(区别于「暂无任务」与服务端 5xx 故障)。
// 服务端故障返回 5xx 会走 !is_success() 的 Ok(None) 分支,仅在网络层/鉴权层拒绝时
// 才是真正的吊销。此时继续轮询只会持续产生被拒请求并刷日志,故直接退出进程,
// 由编排系统(Docker restart / systemd / k8s)拉起;新进程发现 .node_token 失效后
// 会自动走注册审批流程重新申请。
if status.as_u16() == 401 || status.as_u16() == 403 {
tracing::error!(
"领用任务被服务端拒绝 (HTTP {}):node token 已失效或被吊销。请清理 .node_token 文件后重启节点以重新发起注册审批。进程将退出,依赖编排系统重启。",
status
);
std::process::exit(1);
}
if !status.is_success() {
return Ok(None);
}