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>
This commit is contained in:
copilot-swe-agent[bot]
2025-12-29 09:50:41 +00:00
parent 860c345fe2
commit aae989ec29
3 changed files with 479 additions and 7 deletions

View File

@@ -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"""
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>RocksDB Dashboard</title>
<style>
body {{
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
background: #f5f5f5;
}}
h1 {{
color: #333;
border-bottom: 3px solid #4CAF50;
padding-bottom: 10px;
}}
h2 {{
color: #555;
margin-top: 30px;
}}
.stats-grid {{
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin: 20px 0;
}}
.stat-card {{
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}}
.stat-card h3 {{
margin: 0 0 10px 0;
color: #666;
font-size: 14px;
text-transform: uppercase;
}}
.stat-value {{
font-size: 32px;
font-weight: bold;
color: #4CAF50;
}}
.stat-label {{
color: #999;
font-size: 12px;
}}
table {{
width: 100%;
border-collapse: collapse;
background: white;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
margin: 20px 0;
}}
th, td {{
padding: 12px;
text-align: left;
border-bottom: 1px solid #ddd;
}}
th {{
background: #4CAF50;
color: white;
}}
.operations-table {{
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
}}
.operation-item {{
background: white;
padding: 15px;
border-radius: 5px;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}}
.key-sample {{
font-family: 'Courier New', monospace;
font-size: 12px;
color: #333;
}}
.refresh-btn {{
background: #4CAF50;
color: white;
border: none;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
font-size: 14px;
margin: 10px 0;
}}
.refresh-btn:hover {{
background: #45a049;
}}
</style>
</head>
<body>
<h1>🗄️ RocksDB Dashboard</h1>
<button class="refresh-btn" onclick="location.reload()">🔄 Refresh</button>
<div class="stats-grid">
<div class="stat-card">
<h3>Total Keys</h3>
<div class="stat-value">{stats['total_keys']:,}</div>
<div class="stat-label">stored in database</div>
</div>
<div class="stat-card">
<h3>Total Operations</h3>
<div class="stat-value">{stats['total_operations']:,}</div>
<div class="stat-label">since startup</div>
</div>
<div class="stat-card">
<h3>Ops/Second</h3>
<div class="stat-value">{stats['ops_per_second']:.2f}</div>
<div class="stat-label">average throughput</div>
</div>
<div class="stat-card">
<h3>Cache Hit Rate</h3>
<div class="stat-value">{stats['cache_stats']['hit_rate_percent']:.1f}%</div>
<div class="stat-label">{stats['cache_stats']['hits']:,} hits / {stats['cache_stats']['misses']:,} misses</div>
</div>
</div>
<h2>📊 Operations Breakdown</h2>
<div class="operations-table">
<div class="operation-item">
<strong>GET Operations:</strong> {stats['operations']['get']:,}
</div>
<div class="operation-item">
<strong>PUT Operations:</strong> {stats['operations']['put']:,}
</div>
<div class="operation-item">
<strong>DELETE Operations:</strong> {stats['operations']['delete']:,}
</div>
<div class="operation-item">
<strong>CAS PUT Operations:</strong> {stats['operations']['cas_put']:,}
</div>
</div>
<h2>🔑 Sample Keys ({len(sample_keys)} of {len(all_keys)})</h2>
<table>
<thead>
<tr>
<th>#</th>
<th>Key</th>
</tr>
</thead>
<tbody>
"""
for i, key in enumerate(sample_keys, 1):
html += f"""
<tr>
<td>{i}</td>
<td class="key-sample">{key}</td>
</tr>
"""
html += f"""
</tbody>
</table>
<h2> System Information</h2>
<table>
<tr>
<th>Property</th>
<th>Value</th>
</tr>
<tr>
<td>Database Path</td>
<td class="key-sample">{stats['database_path']}</td>
</tr>
<tr>
<td>Uptime</td>
<td>{stats['uptime_seconds']:.2f} seconds ({stats['uptime_seconds']/60:.1f} minutes)</td>
</tr>
</table>
<p style="text-align: center; color: #999; margin-top: 40px;">
RocksDB HTTP Dashboard | Refresh this page to see updated stats
</p>
</body>
</html>
"""
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."""

207
backend/rocksdb_store.py Normal file
View File

@@ -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