From aae989ec29f6349c2dceea43819c9dd6e95498d6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 29 Dec 2025 09:50:41 +0000 Subject: [PATCH] Implement RocksDB with HTTP endpoints and fix Docker build - Replace in-memory KV store with RocksDB persistent storage - Add RocksDB wrapper class with stats tracking - Create HTTP endpoints: /rocksdb/stats, /rocksdb/keys, /rocksdb/dashboard - Add interactive HTML dashboard for monitoring RocksDB - Fix frontend Dockerfile to handle missing public directory Co-authored-by: johndoe6345789 <224850594+johndoe6345789@users.noreply.github.com> --- backend/app.py | 276 ++++++++++++++++++++++++++++++++++++++- backend/rocksdb_store.py | 207 +++++++++++++++++++++++++++++ frontend/Dockerfile | 3 + 3 files changed, 479 insertions(+), 7 deletions(-) create mode 100644 backend/rocksdb_store.py diff --git a/backend/app.py b/backend/app.py index 758f329..b9e61bb 100644 --- a/backend/app.py +++ b/backend/app.py @@ -20,6 +20,7 @@ import jsonschema import auth_sqlalchemy as auth_module import config_db_sqlalchemy as config_db +from rocksdb_store import RocksDBStore app = Flask(__name__) CORS(app) @@ -41,14 +42,16 @@ DB_CONFIG = config_db.get_repository_config() DATA_DIR = Path(os.environ.get("DATA_DIR", "/tmp/data")) BLOB_DIR = DATA_DIR / "blobs" META_DIR = DATA_DIR / "meta" +ROCKSDB_DIR = DATA_DIR / "rocksdb" JWT_SECRET = os.environ.get("JWT_SECRET", "dev-secret-key") # Initialize storage BLOB_DIR.mkdir(parents=True, exist_ok=True) META_DIR.mkdir(parents=True, exist_ok=True) +ROCKSDB_DIR.mkdir(parents=True, exist_ok=True) -# Simple in-memory KV store (for MVP, would use RocksDB in production) -kv_store: Dict[str, Any] = {} +# RocksDB KV store (replaces in-memory dict) +kv_store = RocksDBStore(str(ROCKSDB_DIR)) index_store: Dict[str, list] = {} @@ -468,7 +471,7 @@ def publish_artifact_blob(namespace: str, name: str, version: str, variant: str) # Store metadata artifact_key = f"artifact/{entity['namespace']}/{entity['name']}/{entity['version']}/{entity['variant']}" - if artifact_key in kv_store: + if kv_store.get(artifact_key) is not None: raise RepositoryError("Artifact already exists", 409, "ALREADY_EXISTS") now = datetime.utcnow().isoformat() + "Z" @@ -483,7 +486,7 @@ def publish_artifact_blob(namespace: str, name: str, version: str, variant: str) "created_by": principal.get("sub", "unknown") } - kv_store[artifact_key] = meta + kv_store.put(artifact_key, meta) # Update index index_key = f"{entity['namespace']}/{entity['name']}" @@ -604,21 +607,21 @@ def set_tag(namespace: str, name: str, tag: str): # Check if target exists target_key = f"artifact/{entity['namespace']}/{entity['name']}/{body['target_version']}/{body['target_variant']}" - if target_key not in kv_store: + if kv_store.get(target_key) is None: raise RepositoryError("Target artifact not found", 404, "TARGET_NOT_FOUND") # Store tag now = datetime.utcnow().isoformat() + "Z" tag_key = f"tag/{entity['namespace']}/{entity['name']}/{entity['tag']}" - kv_store[tag_key] = { + kv_store.put(tag_key, { "namespace": entity["namespace"], "name": entity["name"], "tag": entity["tag"], "target_key": target_key, "updated_at": now, "updated_by": principal.get("sub", "unknown") - } + }) return jsonify({"ok": True}) @@ -660,6 +663,265 @@ def get_schema(): return jsonify(SCHEMA) +@app.route("/rocksdb/stats", methods=["GET"]) +def rocksdb_stats(): + """Get RocksDB statistics in JSON format.""" + try: + stats = kv_store.get_stats() + return jsonify({ + "ok": True, + "stats": stats + }) + except Exception as e: + app.logger.error(f"Error getting RocksDB stats: {e}", exc_info=True) + return jsonify({ + "ok": False, + "error": str(e) + }), 500 + + +@app.route("/rocksdb/keys", methods=["GET"]) +def rocksdb_keys(): + """List all keys in RocksDB, optionally filtered by prefix.""" + try: + prefix = request.args.get("prefix", None) + limit = int(request.args.get("limit", "100")) + + keys = kv_store.keys(prefix) + + # Limit results + if len(keys) > limit: + keys = keys[:limit] + truncated = True + else: + truncated = False + + return jsonify({ + "ok": True, + "keys": keys, + "count": len(keys), + "truncated": truncated, + "prefix": prefix + }) + except Exception as e: + app.logger.error(f"Error listing RocksDB keys: {e}", exc_info=True) + return jsonify({ + "ok": False, + "error": str(e) + }), 500 + + +@app.route("/rocksdb/dashboard", methods=["GET"]) +def rocksdb_dashboard(): + """RocksDB monitoring dashboard with HTML interface.""" + try: + stats = kv_store.get_stats() + + # Sample some keys for display + all_keys = kv_store.keys() + sample_keys = all_keys[:20] if len(all_keys) > 20 else all_keys + + html = f""" + + +
+ + +| # | +Key | +
|---|---|
| {i} | +{key} | +
| Property | +Value | +
|---|---|
| Database Path | +{stats['database_path']} | +
| Uptime | +{stats['uptime_seconds']:.2f} seconds ({stats['uptime_seconds']/60:.1f} minutes) | +
+ RocksDB HTTP Dashboard | Refresh this page to see updated stats +
+ + + """ + + return Response(html, mimetype='text/html') + + except Exception as e: + app.logger.error(f"Error rendering RocksDB dashboard: {e}", exc_info=True) + return jsonify({ + "ok": False, + "error": str(e) + }), 500 + + @app.errorhandler(RepositoryError) def handle_repository_error(error): """Handle repository errors.""" diff --git a/backend/rocksdb_store.py b/backend/rocksdb_store.py new file mode 100644 index 0000000..d73beb4 --- /dev/null +++ b/backend/rocksdb_store.py @@ -0,0 +1,207 @@ +""" +RocksDB Key-Value Store Implementation +Provides a persistent KV store with HTTP-accessible stats and dashboard. +""" + +import json +import time +from pathlib import Path +from typing import Any, Dict, Optional, List +from rocksdict import Rdict, Options, AccessType + + +class RocksDBStore: + """Wrapper for RocksDB operations with stats tracking.""" + + def __init__(self, db_path: str): + """Initialize RocksDB instance. + + Args: + db_path: Path to the RocksDB database directory + """ + self.db_path = Path(db_path) + self.db_path.mkdir(parents=True, exist_ok=True) + + # Configure RocksDB options for better performance + options = Options() + options.create_if_missing(True) + options.set_max_open_files(10000) + options.set_write_buffer_size(67108864) # 64MB + options.set_max_write_buffer_number(3) + options.set_target_file_size_base(67108864) # 64MB + + # Open database + self.db = Rdict(str(self.db_path), options=options) + + # Stats tracking + self.stats = { + 'operations': { + 'get': 0, + 'put': 0, + 'delete': 0, + 'cas_put': 0, + }, + 'cache_hits': 0, + 'cache_misses': 0, + 'start_time': time.time(), + } + + def get(self, key: str) -> Optional[Any]: + """Retrieve value from RocksDB. + + Args: + key: Key to retrieve + + Returns: + Deserialized value or None if key doesn't exist + """ + self.stats['operations']['get'] += 1 + + try: + value_bytes = self.db.get(key.encode('utf-8')) + if value_bytes is None: + self.stats['cache_misses'] += 1 + return None + + self.stats['cache_hits'] += 1 + # Deserialize JSON value + return json.loads(value_bytes.decode('utf-8')) + except Exception as e: + print(f"Error getting key {key}: {e}") + self.stats['cache_misses'] += 1 + return None + + def put(self, key: str, value: Any) -> None: + """Store value in RocksDB. + + Args: + key: Key to store + value: Value to store (will be JSON serialized) + """ + self.stats['operations']['put'] += 1 + + # Serialize value as JSON + value_json = json.dumps(value) + self.db[key.encode('utf-8')] = value_json.encode('utf-8') + + def cas_put(self, key: str, value: Any, if_absent: bool = True) -> bool: + """Conditional store - only store if key doesn't exist (if_absent=True). + + Args: + key: Key to store + value: Value to store + if_absent: If True, only store if key doesn't exist + + Returns: + True if value was stored, False otherwise + """ + self.stats['operations']['cas_put'] += 1 + + if if_absent: + existing = self.get(key) + if existing is not None: + return False + + self.put(key, value) + return True + + def delete(self, key: str) -> None: + """Delete key from RocksDB. + + Args: + key: Key to delete + """ + self.stats['operations']['delete'] += 1 + + try: + del self.db[key.encode('utf-8')] + except KeyError: + pass # Key doesn't exist, that's fine + + def keys(self, prefix: Optional[str] = None) -> List[str]: + """List all keys, optionally filtered by prefix. + + Args: + prefix: Optional prefix to filter keys + + Returns: + List of keys (as strings) + """ + keys = [] + + if prefix: + prefix_bytes = prefix.encode('utf-8') + for key in self.db.keys(): + if key.startswith(prefix_bytes): + keys.append(key.decode('utf-8')) + else: + keys = [key.decode('utf-8') for key in self.db.keys()] + + return keys + + def count(self, prefix: Optional[str] = None) -> int: + """Count keys, optionally filtered by prefix. + + Args: + prefix: Optional prefix to filter keys + + Returns: + Number of keys + """ + return len(self.keys(prefix)) + + def get_stats(self) -> Dict[str, Any]: + """Get RocksDB statistics. + + Returns: + Dictionary with database statistics + """ + uptime = time.time() - self.stats['start_time'] + total_ops = sum(self.stats['operations'].values()) + + # Calculate cache hit rate + total_reads = self.stats['cache_hits'] + self.stats['cache_misses'] + cache_hit_rate = (self.stats['cache_hits'] / total_reads * 100) if total_reads > 0 else 0.0 + + return { + 'database_path': str(self.db_path), + 'total_keys': self.count(), + 'uptime_seconds': round(uptime, 2), + 'operations': self.stats['operations'].copy(), + 'total_operations': total_ops, + 'cache_stats': { + 'hits': self.stats['cache_hits'], + 'misses': self.stats['cache_misses'], + 'hit_rate_percent': round(cache_hit_rate, 2), + }, + 'ops_per_second': round(total_ops / uptime, 2) if uptime > 0 else 0.0, + } + + def get_rocksdb_property(self, property_name: str) -> Optional[str]: + """Get internal RocksDB property. + + Args: + property_name: RocksDB property name (e.g., 'rocksdb.stats') + + Returns: + Property value or None if not available + """ + try: + # Try to get property if supported by rocksdict + # Note: rocksdict may not expose all RocksDB properties + return None # Placeholder - rocksdict doesn't expose property interface + except Exception: + return None + + def close(self) -> None: + """Close the RocksDB database.""" + if hasattr(self, 'db') and hasattr(self.db, 'close'): + try: + self.db.close() + except Exception: + pass # Already closed + + def __del__(self): + """Cleanup on deletion.""" + # Note: __del__ is not guaranteed to be called + pass diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 21212ad..8603029 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -16,6 +16,9 @@ COPY . . ENV NEXT_TELEMETRY_DISABLED=1 +# Ensure public directory exists (Next.js may not create it if no static assets) +RUN mkdir -p public + RUN npm run build # Production image, copy all the files and run next