From 860c345fe2031572754de7885e2abe2c5c8d406d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 29 Dec 2025 09:39:38 +0000 Subject: [PATCH 1/5] Initial plan 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 2/5] 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""" + + + + + + RocksDB Dashboard + + + +

đŸ—„ī¸ RocksDB Dashboard

+ + + +
+
+

Total Keys

+
{stats['total_keys']:,}
+
stored in database
+
+ +
+

Total Operations

+
{stats['total_operations']:,}
+
since startup
+
+ +
+

Ops/Second

+
{stats['ops_per_second']:.2f}
+
average throughput
+
+ +
+

Cache Hit Rate

+
{stats['cache_stats']['hit_rate_percent']:.1f}%
+
{stats['cache_stats']['hits']:,} hits / {stats['cache_stats']['misses']:,} misses
+
+
+ +

📊 Operations Breakdown

+
+
+ GET Operations: {stats['operations']['get']:,} +
+
+ PUT Operations: {stats['operations']['put']:,} +
+
+ DELETE Operations: {stats['operations']['delete']:,} +
+
+ CAS PUT Operations: {stats['operations']['cas_put']:,} +
+
+ +

🔑 Sample Keys ({len(sample_keys)} of {len(all_keys)})

+ + + + + + + + + """ + + for i, key in enumerate(sample_keys, 1): + html += f""" + + + + + """ + + html += f""" + +
#Key
{i}{key}
+ +

â„šī¸ System Information

+ + + + + + + + + + + + + +
PropertyValue
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 From 26b78a0d0e2f53898784d9245f0d566285132df7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 29 Dec 2025 09:52:47 +0000 Subject: [PATCH 3/5] Address code review feedback - improve RocksDB implementation - Remove unused AccessType import - Fix cas_put to avoid inflating operation counters - Add limit parameter to keys() method for efficiency - Optimize count() method to avoid loading all keys twice - Remove non-functional get_rocksdb_property method - Update dashboard to use keys() limit parameter Co-authored-by: johndoe6345789 <224850594+johndoe6345789@users.noreply.github.com> --- backend/app.py | 8 +++--- backend/rocksdb_store.py | 53 +++++++++++++++++++++++----------------- 2 files changed, 34 insertions(+), 27 deletions(-) diff --git a/backend/app.py b/backend/app.py index b9e61bb..0399256 100644 --- a/backend/app.py +++ b/backend/app.py @@ -717,9 +717,9 @@ def rocksdb_dashboard(): 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 + # Sample some keys for display (limit to avoid loading all keys) + sample_keys = kv_store.keys(limit=20) + total_keys = stats['total_keys'] html = f""" @@ -866,7 +866,7 @@ def rocksdb_dashboard(): -

🔑 Sample Keys ({len(sample_keys)} of {len(all_keys)})

+

🔑 Sample Keys ({len(sample_keys)} of {total_keys})

