Merge branch 'feature/diagram-post-caching' into testing
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -10,3 +10,6 @@ keys/
|
|||||||
.idea/
|
.idea/
|
||||||
|
|
||||||
*.kate-swp
|
*.kate-swp
|
||||||
|
|
||||||
|
# Diagram cache directory
|
||||||
|
media/diagram_cache/
|
||||||
|
|||||||
105
Documentation/DIAGRAM_CACHING.md
Normal file
105
Documentation/DIAGRAM_CACHING.md
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
# Diagram POST Caching Implementation
|
||||||
|
|
||||||
|
This feature replaces the URL-encoded GET approach for diagram generation with POST requests and local filesystem caching.
|
||||||
|
|
||||||
|
## Changes Overview
|
||||||
|
|
||||||
|
### New Files
|
||||||
|
- `diagramm_proxy/__init__.py` - Module initialization
|
||||||
|
- `diagramm_proxy/diagram_cache.py` - Caching logic and POST request handling
|
||||||
|
- `abschnitte/management/commands/clear_diagram_cache.py` - Management command for cache clearing
|
||||||
|
|
||||||
|
### Modified Files
|
||||||
|
- `abschnitte/utils.py` - Updated `render_textabschnitte()` to use caching
|
||||||
|
- `.gitignore` - Added cache directory exclusion
|
||||||
|
|
||||||
|
## Configuration Required
|
||||||
|
|
||||||
|
Add to your Django settings file (e.g., `VorgabenUI/settings.py`):
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Diagram cache settings
|
||||||
|
DIAGRAM_CACHE_DIR = 'diagram_cache' # relative to MEDIA_ROOT
|
||||||
|
|
||||||
|
# Ensure MEDIA_ROOT and MEDIA_URL are configured
|
||||||
|
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
|
||||||
|
MEDIA_URL = '/media/'
|
||||||
|
```
|
||||||
|
|
||||||
|
### URL Configuration
|
||||||
|
|
||||||
|
Ensure media files are served in development. In your main `urls.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from django.conf import settings
|
||||||
|
from django.conf.urls.static import static
|
||||||
|
|
||||||
|
# ... existing urlpatterns ...
|
||||||
|
|
||||||
|
# Serve media files in development
|
||||||
|
if settings.DEBUG:
|
||||||
|
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||||
|
```
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
1. When a diagram is rendered, the system computes a SHA256 hash of the diagram content
|
||||||
|
2. It checks if a cached SVG exists for that hash
|
||||||
|
3. If cached: serves the existing file
|
||||||
|
4. If not cached: POSTs content to Kroki server, saves the response, and serves it
|
||||||
|
5. Diagrams are served from `MEDIA_URL/diagram_cache/{type}/{hash}.svg`
|
||||||
|
|
||||||
|
## Benefits
|
||||||
|
|
||||||
|
- **No URL length limitations** - Content is POSTed instead of URL-encoded
|
||||||
|
- **Improved performance** - Cached diagrams are served directly from filesystem
|
||||||
|
- **Reduced server load** - Kroki server is only called once per unique diagram
|
||||||
|
- **Persistent cache** - Survives application restarts
|
||||||
|
- **Better error handling** - Graceful fallback on generation failures
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Viewing Diagrams
|
||||||
|
No changes required - diagrams will be automatically cached on first render.
|
||||||
|
|
||||||
|
### Clearing Cache
|
||||||
|
|
||||||
|
Clear all cached diagrams:
|
||||||
|
```bash
|
||||||
|
python manage.py clear_diagram_cache
|
||||||
|
```
|
||||||
|
|
||||||
|
Clear diagrams of a specific type:
|
||||||
|
```bash
|
||||||
|
python manage.py clear_diagram_cache --type plantuml
|
||||||
|
python manage.py clear_diagram_cache --type mermaid
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
1. Create or view a page with diagrams
|
||||||
|
2. Verify diagrams render correctly
|
||||||
|
3. Check that `media/diagram_cache/` directory is created with cached SVGs
|
||||||
|
4. Refresh the page - second load should be faster (cache hit)
|
||||||
|
5. Check logs for cache hit/miss messages
|
||||||
|
6. Test cache clearing command
|
||||||
|
|
||||||
|
## Migration Notes
|
||||||
|
|
||||||
|
- Existing diagrams will be regenerated on first view after deployment
|
||||||
|
- The old URL-based approach is completely replaced
|
||||||
|
- No database migrations needed
|
||||||
|
- Ensure `requests` library is installed (already in requirements.txt)
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Diagrams not rendering
|
||||||
|
- Check that MEDIA_ROOT and MEDIA_URL are configured correctly
|
||||||
|
- Verify Kroki server is accessible at `http://svckroki:8000`
|
||||||
|
- Check application logs for error messages
|
||||||
|
- Ensure media directory is writable
|
||||||
|
|
||||||
|
### Cache not working
|
||||||
|
- Verify Django storage configuration
|
||||||
|
- Check file permissions on media/diagram_cache directory
|
||||||
|
- Review logs for cache-related errors
|
||||||
1
abschnitte/management/__init__.py
Normal file
1
abschnitte/management/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# Management commands
|
||||||
1
abschnitte/management/commands/__init__.py
Normal file
1
abschnitte/management/commands/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# Commands package
|
||||||
23
abschnitte/management/commands/clear_diagram_cache.py
Normal file
23
abschnitte/management/commands/clear_diagram_cache.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from diagramm_proxy.diagram_cache import clear_cache
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = 'Clear cached diagrams'
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
parser.add_argument(
|
||||||
|
'--type',
|
||||||
|
type=str,
|
||||||
|
help='Diagram type to clear (e.g., plantuml, mermaid)',
|
||||||
|
)
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
diagram_type = options.get('type')
|
||||||
|
if diagram_type:
|
||||||
|
self.stdout.write(f'Clearing cache for {diagram_type}...')
|
||||||
|
clear_cache(diagram_type)
|
||||||
|
else:
|
||||||
|
self.stdout.write('Clearing all diagram caches...')
|
||||||
|
clear_cache()
|
||||||
|
self.stdout.write(self.style.SUCCESS('Cache cleared successfully'))
|
||||||
@@ -3,6 +3,10 @@ import base64
|
|||||||
import zlib
|
import zlib
|
||||||
import re
|
import re
|
||||||
from textwrap import dedent
|
from textwrap import dedent
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
# Import the caching function
|
||||||
|
from diagramm_proxy.diagram_cache import get_cached_diagram
|
||||||
|
|
||||||
DIAGRAMMSERVER="/diagramm"
|
DIAGRAMMSERVER="/diagramm"
|
||||||
|
|
||||||
@@ -28,12 +32,20 @@ def render_textabschnitte(queryset):
|
|||||||
temp = inhalt.splitlines()
|
temp = inhalt.splitlines()
|
||||||
diagramtype = temp.pop(0)
|
diagramtype = temp.pop(0)
|
||||||
diagramoptions = 'width="100%"'
|
diagramoptions = 'width="100%"'
|
||||||
if temp[0][0:6].lower() == "option":
|
if temp and temp[0][0:6].lower() == "option":
|
||||||
diagramoptions = temp.pop(0).split(":", 1)[1]
|
diagramoptions = temp.pop(0).split(":", 1)[1]
|
||||||
rest = "\n".join(temp)
|
rest = "\n".join(temp)
|
||||||
html = '<p><img '+diagramoptions+' src="'+DIAGRAMMSERVER+"/"+diagramtype+"/svg/"
|
|
||||||
html += base64.urlsafe_b64encode(zlib.compress(rest.encode("utf-8"),9)).decode()
|
# Use caching instead of URL encoding
|
||||||
html += '"></p>'
|
try:
|
||||||
|
cache_path = get_cached_diagram(diagramtype, rest)
|
||||||
|
# Generate URL to serve from media/static
|
||||||
|
diagram_url = settings.MEDIA_URL + cache_path
|
||||||
|
html = f'<p><img {diagramoptions} src="{diagram_url}"></p>'
|
||||||
|
except Exception as e:
|
||||||
|
# Fallback to error message
|
||||||
|
html = f'<p class="text-danger">Error generating diagram: {str(e)}</p>'
|
||||||
|
|
||||||
elif typ == "code":
|
elif typ == "code":
|
||||||
html = "<pre><code>"
|
html = "<pre><code>"
|
||||||
html += markdown(inhalt, extensions=['tables', 'attr_list'])
|
html += markdown(inhalt, extensions=['tables', 'attr_list'])
|
||||||
|
|||||||
1
diagramm_proxy/__init__.py
Normal file
1
diagramm_proxy/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# Diagram proxy module
|
||||||
91
diagramm_proxy/diagram_cache.py
Normal file
91
diagramm_proxy/diagram_cache.py
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import hashlib
|
||||||
|
import os
|
||||||
|
import requests
|
||||||
|
from pathlib import Path
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.files.storage import default_storage
|
||||||
|
from django.core.files.base import ContentFile
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Configure cache directory
|
||||||
|
CACHE_DIR = getattr(settings, 'DIAGRAM_CACHE_DIR', 'diagram_cache')
|
||||||
|
KROKI_UPSTREAM = "http://svckroki:8000"
|
||||||
|
|
||||||
|
def get_cache_path(diagram_type, content_hash):
|
||||||
|
"""Generate cache file path for a diagram."""
|
||||||
|
return os.path.join(CACHE_DIR, diagram_type, f"{content_hash}.svg")
|
||||||
|
|
||||||
|
def compute_hash(content):
|
||||||
|
"""Compute SHA256 hash of diagram content."""
|
||||||
|
return hashlib.sha256(content.encode('utf-8')).hexdigest()
|
||||||
|
|
||||||
|
def get_cached_diagram(diagram_type, diagram_content):
|
||||||
|
"""
|
||||||
|
Retrieve diagram from cache or generate it via POST.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
diagram_type: Type of diagram (e.g., 'plantuml', 'mermaid')
|
||||||
|
diagram_content: Raw diagram content
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Path to cached diagram file (relative to MEDIA_ROOT)
|
||||||
|
"""
|
||||||
|
content_hash = compute_hash(diagram_content)
|
||||||
|
cache_path = get_cache_path(diagram_type, content_hash)
|
||||||
|
|
||||||
|
# Check if diagram exists in cache
|
||||||
|
if default_storage.exists(cache_path):
|
||||||
|
logger.debug(f"Cache hit for {diagram_type} diagram: {content_hash[:8]}")
|
||||||
|
return cache_path
|
||||||
|
|
||||||
|
# Generate diagram via POST request
|
||||||
|
logger.info(f"Cache miss for {diagram_type} diagram: {content_hash[:8]}, generating...")
|
||||||
|
try:
|
||||||
|
url = f"{KROKI_UPSTREAM}/{diagram_type}/svg"
|
||||||
|
response = requests.post(
|
||||||
|
url,
|
||||||
|
data=diagram_content.encode('utf-8'),
|
||||||
|
headers={'Content-Type': 'text/plain'},
|
||||||
|
timeout=30
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
# Ensure cache directory exists
|
||||||
|
cache_dir = os.path.dirname(default_storage.path(cache_path))
|
||||||
|
os.makedirs(cache_dir, exist_ok=True)
|
||||||
|
|
||||||
|
# Save to cache
|
||||||
|
default_storage.save(cache_path, ContentFile(response.content))
|
||||||
|
logger.info(f"Diagram cached successfully: {cache_path}")
|
||||||
|
|
||||||
|
return cache_path
|
||||||
|
|
||||||
|
except requests.RequestException as e:
|
||||||
|
logger.error(f"Error generating diagram: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
def clear_cache(diagram_type=None):
|
||||||
|
"""
|
||||||
|
Clear cached diagrams.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
diagram_type: If specified, only clear diagrams of this type
|
||||||
|
"""
|
||||||
|
if diagram_type:
|
||||||
|
cache_path = os.path.join(CACHE_DIR, diagram_type)
|
||||||
|
else:
|
||||||
|
cache_path = CACHE_DIR
|
||||||
|
|
||||||
|
if default_storage.exists(cache_path):
|
||||||
|
full_path = default_storage.path(cache_path)
|
||||||
|
# Walk through and delete files
|
||||||
|
for root, dirs, files in os.walk(full_path):
|
||||||
|
for file in files:
|
||||||
|
file_path = os.path.join(root, file)
|
||||||
|
try:
|
||||||
|
os.remove(file_path)
|
||||||
|
logger.info(f"Deleted cached diagram: {file_path}")
|
||||||
|
except OSError as e:
|
||||||
|
logger.error(f"Error deleting {file_path}: {e}")
|
||||||
Reference in New Issue
Block a user