High-performance implementation of plcbundle written in Rust
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Docker

+195 -501
+8
.dockerignore
··· 1 + target 2 + .git 3 + .github 4 + .DS_Store 5 + **/*.rs.bk 6 + **/*.swp 7 + node_modules 8 + data
+45
Dockerfile
··· 1 + FROM rust:1.90-alpine AS builder 2 + 3 + # Determine Rust target based on build platform 4 + ARG TARGETARCH 5 + ARG TARGETPLATFORM 6 + RUN case ${TARGETARCH} in \ 7 + amd64) RUST_TARGET=x86_64-unknown-linux-musl ;; \ 8 + arm64) RUST_TARGET=aarch64-unknown-linux-musl ;; \ 9 + *) echo "Unsupported architecture: ${TARGETARCH}" && exit 1 ;; \ 10 + esac && \ 11 + echo "Building for ${RUST_TARGET}" && \ 12 + rustup target add ${RUST_TARGET} 13 + 14 + RUN apk update && \ 15 + apk add --no-cache build-base musl-dev pkgconfig ca-certificates openssl-dev openssl-libs-static 16 + 17 + WORKDIR /app 18 + COPY Cargo.toml Cargo.lock ./ 19 + COPY src ./src 20 + 21 + ENV OPENSSL_STATIC=1 22 + ENV OPENSSL_DIR=/usr 23 + 24 + # Build with the determined target 25 + ARG TARGETARCH 26 + RUN case ${TARGETARCH} in \ 27 + amd64) RUST_TARGET=x86_64-unknown-linux-musl ;; \ 28 + arm64) RUST_TARGET=aarch64-unknown-linux-musl ;; \ 29 + *) echo "Unsupported architecture: ${TARGETARCH}" && exit 1 ;; \ 30 + esac && \ 31 + cargo build --release --features server --target ${RUST_TARGET} && \ 32 + cp /app/target/${RUST_TARGET}/release/plcbundle /app/plcbundle 33 + 34 + FROM alpine:3 AS runtime 35 + RUN adduser -D -u 10001 appuser && \ 36 + apk add --no-cache ca-certificates tzdata && \ 37 + update-ca-certificates 38 + 39 + WORKDIR /data 40 + 41 + # Copy the binary from the fixed location in builder stage 42 + COPY --from=builder --chmod=755 /app/plcbundle /usr/local/bin/plcbundle 43 + USER appuser 44 + EXPOSE 8080 45 + ENTRYPOINT ["plcbundle"]
+54 -3
Makefile
··· 1 - .PHONY: build install release clean test run help version patch minor major alpha beta rc 1 + .PHONY: build install release clean test run help version patch minor major alpha beta rc docker-build docker-run docker-compose-up docker-compose-down docker-compose-logs docker-push docker-tag 2 2 3 3 # Default target 4 4 help: 5 - @echo "bundleq Makefile" 5 + @echo "plcbundle Makefile" 6 6 @echo "" 7 7 @echo "Available targets:" 8 8 @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-12s\033[0m %s\n", $$1, $$2}' ··· 20 20 release: ## Build optimized release binary 21 21 cargo build --release 22 22 @echo "" 23 - @echo "Release binary: target/release/bundleq" 23 + @echo "Release binary: target/release/plcbundle" 24 24 25 25 install: ## Install to ~/.cargo/bin (with all features) 26 26 cargo install --all-features --path . ··· 73 73 74 74 rc: ## Bump release candidate version (0.9.0-rc.0 -> 0.9.0-rc.1) 75 75 cargo release rc --execute 76 + 77 + # ----------------------------------------------------------------------------- 78 + # Docker targets 79 + # ----------------------------------------------------------------------------- 80 + 81 + IMAGE ?= atscan/plcbundle:latest 82 + HTTP_PORT ?= 8080 83 + DATA_DIR ?= ./data 84 + TZ ?= UTC 85 + 86 + docker-build: ## Build Docker image (Alpine musl) for current platform 87 + docker build -t $(IMAGE) . 88 + 89 + docker-build-amd64: ## Build Docker image for linux/amd64 90 + docker buildx build --platform linux/amd64 -t $(IMAGE) --load . 91 + 92 + docker-build-arm64: ## Build Docker image for linux/arm64 93 + docker buildx build --platform linux/arm64 -t $(IMAGE) --load . 94 + 95 + docker-build-multi: ## Build multi-arch Docker image (amd64 + arm64) 96 + @echo "Building multi-architecture image for linux/amd64,linux/arm64..." 97 + docker buildx build --platform linux/amd64,linux/arm64 -t $(IMAGE) --push . 98 + 99 + docker-build-multi-local: ## Build multi-arch image and load both architectures locally 100 + @echo "Building multi-architecture image for linux/amd64,linux/arm64 (local)..." 101 + docker buildx build --platform linux/amd64,linux/arm64 -t $(IMAGE) . 102 + 103 + docker-run: ## Run container locally with sync and websocket 104 + mkdir -p $(DATA_DIR) 105 + docker run --rm \ 106 + --name plcbundle \ 107 + -p $(HTTP_PORT):8080 \ 108 + -v $(PWD)/$(DATA_DIR):/data \ 109 + -e TZ=$(TZ) \ 110 + $(IMAGE) server --host 0.0.0.0 --port 8080 --sync --websocket -C /data 111 + 112 + docker-compose-up: ## Start via docker compose (detached) 113 + docker compose up -d 114 + 115 + docker-compose-down: ## Stop compose services 116 + docker compose down 117 + 118 + docker-compose-logs: ## Tail compose logs 119 + docker compose logs -f --tail=100 120 + 121 + docker-tag: ## Tag local image (set TAG=...) 122 + @if [ -z "$(TAG)" ]; then echo "TAG is required (e.g., make docker-tag TAG=v0.9.0)"; exit 1; fi 123 + docker tag $(IMAGE) $(IMAGE:latest=$(TAG)) 124 + 125 + docker-push: ## Push image to registry 126 + docker push $(IMAGE)
+39
docker-compose.yml
··· 1 + networks: 2 + plcbundle: 3 + 4 + services: 5 + plcbundle: 6 + build: . 7 + image: atscan/plcbundle:latest 8 + container_name: plcbundle 9 + restart: unless-stopped 10 + ports: 11 + - "${HTTP_PORT:-8080}:8080" 12 + volumes: 13 + - ${DATA_DIR:-./data}:/data 14 + environment: 15 + - TZ=${TZ:-UTC} 16 + command: [ 17 + "server", 18 + "-C","/data", 19 + "--host","0.0.0.0", 20 + "--port","8080", 21 + "--sync", 22 + "--websocket", 23 + "--resolver" 24 + ] 25 + networks: 26 + - plcbundle 27 + 28 + watchtower: 29 + container_name: watchtower 30 + image: containrrr/watchtower:latest 31 + network_mode: host 32 + volumes: 33 + - type: bind 34 + source: /var/run/docker.sock 35 + target: /var/run/docker.sock 36 + restart: unless-stopped 37 + environment: 38 + WATCHTOWER_CLEANUP: "true" 39 + WATCHTOWER_SCHEDULE: "@midnight"
+48 -72
src/cli/cmd_verify.rs
··· 6 6 use std::path::PathBuf; 7 7 use std::time::Instant; 8 8 9 + /// Print failed bundles with their error messages 10 + /// 11 + /// # Arguments 12 + /// * `failed_bundles` - Vector of (bundle_num, errors) tuples 13 + /// * `threshold` - Maximum number of bundles to list in full before truncating (e.g., 10 or 20) 14 + fn print_failed_bundles(failed_bundles: &[(u32, Vec<String>)], threshold: usize) { 15 + if failed_bundles.is_empty() { 16 + return; 17 + } 18 + 19 + if failed_bundles.len() <= threshold { 20 + eprintln!("\n ⚠️ Failed bundles:"); 21 + for (bundle_num, errors) in failed_bundles { 22 + eprintln!(" Bundle {}:", bundle_num); 23 + if errors.is_empty() { 24 + eprintln!(" • Verification failed (no error details)"); 25 + } else { 26 + for err in errors { 27 + eprintln!(" • {}", err); 28 + } 29 + } 30 + } 31 + } else { 32 + eprintln!( 33 + " ⚠️ Failed bundles: {} (too many to list)", 34 + failed_bundles.len() 35 + ); 36 + // Show first few with details 37 + eprintln!("\n First few failures:"); 38 + for (bundle_num, errors) in failed_bundles.iter().take(5) { 39 + eprintln!(" Bundle {}:", bundle_num); 40 + if errors.is_empty() { 41 + eprintln!(" • Verification failed (no error details)"); 42 + } else { 43 + for err in errors.iter().take(3) { 44 + eprintln!(" • {}", err); 45 + } 46 + if errors.len() > 3 { 47 + eprintln!(" • ... and {} more error(s)", errors.len() - 3); 48 + } 49 + } 50 + } 51 + eprintln!(" ... and {} more failed bundles", failed_bundles.len() - 5); 52 + } 53 + } 54 + 9 55 #[derive(Args)] 10 56 #[command( 11 57 about = "Verify bundle integrity and chain", ··· 547 593 eprintln!(" Errors: {}", error_count); 548 594 549 595 // Show failed bundles with their error messages 550 - if !failed_bundles.is_empty() { 551 - if failed_bundles.len() <= 10 { 552 - eprintln!("\n ⚠️ Failed bundles:"); 553 - for (bundle_num, errors) in &failed_bundles { 554 - eprintln!(" Bundle {}:", bundle_num); 555 - if errors.is_empty() { 556 - eprintln!(" • Verification failed (no error details)"); 557 - } else { 558 - for err in errors { 559 - eprintln!(" • {}", err); 560 - } 561 - } 562 - } 563 - } else { 564 - eprintln!( 565 - " ⚠️ Failed bundles: {} (too many to list)", 566 - failed_bundles.len() 567 - ); 568 - // Show first few with details 569 - eprintln!("\n First few failures:"); 570 - for (bundle_num, errors) in failed_bundles.iter().take(5) { 571 - eprintln!(" Bundle {}:", bundle_num); 572 - if errors.is_empty() { 573 - eprintln!(" • Verification failed (no error details)"); 574 - } else { 575 - for err in errors.iter().take(3) { 576 - eprintln!(" • {}", err); 577 - } 578 - if errors.len() > 3 { 579 - eprintln!(" • ... and {} more error(s)", errors.len() - 3); 580 - } 581 - } 582 - } 583 - eprintln!(" ... and {} more failed bundles", failed_bundles.len() - 5); 584 - } 585 - } 596 + print_failed_bundles(&failed_bundles, 10); 586 597 587 598 eprintln!(" Time: {:?}", elapsed); 588 599 ··· 771 782 eprintln!(" Verified: {}/{}", verified, total); 772 783 eprintln!(" Failed: {}", failed); 773 784 774 - if !failed_bundles.is_empty() { 775 - if failed_bundles.len() <= 20 { 776 - eprintln!("\n ⚠️ Failed bundles:"); 777 - for (bundle_num, errors) in &failed_bundles { 778 - eprintln!(" Bundle {}:", bundle_num); 779 - if errors.is_empty() { 780 - eprintln!(" • Verification failed (no error details)"); 781 - } else { 782 - for err in errors { 783 - eprintln!(" • {}", err); 784 - } 785 - } 786 - } 787 - } else { 788 - eprintln!( 789 - "\n ⚠️ Failed bundles: {} (too many to list)", 790 - failed_bundles.len() 791 - ); 792 - // Show first few with details 793 - eprintln!("\n First few failures:"); 794 - for (bundle_num, errors) in failed_bundles.iter().take(5) { 795 - eprintln!(" Bundle {}:", bundle_num); 796 - if errors.is_empty() { 797 - eprintln!(" • Verification failed (no error details)"); 798 - } else { 799 - for err in errors.iter().take(3) { 800 - eprintln!(" • {}", err); 801 - } 802 - if errors.len() > 3 { 803 - eprintln!(" • ... and {} more error(s)", errors.len() - 3); 804 - } 805 - } 806 - } 807 - eprintln!(" ... and {} more failed bundles", failed_bundles.len() - 5); 808 - } 809 - } 785 + print_failed_bundles(&failed_bundles, 20); 810 786 811 787 bail!("verification failed for {} bundles", failed) 812 788 }
-4
src/cli/utils.rs
··· 18 18 19 19 /// Dim/bright black color (used for context, unchanged lines, etc.) 20 20 pub const DIM: &str = "\x1b[2m"; 21 - 22 - /// Bold text 23 - #[allow(dead_code)] 24 - pub const BOLD: &str = "\x1b[1m"; 25 21 } 26 22 27 23 #[cfg(feature = "cli")]
+1 -1
src/index.rs
··· 298 298 299 299 // Extract embedded metadata from bundle file 300 300 let embedded = crate::bundle_format::extract_metadata_from_file(bundle_path) 301 - .with_context(|| format!("Failed to extract metadata from bundle {:06}", bundle_num))?; 301 + .with_context(|| format!("Failed to extract metadata from bundle {}", bundle_num))?; 302 302 303 303 // Auto-detect origin from first bundle if not provided 304 304 {
-170
src/server/handlers/bundle.rs
··· 1 - // Bundle-related handlers 2 - 3 - use crate::constants; 4 - use crate::server::error::{bad_request, internal_error, is_not_found_error, not_found, task_join_error}; 5 - use crate::server::handlers::ServerState; 6 - use crate::server::utils::{bundle_download_headers, parse_operation_pointer}; 7 - use axum::{ 8 - body::Body, 9 - extract::{Path, State}, 10 - http::{HeaderMap, HeaderValue, StatusCode}, 11 - response::IntoResponse, 12 - }; 13 - use std::sync::Arc; 14 - use std::time::Instant; 15 - use tokio_util::io::ReaderStream; 16 - 17 - pub async fn handle_bundle( 18 - State(state): State<ServerState>, 19 - Path(number): Path<u32>, 20 - ) -> impl IntoResponse { 21 - match state.manager.get_bundle_metadata(number) { 22 - Ok(Some(meta)) => (StatusCode::OK, axum::Json(meta)).into_response(), 23 - Ok(None) => not_found("Bundle not found").into_response(), 24 - Err(e) => internal_error(&e.to_string()).into_response(), 25 - } 26 - } 27 - 28 - pub async fn handle_bundle_data( 29 - State(state): State<ServerState>, 30 - Path(number): Path<u32>, 31 - ) -> impl IntoResponse { 32 - // Use BundleManager API to get bundle file stream 33 - let file_result = tokio::task::spawn_blocking({ 34 - let manager = Arc::clone(&state.manager); 35 - move || manager.stream_bundle_raw(number) 36 - }) 37 - .await; 38 - 39 - match file_result { 40 - Ok(Ok(std_file)) => { 41 - // Convert std::fs::File to tokio::fs::File 42 - let file = tokio::fs::File::from_std(std_file); 43 - let stream = ReaderStream::new(file); 44 - let body = Body::from_stream(stream); 45 - 46 - let headers = bundle_download_headers( 47 - "application/zstd", 48 - &constants::bundle_filename(number), 49 - ); 50 - 51 - (StatusCode::OK, headers, body).into_response() 52 - } 53 - Ok(Err(e)) => { 54 - // Handle errors from BundleManager 55 - if is_not_found_error(&e) { 56 - not_found("Bundle not found").into_response() 57 - } else { 58 - internal_error(&e.to_string()).into_response() 59 - } 60 - } 61 - Err(e) => task_join_error(e).into_response(), 62 - } 63 - } 64 - 65 - pub async fn handle_bundle_jsonl( 66 - State(state): State<ServerState>, 67 - Path(number): Path<u32>, 68 - ) -> impl IntoResponse { 69 - // For streaming decompressed data, read in spawn_blocking and stream chunks 70 - // TODO: Implement true async streaming when tokio-util supports it better 71 - match tokio::task::spawn_blocking({ 72 - let manager = Arc::clone(&state.manager); 73 - move || { 74 - let mut reader = manager.stream_bundle_decompressed(number)?; 75 - use std::io::Read; 76 - let mut buf = Vec::new(); 77 - reader.read_to_end(&mut buf)?; 78 - Ok::<Vec<u8>, anyhow::Error>(buf) 79 - } 80 - }) 81 - .await 82 - { 83 - Ok(Ok(data)) => { 84 - let filename = constants::bundle_filename(number).replace(".zst", ""); 85 - let headers = bundle_download_headers("application/x-ndjson", &filename); 86 - 87 - (StatusCode::OK, headers, data).into_response() 88 - } 89 - Ok(Err(e)) => { 90 - if is_not_found_error(&e) { 91 - not_found("Bundle not found").into_response() 92 - } else { 93 - internal_error(&e.to_string()).into_response() 94 - } 95 - } 96 - Err(_) => internal_error("Task join error").into_response(), 97 - } 98 - } 99 - 100 - pub async fn handle_operation( 101 - State(state): State<ServerState>, 102 - Path(pointer): Path<String>, 103 - ) -> impl IntoResponse { 104 - // Parse pointer: "bundle:position" or global position 105 - let (bundle_num, position) = match parse_operation_pointer(&pointer) { 106 - Ok((b, p)) => (b, p), 107 - Err(e) => return bad_request(&e.to_string()).into_response(), 108 - }; 109 - 110 - if position >= constants::BUNDLE_SIZE { 111 - return bad_request("Position must be 0-9999").into_response(); 112 - } 113 - 114 - let total_start = Instant::now(); 115 - let load_start = Instant::now(); 116 - 117 - // get_operation_raw performs blocking file I/O, so we need to use spawn_blocking 118 - let json_result = tokio::task::spawn_blocking({ 119 - let manager = Arc::clone(&state.manager); 120 - move || manager.get_operation_raw(bundle_num, position) 121 - }) 122 - .await; 123 - 124 - match json_result { 125 - Ok(Ok(json)) => { 126 - let load_duration = load_start.elapsed(); 127 - let total_duration = total_start.elapsed(); 128 - 129 - let global_pos = (bundle_num as u64 * constants::BUNDLE_SIZE as u64) + position as u64; 130 - 131 - let mut headers = HeaderMap::new(); 132 - headers.insert("X-Bundle-Number", HeaderValue::from(bundle_num)); 133 - headers.insert("X-Position", HeaderValue::from(position)); 134 - headers.insert( 135 - "X-Global-Position", 136 - HeaderValue::from_str(&global_pos.to_string()).unwrap(), 137 - ); 138 - headers.insert( 139 - "X-Pointer", 140 - HeaderValue::from_str(&format!("{}:{}", bundle_num, position)).unwrap(), 141 - ); 142 - headers.insert( 143 - "X-Load-Time-Ms", 144 - HeaderValue::from_str(&format!("{:.3}", load_duration.as_secs_f64() * 1000.0)) 145 - .unwrap(), 146 - ); 147 - headers.insert( 148 - "X-Total-Time-Ms", 149 - HeaderValue::from_str(&format!("{:.3}", total_duration.as_secs_f64() * 1000.0)) 150 - .unwrap(), 151 - ); 152 - headers.insert( 153 - "Cache-Control", 154 - HeaderValue::from_static("public, max-age=31536000, immutable"), 155 - ); 156 - headers.insert("Content-Type", HeaderValue::from_static("application/json")); 157 - 158 - (StatusCode::OK, headers, json).into_response() 159 - } 160 - Ok(Err(e)) => { 161 - if is_not_found_error(&e) { 162 - not_found("Operation not found").into_response() 163 - } else { 164 - internal_error(&e.to_string()).into_response() 165 - } 166 - } 167 - Err(e) => task_join_error(e).into_response(), 168 - } 169 - } 170 -
-63
src/server/handlers/debug.rs
··· 1 - // Debug handlers 2 - 3 - use crate::server::error::not_found; 4 - use crate::server::handlers::ServerState; 5 - use axum::{ 6 - extract::State, 7 - http::{HeaderMap, HeaderValue, StatusCode}, 8 - response::IntoResponse, 9 - }; 10 - use serde_json::json; 11 - use std::sync::Arc; 12 - 13 - pub async fn handle_debug_memory(State(state): State<ServerState>) -> impl IntoResponse { 14 - // Get DID index stats for memory info (avoid holding lock in async context) 15 - let did_stats = tokio::task::spawn_blocking({ 16 - let manager = Arc::clone(&state.manager); 17 - move || { 18 - manager.get_did_index_stats() 19 - } 20 - }) 21 - .await 22 - .unwrap_or_default(); 23 - 24 - let cached_shards = did_stats 25 - .get("cached_shards") 26 - .and_then(|v| v.as_i64()) 27 - .unwrap_or(0); 28 - let cache_limit = did_stats 29 - .get("cache_limit") 30 - .and_then(|v| v.as_i64()) 31 - .unwrap_or(0); 32 - 33 - let response = format!( 34 - "Memory Stats:\n (Not fully implemented - using sysinfo would require additional dependency)\n\nDID Index:\n Cached shards: {}/{}\n", 35 - cached_shards, cache_limit 36 - ); 37 - let mut headers = HeaderMap::new(); 38 - headers.insert("Content-Type", HeaderValue::from_static("text/plain")); 39 - (StatusCode::OK, headers, response) 40 - } 41 - 42 - pub async fn handle_debug_didindex(State(state): State<ServerState>) -> impl IntoResponse { 43 - // Avoid holding lock in async context 44 - let stats = tokio::task::spawn_blocking({ 45 - let manager = Arc::clone(&state.manager); 46 - move || { 47 - manager.get_did_index_stats() 48 - } 49 - }) 50 - .await 51 - .unwrap_or_default(); 52 - (StatusCode::OK, axum::Json(json!(stats))).into_response() 53 - } 54 - 55 - pub async fn handle_debug_resolver(State(state): State<ServerState>) -> impl IntoResponse { 56 - if !state.config.enable_resolver { 57 - return not_found("Resolver not enabled").into_response(); 58 - } 59 - 60 - let resolver_stats = state.manager.get_resolver_stats(); 61 - (StatusCode::OK, axum::Json(json!(resolver_stats))).into_response() 62 - } 63 -
-40
src/server/handlers/mod.rs
··· 1 - // HTTP handlers module 2 - 3 - #[cfg(feature = "server")] 4 - mod handle_bundle; 5 - #[cfg(feature = "server")] 6 - mod handle_debug; 7 - #[cfg(feature = "server")] 8 - mod handle_did; 9 - #[cfg(feature = "server")] 10 - mod handle_root; 11 - #[cfg(feature = "server")] 12 - mod handle_status; 13 - 14 - #[cfg(feature = "server")] 15 - use crate::server::config::ServerConfig; 16 - #[cfg(feature = "server")] 17 - use crate::manager::BundleManager; 18 - #[cfg(feature = "server")] 19 - use std::sync::Arc; 20 - #[cfg(feature = "server")] 21 - use std::time::Instant; 22 - 23 - #[derive(Clone)] 24 - pub struct ServerState { 25 - pub manager: Arc<BundleManager>, 26 - pub config: ServerConfig, 27 - pub start_time: Instant, 28 - } 29 - 30 - #[cfg(feature = "server")] 31 - pub use handle_bundle::*; 32 - #[cfg(feature = "server")] 33 - pub use handle_debug::*; 34 - #[cfg(feature = "server")] 35 - pub use handle_did::*; 36 - #[cfg(feature = "server")] 37 - pub use handle_root::*; 38 - #[cfg(feature = "server")] 39 - pub use handle_status::*; 40 -
-148
src/server/handlers/status.rs
··· 1 - // Status, mempool, and index handlers 2 - 3 - use crate::constants; 4 - use crate::server::error::{internal_error, not_found}; 5 - use crate::server::handlers::ServerState; 6 - use axum::{ 7 - extract::State, 8 - http::{HeaderMap, HeaderValue, StatusCode}, 9 - response::IntoResponse, 10 - }; 11 - use serde_json::json; 12 - use std::sync::Arc; 13 - 14 - pub async fn handle_index_json(State(state): State<ServerState>) -> impl IntoResponse { 15 - let index = state.manager.get_index(); 16 - (StatusCode::OK, axum::Json(index)).into_response() 17 - } 18 - 19 - pub async fn handle_status(State(state): State<ServerState>) -> impl IntoResponse { 20 - let index = state.manager.get_index(); 21 - let start_time = state.start_time; 22 - let uptime = start_time.elapsed().as_secs(); 23 - let origin = state.manager.get_plc_origin(); 24 - 25 - let mut response = json!({ 26 - "server": { 27 - "version": state.config.version, 28 - "uptime_seconds": uptime, 29 - "sync_mode": state.config.sync_mode, 30 - "sync_interval_seconds": if state.config.sync_mode { Some(state.config.sync_interval_seconds) } else { None }, 31 - "websocket_enabled": state.config.enable_websocket, 32 - "resolver_enabled": state.config.enable_resolver, 33 - "origin": origin, 34 - }, 35 - "bundles": { 36 - "count": index.bundles.len(), 37 - "total_size": index.total_size_bytes, 38 - "uncompressed_size": index.total_uncompressed_size_bytes, 39 - "updated_at": index.updated_at, 40 - } 41 - }); 42 - 43 - if let Some(handle_resolver) = state.manager.get_handle_resolver_base_url() { 44 - response["server"]["handle_resolver"] = json!(handle_resolver); 45 - } 46 - 47 - if !index.bundles.is_empty() { 48 - let first_bundle = index.bundles.first().unwrap().bundle_number; 49 - let last_bundle = index.last_bundle; 50 - let first_meta = index.get_bundle(first_bundle); 51 - let last_meta = index.get_bundle(last_bundle); 52 - 53 - response["bundles"]["first_bundle"] = json!(first_bundle); 54 - response["bundles"]["last_bundle"] = json!(last_bundle); 55 - 56 - if let Some(meta) = first_meta { 57 - response["bundles"]["root_hash"] = json!(meta.hash); 58 - response["bundles"]["start_time"] = json!(meta.start_time); 59 - } 60 - if let Some(meta) = last_meta { 61 - response["bundles"]["head_hash"] = json!(meta.hash); 62 - response["bundles"]["end_time"] = json!(meta.end_time); 63 - } 64 - 65 - let total_ops = index.bundles.len() * constants::BUNDLE_SIZE; 66 - response["bundles"]["total_operations"] = json!(total_ops); 67 - } 68 - 69 - if state.config.sync_mode { 70 - if let Ok(mempool_stats) = state.manager.get_mempool_stats() { 71 - response["mempool"] = json!({ 72 - "count": mempool_stats.count, 73 - "target_bundle": mempool_stats.target_bundle, 74 - "can_create_bundle": mempool_stats.count >= constants::BUNDLE_SIZE, 75 - "progress_percent": (mempool_stats.count as f64 / constants::BUNDLE_SIZE as f64) * 100.0, 76 - "bundle_size": constants::BUNDLE_SIZE, 77 - "operations_needed": constants::BUNDLE_SIZE - mempool_stats.count, 78 - }); 79 - } 80 - } 81 - 82 - // DID Index stats (get_stats is fast, but we should still avoid holding lock in async context) 83 - let did_stats = tokio::task::spawn_blocking({ 84 - let manager = Arc::clone(&state.manager); 85 - move || { 86 - manager.get_did_index_stats() 87 - } 88 - }) 89 - .await 90 - .unwrap_or_default(); 91 - if did_stats 92 - .get("exists") 93 - .and_then(|v| v.as_bool()) 94 - .unwrap_or(false) 95 - { 96 - let did_index_status = json!({ 97 - "enabled": state.config.enable_resolver, 98 - "exists": true, 99 - "total_dids": did_stats.get("total_dids").and_then(|v| v.as_i64()).unwrap_or(0), 100 - "last_bundle": did_stats.get("last_bundle").and_then(|v| v.as_i64()).unwrap_or(0), 101 - "shard_count": did_stats.get("shard_count").and_then(|v| v.as_i64()).unwrap_or(0), 102 - "cached_shards": did_stats.get("cached_shards").and_then(|v| v.as_i64()).unwrap_or(0), 103 - "cache_limit": did_stats.get("cache_limit").and_then(|v| v.as_i64()).unwrap_or(0), 104 - "cache_hit_rate": did_stats.get("cache_hit_rate").and_then(|v| v.as_f64()).unwrap_or(0.0), 105 - "cache_hits": did_stats.get("cache_hits").and_then(|v| v.as_i64()).unwrap_or(0), 106 - "cache_misses": did_stats.get("cache_misses").and_then(|v| v.as_i64()).unwrap_or(0), 107 - "total_lookups": did_stats.get("total_lookups").and_then(|v| v.as_i64()).unwrap_or(0), 108 - }); 109 - response["didindex"] = did_index_status; 110 - } 111 - 112 - // Resolver stats 113 - if state.config.enable_resolver { 114 - let resolver_stats = state.manager.get_resolver_stats(); 115 - if !resolver_stats.is_empty() { 116 - response["resolver"] = json!(resolver_stats); 117 - } 118 - } 119 - 120 - (StatusCode::OK, axum::Json(response)).into_response() 121 - } 122 - 123 - pub async fn handle_mempool(State(state): State<ServerState>) -> impl IntoResponse { 124 - if !state.config.sync_mode { 125 - return not_found("Mempool only available in sync mode").into_response(); 126 - } 127 - 128 - match state.manager.get_mempool_operations() { 129 - Ok(ops) => { 130 - // Convert operations to JSONL 131 - let mut jsonl = Vec::new(); 132 - for op in ops { 133 - if let Ok(json) = sonic_rs::to_string(&op) { 134 - jsonl.extend_from_slice(json.as_bytes()); 135 - jsonl.push(b'\n'); 136 - } 137 - } 138 - let mut headers = HeaderMap::new(); 139 - headers.insert( 140 - "Content-Type", 141 - HeaderValue::from_static("application/x-ndjson"), 142 - ); 143 - (StatusCode::OK, headers, jsonl).into_response() 144 - } 145 - Err(e) => internal_error(&e.to_string()).into_response(), 146 - } 147 - } 148 -