diff --git a/backend/rocksdb_store.py b/backend/rocksdb_store.py index d73beb4..26db1b8 100644 --- a/backend/rocksdb_store.py +++ b/backend/rocksdb_store.py @@ -7,7 +7,7 @@ import json import time from pathlib import Path from typing import Any, Dict, Optional, List -from rocksdict import Rdict, Options, AccessType +from rocksdict import Rdict, Options class RocksDBStore: @@ -98,9 +98,13 @@ class RocksDBStore: self.stats['operations']['cas_put'] += 1 if if_absent: - existing = self.get(key) - if existing is not None: - return False + # Check existence without incrementing get counter (internal operation) + try: + value_bytes = self.db.get(key.encode('utf-8')) + if value_bytes is not None: + return False + except Exception: + pass self.put(key, value) return True @@ -118,11 +122,12 @@ class RocksDBStore: except KeyError: pass # Key doesn't exist, that's fine - def keys(self, prefix: Optional[str] = None) -> List[str]: + def keys(self, prefix: Optional[str] = None, limit: Optional[int] = None) -> List[str]: """List all keys, optionally filtered by prefix. Args: prefix: Optional prefix to filter keys + limit: Optional limit on number of keys to return Returns: List of keys (as strings) @@ -134,8 +139,13 @@ class RocksDBStore: for key in self.db.keys(): if key.startswith(prefix_bytes): keys.append(key.decode('utf-8')) + if limit and len(keys) >= limit: + break else: - keys = [key.decode('utf-8') for key in self.db.keys()] + for key in self.db.keys(): + keys.append(key.decode('utf-8')) + if limit and len(keys) >= limit: + break return keys @@ -148,7 +158,20 @@ class RocksDBStore: Returns: Number of keys """ - return len(self.keys(prefix)) + # For accurate counts, we need to iterate + # In production, consider maintaining separate counters + count = 0 + + if prefix: + prefix_bytes = prefix.encode('utf-8') + for key in self.db.keys(): + if key.startswith(prefix_bytes): + count += 1 + else: + for key in self.db.keys(): + count += 1 + + return count def get_stats(self) -> Dict[str, Any]: """Get RocksDB statistics. @@ -177,22 +200,6 @@ class RocksDBStore: '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'): From a582d733ea80185ce831dbd7ba70ac02717df47b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 29 Dec 2025 09:54:43 +0000 Subject: [PATCH 4/5] Fix remaining code review issues - Prevent double-counting in cas_put by adding internal flag to put() - Pass limit parameter directly to keys() method in /rocksdb/keys endpoint - Restore __del__ method to ensure proper RocksDB cleanup on garbage collection Co-authored-by: johndoe6345789 <224850594+johndoe6345789@users.noreply.github.com> --- backend/app.py | 11 ++++------- backend/rocksdb_store.py | 12 +++++++----- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/backend/app.py b/backend/app.py index 0399256..eb37999 100644 --- a/backend/app.py +++ b/backend/app.py @@ -687,14 +687,11 @@ def rocksdb_keys(): prefix = request.args.get("prefix", None) limit = int(request.args.get("limit", "100")) - keys = kv_store.keys(prefix) + # Pass limit to keys() method for efficiency + keys = kv_store.keys(prefix, limit=limit) - # Limit results - if len(keys) > limit: - keys = keys[:limit] - truncated = True - else: - truncated = False + # Check if we hit the limit (might have more keys) + truncated = len(keys) == limit return jsonify({ "ok": True, diff --git a/backend/rocksdb_store.py b/backend/rocksdb_store.py index 26db1b8..5fd4a38 100644 --- a/backend/rocksdb_store.py +++ b/backend/rocksdb_store.py @@ -71,14 +71,16 @@ class RocksDBStore: self.stats['cache_misses'] += 1 return None - def put(self, key: str, value: Any) -> None: + def put(self, key: str, value: Any, _internal: bool = False) -> None: """Store value in RocksDB. Args: key: Key to store value: Value to store (will be JSON serialized) + _internal: If True, don't increment operation counter (internal use) """ - self.stats['operations']['put'] += 1 + if not _internal: + self.stats['operations']['put'] += 1 # Serialize value as JSON value_json = json.dumps(value) @@ -106,7 +108,8 @@ class RocksDBStore: except Exception: pass - self.put(key, value) + # Use internal put to avoid double-counting + self.put(key, value, _internal=True) return True def delete(self, key: str) -> None: @@ -210,5 +213,4 @@ class RocksDBStore: def __del__(self): """Cleanup on deletion.""" - # Note: __del__ is not guaranteed to be called - pass + self.close() From 9fbeacdeb954a7d743c1fec6a49226ff94373b57 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 29 Dec 2025 09:56:45 +0000 Subject: [PATCH 5/5] Add documentation and improve error handling - Add comment about index_store being in-memory (future migration consideration) - Improve exception handling in cas_put to log unexpected errors - Add detailed docstring to count() method noting performance considerations Co-authored-by: johndoe6345789 <224850594+johndoe6345789@users.noreply.github.com> --- backend/app.py | 3 +++ backend/rocksdb_store.py | 14 ++++++++++---- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/backend/app.py b/backend/app.py index eb37999..5bcb82a 100644 --- a/backend/app.py +++ b/backend/app.py @@ -52,6 +52,9 @@ ROCKSDB_DIR.mkdir(parents=True, exist_ok=True) # RocksDB KV store (replaces in-memory dict) kv_store = RocksDBStore(str(ROCKSDB_DIR)) + +# Index store - currently in-memory, could be migrated to RocksDB in the future +# for full persistence and consistency across restarts index_store: Dict[str, list] = {} diff --git a/backend/rocksdb_store.py b/backend/rocksdb_store.py index 5fd4a38..23b1779 100644 --- a/backend/rocksdb_store.py +++ b/backend/rocksdb_store.py @@ -105,8 +105,11 @@ class RocksDBStore: value_bytes = self.db.get(key.encode('utf-8')) if value_bytes is not None: return False - except Exception: - pass + except KeyError: + pass # Key doesn't exist + except Exception as e: + # Log unexpected errors but continue with put operation + print(f"Warning: Error checking key existence in cas_put: {e}") # Use internal put to avoid double-counting self.put(key, value, _internal=True) @@ -160,9 +163,12 @@ class RocksDBStore: Returns: Number of keys + + Note: + This method iterates through all keys, which can be expensive for large + datasets. In production, consider maintaining separate counters updated + during put/delete operations for better performance. """ - # For accurate counts, we need to iterate - # In production, consider maintaining separate counters count = 0 if prefix: