Compare commits
5 Commits
interim-do
...
v0.35.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
b58b91cd14
|
|||
|
20293046d3
|
|||
|
a6d969d7e9
|
|||
|
a5dc827e15
|
|||
|
be81b3320e
|
@@ -111,7 +111,13 @@
|
||||
"Bash(fi)",
|
||||
"Bash(xargs:*)",
|
||||
"Bash(for i in 1 2 3 4 5)",
|
||||
"Bash(do)"
|
||||
"Bash(do)",
|
||||
"WebFetch(domain:vermaden.wordpress.com)",
|
||||
"WebFetch(domain:eylenburg.github.io)",
|
||||
"Bash(go run -exec '' -c 'package main; import \"\"git.mleku.dev/mleku/nostr/utils/normalize\"\"; import \"\"fmt\"\"; func main() { fmt.Println(string(normalize.URL([]byte(\"\"relay.example.com:3334\"\")))); fmt.Println(string(normalize.URL([]byte(\"\"relay.example.com:443\"\")))); fmt.Println(string(normalize.URL([]byte(\"\"ws://relay.example.com:3334\"\")))); fmt.Println(string(normalize.URL([]byte(\"\"wss://relay.example.com:3334\"\")))) }')",
|
||||
"Bash(go run:*)",
|
||||
"Bash(git commit -m \"$(cat <<''EOF''\nFix NIP-11 fetch URL scheme conversion for non-proxied relays\n\n- Convert wss:// to https:// and ws:// to http:// before fetching NIP-11\n documents, fixing failures for users not using HTTPS upgrade proxies\n- The fetchNIP11 function was using WebSocket URLs directly for HTTP\n requests, causing scheme mismatch errors\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)\n\nCo-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>\nEOF\n)\")",
|
||||
"Bash(/tmp/orly help:*)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
|
||||
254
BUG_REPORTS_AND_FEATURE_REQUEST_PROTOCOL.md
Normal file
254
BUG_REPORTS_AND_FEATURE_REQUEST_PROTOCOL.md
Normal file
@@ -0,0 +1,254 @@
|
||||
# Feature Request and Bug Report Protocol
|
||||
|
||||
This document describes how to submit effective bug reports and feature requests for ORLY relay. Following these guidelines helps maintainers understand and resolve issues quickly.
|
||||
|
||||
## Before Submitting
|
||||
|
||||
1. **Search existing issues** - Your issue may already be reported or discussed
|
||||
2. **Check documentation** - Review `CLAUDE.md`, `docs/`, and `pkg/*/README.md` files
|
||||
3. **Verify with latest version** - Ensure the issue exists in the current release
|
||||
4. **Test with default configuration** - Rule out configuration-specific problems
|
||||
|
||||
## Bug Reports
|
||||
|
||||
### Required Information
|
||||
|
||||
**Title**: Concise summary of the problem
|
||||
- Good: "Kind 3 events with 8000+ follows truncated on save"
|
||||
- Bad: "Events not saving" or "Bug in database"
|
||||
|
||||
**Environment**:
|
||||
```
|
||||
ORLY version: (output of ./orly version)
|
||||
OS: (e.g., Ubuntu 24.04, macOS 14.2)
|
||||
Go version: (output of go version)
|
||||
Database backend: (badger/neo4j/wasmdb)
|
||||
```
|
||||
|
||||
**Configuration** (relevant settings only):
|
||||
```bash
|
||||
ORLY_DB_TYPE=badger
|
||||
ORLY_POLICY_ENABLED=true
|
||||
# Include any non-default settings
|
||||
```
|
||||
|
||||
**Steps to Reproduce**:
|
||||
1. Start relay with configuration X
|
||||
2. Connect client and send event Y
|
||||
3. Query for event with filter Z
|
||||
4. Observe error/unexpected behavior
|
||||
|
||||
**Expected Behavior**: What should happen
|
||||
|
||||
**Actual Behavior**: What actually happens
|
||||
|
||||
**Logs**: Include relevant log output with `ORLY_LOG_LEVEL=debug` or `trace`
|
||||
|
||||
### Minimal Reproduction
|
||||
|
||||
The most effective bug reports include a minimal reproduction case:
|
||||
|
||||
```bash
|
||||
# Example: Script that demonstrates the issue
|
||||
export ORLY_LOG_LEVEL=debug
|
||||
./orly &
|
||||
sleep 2
|
||||
|
||||
# Send problematic event
|
||||
echo '["EVENT", {...}]' | websocat ws://localhost:3334
|
||||
|
||||
# Show the failure
|
||||
echo '["REQ", "test", {"kinds": [1]}]' | websocat ws://localhost:3334
|
||||
```
|
||||
|
||||
Or provide a failing test case:
|
||||
|
||||
```go
|
||||
func TestReproduceBug(t *testing.T) {
|
||||
// Setup
|
||||
db := setupTestDB(t)
|
||||
|
||||
// This should work but fails
|
||||
event := createTestEvent(kind, content)
|
||||
err := db.SaveEvent(ctx, event)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Query returns unexpected result
|
||||
results, err := db.QueryEvents(ctx, filter)
|
||||
assert.Len(t, results, 1) // Fails: got 0
|
||||
}
|
||||
```
|
||||
|
||||
## Feature Requests
|
||||
|
||||
### Required Information
|
||||
|
||||
**Title**: Clear description of the feature
|
||||
- Good: "Add WebSocket compression support (permessage-deflate)"
|
||||
- Bad: "Make it faster" or "New feature idea"
|
||||
|
||||
**Problem Statement**: What problem does this solve?
|
||||
```
|
||||
Currently, clients with high-latency connections experience slow sync times
|
||||
because event data is transmitted uncompressed. A typical session transfers
|
||||
50MB of JSON that could be reduced to ~10MB with compression.
|
||||
```
|
||||
|
||||
**Proposed Solution**: How should it work?
|
||||
```
|
||||
Add optional permessage-deflate WebSocket extension support:
|
||||
- New config: ORLY_WS_COMPRESSION=true
|
||||
- Negotiate compression during WebSocket handshake
|
||||
- Apply to messages over configurable threshold (default 1KB)
|
||||
```
|
||||
|
||||
**Use Case**: Who benefits and how?
|
||||
```
|
||||
- Mobile clients on cellular connections
|
||||
- Users syncing large follow lists
|
||||
- Relays with bandwidth constraints
|
||||
```
|
||||
|
||||
**Alternatives Considered** (optional):
|
||||
```
|
||||
- Application-level compression: Rejected because it requires client changes
|
||||
- HTTP/2: Not applicable for WebSocket connections
|
||||
```
|
||||
|
||||
### Implementation Notes (optional)
|
||||
|
||||
If you have implementation ideas:
|
||||
|
||||
```
|
||||
Suggested approach:
|
||||
1. Add compression config to app/config/config.go
|
||||
2. Modify gorilla/websocket upgrader in app/handle-websocket.go
|
||||
3. Add compression threshold check before WriteMessage()
|
||||
|
||||
Reference: gorilla/websocket has built-in permessage-deflate support
|
||||
```
|
||||
|
||||
## What Makes Reports Effective
|
||||
|
||||
**Do**:
|
||||
- Be specific and factual
|
||||
- Include version numbers and exact error messages
|
||||
- Provide reproducible steps
|
||||
- Attach relevant logs (redact sensitive data)
|
||||
- Link to related issues or discussions
|
||||
- Respond to follow-up questions promptly
|
||||
|
||||
**Avoid**:
|
||||
- Vague descriptions ("it doesn't work")
|
||||
- Multiple unrelated issues in one report
|
||||
- Assuming the cause without evidence
|
||||
- Demanding immediate fixes
|
||||
- Duplicating existing issues
|
||||
|
||||
## Issue Labels
|
||||
|
||||
When applicable, suggest appropriate labels:
|
||||
|
||||
| Label | Use When |
|
||||
|-------|----------|
|
||||
| `bug` | Something isn't working as documented |
|
||||
| `enhancement` | New feature or improvement |
|
||||
| `performance` | Speed or resource usage issue |
|
||||
| `documentation` | Docs are missing or incorrect |
|
||||
| `question` | Clarification needed (not a bug) |
|
||||
| `good first issue` | Suitable for new contributors |
|
||||
|
||||
## Response Expectations
|
||||
|
||||
- **Acknowledgment**: Within a few days
|
||||
- **Triage**: Issue labeled and prioritized
|
||||
- **Resolution**: Depends on complexity and priority
|
||||
|
||||
Complex features may require discussion before implementation. Bug fixes for critical issues are prioritized.
|
||||
|
||||
## Following Up
|
||||
|
||||
If your issue hasn't received attention:
|
||||
|
||||
1. **Check issue status** - It may be labeled or assigned
|
||||
2. **Add new information** - If you've discovered more details
|
||||
3. **Politely bump** - A single follow-up comment after 2 weeks is appropriate
|
||||
4. **Consider contributing** - PRs that fix bugs or implement features are welcome
|
||||
|
||||
## Contributing Fixes
|
||||
|
||||
If you want to fix a bug or implement a feature yourself:
|
||||
|
||||
1. Comment on the issue to avoid duplicate work
|
||||
2. Follow the coding patterns in `CLAUDE.md`
|
||||
3. Include tests for your changes
|
||||
4. Keep PRs focused on a single issue
|
||||
5. Reference the issue number in your PR
|
||||
|
||||
## Security Issues
|
||||
|
||||
**Do not report security vulnerabilities in public issues.**
|
||||
|
||||
For security-sensitive bugs:
|
||||
- Contact maintainers directly
|
||||
- Provide detailed reproduction steps privately
|
||||
- Allow reasonable time for a fix before disclosure
|
||||
|
||||
## Examples
|
||||
|
||||
### Good Bug Report
|
||||
|
||||
```markdown
|
||||
## WebSocket disconnects after 60 seconds of inactivity
|
||||
|
||||
**Environment**:
|
||||
- ORLY v0.34.5
|
||||
- Ubuntu 22.04
|
||||
- Go 1.25.3
|
||||
- Badger backend
|
||||
|
||||
**Steps to Reproduce**:
|
||||
1. Connect to relay: `websocat ws://localhost:3334`
|
||||
2. Send subscription: `["REQ", "test", {"kinds": [1], "limit": 1}]`
|
||||
3. Wait 60 seconds without sending messages
|
||||
4. Observe connection closed
|
||||
|
||||
**Expected**: Connection remains open (Nostr relays should maintain persistent connections)
|
||||
|
||||
**Actual**: Connection closed with code 1000 after exactly 60 seconds
|
||||
|
||||
**Logs** (ORLY_LOG_LEVEL=debug):
|
||||
```
|
||||
1764783029014485🔎 client timeout, closing connection /app/handle-websocket.go:142
|
||||
```
|
||||
|
||||
**Possible Cause**: May be related to read deadline not being extended on subscription activity
|
||||
```
|
||||
|
||||
### Good Feature Request
|
||||
|
||||
```markdown
|
||||
## Add rate limiting per pubkey
|
||||
|
||||
**Problem**:
|
||||
A single pubkey can flood the relay with events, consuming storage and
|
||||
bandwidth. Currently there's no way to limit per-author submission rate.
|
||||
|
||||
**Proposed Solution**:
|
||||
Add configurable rate limiting:
|
||||
```bash
|
||||
ORLY_RATE_LIMIT_EVENTS_PER_MINUTE=60
|
||||
ORLY_RATE_LIMIT_BURST=10
|
||||
```
|
||||
|
||||
When exceeded, return OK false with "rate-limited" message per NIP-20.
|
||||
|
||||
**Use Case**:
|
||||
- Public relays protecting against spam
|
||||
- Community relays with fair-use policies
|
||||
- Paid relays enforcing subscription tiers
|
||||
|
||||
**Alternatives Considered**:
|
||||
- IP-based limiting: Ineffective because users share IPs and use VPNs
|
||||
- Global limiting: Punishes all users for one bad actor
|
||||
```
|
||||
@@ -147,6 +147,10 @@ export ORLY_SPROCKET_ENABLED=true
|
||||
# Enable policy system
|
||||
export ORLY_POLICY_ENABLED=true
|
||||
|
||||
# Custom policy file path (MUST be ABSOLUTE path starting with /)
|
||||
# Default: ~/.config/ORLY/policy.json (or ~/.config/{ORLY_APP_NAME}/policy.json)
|
||||
# export ORLY_POLICY_PATH=/etc/orly/policy.json
|
||||
|
||||
# Database backend selection (badger, neo4j, or wasmdb)
|
||||
export ORLY_DB_TYPE=badger
|
||||
|
||||
@@ -270,7 +274,8 @@ export ORLY_AUTH_TO_WRITE=false # Require auth only for writes
|
||||
- `none.go` - Open relay (no restrictions)
|
||||
|
||||
**`pkg/policy/`** - Event filtering and validation policies
|
||||
- Policy configuration loaded from `~/.config/ORLY/policy.json`
|
||||
- Policy configuration loaded from `~/.config/ORLY/policy.json` by default
|
||||
- Custom path via `ORLY_POLICY_PATH` (MUST be absolute path starting with `/`)
|
||||
- Per-kind size limits, age restrictions, custom scripts
|
||||
- **Write-Only Validation**: Size, age, tag, and expiry validations apply ONLY to write operations
|
||||
- **Read-Only Filtering**: `read_allow`, `read_deny`, `privileged` apply ONLY to read operations
|
||||
|
||||
22
README.md
22
README.md
@@ -1,5 +1,7 @@
|
||||
# next.orly.dev
|
||||
|
||||
---
|
||||
|
||||

|
||||
|
||||

|
||||
@@ -10,6 +12,19 @@ zap me: <20>mlekudev@getalby.com
|
||||
|
||||
follow me on [nostr](https://jumble.social/users/npub1fjqqy4a93z5zsjwsfxqhc2764kvykfdyttvldkkkdera8dr78vhsmmleku)
|
||||
|
||||
## ⚠️ Bug Reports & Feature Requests
|
||||
|
||||
**Bug reports and feature requests that do not follow the protocol will not be accepted.**
|
||||
|
||||
Before submitting any issue, you must read and follow [BUG_REPORTS_AND_FEATURE_REQUEST_PROTOCOL.md](./BUG_REPORTS_AND_FEATURE_REQUEST_PROTOCOL.md).
|
||||
|
||||
Requirements:
|
||||
- **Bug reports**: Include environment details, reproduction steps, expected/actual behavior, and logs
|
||||
- **Feature requests**: Include problem statement, proposed solution, and use cases
|
||||
- **Both**: Search existing issues first, verify with latest version, provide minimal reproduction
|
||||
|
||||
Issues missing required information will be closed without review.
|
||||
|
||||
## ⚠️ System Requirements
|
||||
|
||||
> **IMPORTANT: ORLY requires a minimum of 500MB of free memory to operate.**
|
||||
@@ -217,7 +232,12 @@ ORLY includes a comprehensive policy system for fine-grained control over event
|
||||
|
||||
```bash
|
||||
export ORLY_POLICY_ENABLED=true
|
||||
# Create policy file at ~/.config/ORLY/policy.json
|
||||
# Default policy file: ~/.config/ORLY/policy.json
|
||||
|
||||
# OPTIONAL: Use a custom policy file location
|
||||
# WARNING: ORLY_POLICY_PATH MUST be an ABSOLUTE path (starting with /)
|
||||
# Relative paths will be REJECTED and the relay will fail to start
|
||||
export ORLY_POLICY_PATH=/etc/orly/policy.json
|
||||
```
|
||||
|
||||
For detailed configuration and examples, see the [Policy Usage Guide](docs/POLICY_USAGE_GUIDE.md).
|
||||
|
||||
@@ -82,7 +82,8 @@ type C struct {
|
||||
DirectorySpiderInterval time.Duration `env:"ORLY_DIRECTORY_SPIDER_INTERVAL" default:"24h" usage:"how often to run directory spider"`
|
||||
DirectorySpiderMaxHops int `env:"ORLY_DIRECTORY_SPIDER_HOPS" default:"3" usage:"maximum hops for relay discovery from seed users"`
|
||||
|
||||
PolicyEnabled bool `env:"ORLY_POLICY_ENABLED" default:"false" usage:"enable policy-based event processing (configuration found in $HOME/.config/ORLY/policy.json)"`
|
||||
PolicyEnabled bool `env:"ORLY_POLICY_ENABLED" default:"false" usage:"enable policy-based event processing (default config: $HOME/.config/ORLY/policy.json)"`
|
||||
PolicyPath string `env:"ORLY_POLICY_PATH" usage:"ABSOLUTE path to policy configuration file (MUST start with /); overrides default location; relative paths are rejected"`
|
||||
|
||||
// NIP-43 Relay Access Metadata and Requests
|
||||
NIP43Enabled bool `env:"ORLY_NIP43_ENABLED" default:"false" usage:"enable NIP-43 relay access metadata and invite system"`
|
||||
|
||||
@@ -3,9 +3,7 @@ package app
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/adrg/xdg"
|
||||
"lol.mleku.dev/log"
|
||||
"git.mleku.dev/mleku/nostr/encoders/event"
|
||||
"git.mleku.dev/mleku/nostr/encoders/filter"
|
||||
@@ -76,8 +74,8 @@ func (l *Listener) HandlePolicyConfigUpdate(ev *event.E) error {
|
||||
|
||||
log.I.F("policy config validation passed")
|
||||
|
||||
// Get config path for saving
|
||||
configPath := filepath.Join(xdg.ConfigHome, l.Config.AppName, "policy.json")
|
||||
// Get config path for saving (uses custom path if set, otherwise default)
|
||||
configPath := l.policyManager.ConfigPath()
|
||||
|
||||
// 3. Pause ALL message processing (lock mutex)
|
||||
// Note: We need to release the RLock first (which caller holds), then acquire exclusive Lock
|
||||
|
||||
@@ -74,7 +74,7 @@ func setupPolicyTestListener(t *testing.T, policyAdminHex string) (*Listener, *d
|
||||
}
|
||||
|
||||
// Create policy manager - now config file exists at XDG path
|
||||
policyManager := policy.NewWithManager(ctx, cfg.AppName, cfg.PolicyEnabled)
|
||||
policyManager := policy.NewWithManager(ctx, cfg.AppName, cfg.PolicyEnabled, "")
|
||||
|
||||
server := &Server{
|
||||
Ctx: ctx,
|
||||
|
||||
@@ -87,7 +87,7 @@ func Run(
|
||||
l.sprocketManager = NewSprocketManager(ctx, cfg.AppName, cfg.SprocketEnabled)
|
||||
|
||||
// Initialize policy manager
|
||||
l.policyManager = policy.NewWithManager(ctx, cfg.AppName, cfg.PolicyEnabled)
|
||||
l.policyManager = policy.NewWithManager(ctx, cfg.AppName, cfg.PolicyEnabled, cfg.PolicyPath)
|
||||
|
||||
// Merge policy-defined owners with environment-defined owners
|
||||
// This allows cloud deployments to add owners via policy.json when env vars cannot be modified
|
||||
|
||||
142
docs/RATE_LIMITING_TEST_REPORT_NEO4J.md
Normal file
142
docs/RATE_LIMITING_TEST_REPORT_NEO4J.md
Normal file
@@ -0,0 +1,142 @@
|
||||
# Rate Limiting Test Report: Neo4j Backend
|
||||
|
||||
**Test Date:** December 12, 2025
|
||||
**Test Duration:** 73 minutes (4,409 seconds)
|
||||
**Import File:** `wot_reference.jsonl` (2.7 GB, 2,158,366 events)
|
||||
|
||||
## Configuration
|
||||
|
||||
| Parameter | Value |
|
||||
|-----------|-------|
|
||||
| Database Backend | Neo4j 5-community (Docker) |
|
||||
| Target Memory | 1,500 MB (relay process) |
|
||||
| Emergency Threshold | 1,167 (target + 1/6) |
|
||||
| Recovery Threshold | 833 (target - 1/6) |
|
||||
| Max Write Delay | 1,000 ms (normal), 5,000 ms (emergency) |
|
||||
| Neo4j Memory Limits | Heap: 512MB-1GB, Page Cache: 512MB |
|
||||
|
||||
## Results Summary
|
||||
|
||||
### Memory Management
|
||||
|
||||
| Component | Metric | Value |
|
||||
|-----------|--------|-------|
|
||||
| **Relay Process** | Peak RSS (VmHWM) | 148 MB |
|
||||
| **Relay Process** | Final RSS | 35 MB |
|
||||
| **Neo4j Container** | Memory Usage | 1.614 GB |
|
||||
| **Neo4j Container** | Memory % | 10.83% of 14.91GB |
|
||||
| **Rate Limiting** | Events Triggered | **0** |
|
||||
|
||||
### Key Finding: Architecture Difference
|
||||
|
||||
Unlike Badger (embedded database), Neo4j runs as a **separate process** in a Docker container. This means:
|
||||
|
||||
1. **Relay process memory stays low** (~35MB) because it's just a client
|
||||
2. **Neo4j manages its own memory** within the container (1.6GB used)
|
||||
3. **Rate limiter monitors relay RSS**, which doesn't reflect Neo4j's actual load
|
||||
4. **No rate limiting triggered** because relay memory never approached the 1.5GB target
|
||||
|
||||
This is architecturally correct - the relay doesn't need memory-based rate limiting for Neo4j because it's not holding the data in process.
|
||||
|
||||
### Event Processing
|
||||
|
||||
| Event Type | Count | Rate |
|
||||
|------------|-------|------|
|
||||
| Contact Lists (kind 3) | 174,836 | 40 events/sec |
|
||||
| Mute Lists (kind 10000) | 4,027 | 0.9 events/sec |
|
||||
| **Total Social Events** | **178,863** | **41 events/sec** |
|
||||
|
||||
### Neo4j Performance
|
||||
|
||||
| Metric | Value |
|
||||
|--------|-------|
|
||||
| CPU Usage | 40-45% |
|
||||
| Memory | Stable at 1.6GB |
|
||||
| Disk Writes | 12.7 GB |
|
||||
| Network In | 1.8 GB |
|
||||
| Network Out | 583 MB |
|
||||
| Process Count | 77-82 |
|
||||
|
||||
### Import Throughput Over Time
|
||||
|
||||
```
|
||||
Time Contact Lists Delta/min Neo4j Memory
|
||||
------ ------------- --------- ------------
|
||||
08:28 0 - 1.57 GB
|
||||
08:47 31,257 ~2,100 1.61 GB
|
||||
08:52 42,403 ~2,200 1.61 GB
|
||||
09:02 67,581 ~2,500 1.61 GB
|
||||
09:12 97,316 ~3,000 1.60 GB
|
||||
09:22 112,681 ~3,100 1.61 GB
|
||||
09:27 163,252 ~10,000* 1.61 GB
|
||||
09:41 174,836 ~2,400 1.61 GB
|
||||
```
|
||||
*Spike may be due to batch processing of cached events
|
||||
|
||||
### Memory Stability
|
||||
|
||||
Neo4j's memory usage remained remarkably stable throughout the test:
|
||||
|
||||
```
|
||||
Sample Memory Delta
|
||||
-------- -------- -----
|
||||
08:47 1.605 GB -
|
||||
09:02 1.611 GB +6 MB
|
||||
09:12 1.603 GB -8 MB
|
||||
09:27 1.607 GB +4 MB
|
||||
09:41 1.614 GB +7 MB
|
||||
```
|
||||
|
||||
**Variance:** < 15 MB over 73 minutes - excellent stability.
|
||||
|
||||
## Architecture Comparison: Badger vs Neo4j
|
||||
|
||||
| Aspect | Badger | Neo4j |
|
||||
|--------|--------|-------|
|
||||
| Database Type | Embedded | External (Docker) |
|
||||
| Memory Consumer | Relay process | Container process |
|
||||
| Rate Limiter Target | Relay RSS | Relay RSS |
|
||||
| Rate Limiting Effectiveness | High | Low* |
|
||||
| Compaction Triggering | Yes | N/A |
|
||||
| Emergency Mode | Yes | Not triggered |
|
||||
|
||||
*The current rate limiter design targets relay process memory, which doesn't reflect Neo4j's actual resource usage.
|
||||
|
||||
## Recommendations for Neo4j Rate Limiting
|
||||
|
||||
The current implementation monitors **relay process memory**, but for Neo4j this should be enhanced to monitor:
|
||||
|
||||
### 1. Query Latency-Based Throttling (Currently Implemented)
|
||||
The Neo4j monitor already tracks query latency via `RecordQueryLatency()` and `RecordWriteLatency()`, using EMA smoothing. Latency > 500ms increases reported load.
|
||||
|
||||
### 2. Connection Pool Saturation (Currently Implemented)
|
||||
The `querySem` semaphore limits concurrent queries (default 10). When full, the load metric increases.
|
||||
|
||||
### 3. Future Enhancement: Container Metrics
|
||||
Consider monitoring Neo4j container metrics via:
|
||||
- Docker stats API for memory/CPU
|
||||
- Neo4j metrics endpoint for transaction counts, cache hit rates
|
||||
- JMX metrics for heap usage and GC pressure
|
||||
|
||||
## Conclusion
|
||||
|
||||
The Neo4j import test demonstrated:
|
||||
|
||||
1. **Stable Memory Usage**: Neo4j maintained consistent 1.6GB memory throughout
|
||||
2. **Consistent Throughput**: ~40 social events/second with no degradation
|
||||
3. **Architectural Isolation**: Relay stays lightweight while Neo4j handles data
|
||||
4. **Rate Limiter Design**: Current RSS-based limiting is appropriate for Badger but less relevant for Neo4j
|
||||
|
||||
**Recommendation:** The Neo4j rate limiter is correctly implemented but relies on latency and concurrency metrics rather than memory pressure. For production deployments with Neo4j, configure appropriate Neo4j memory limits in the container (heap_initial, heap_max, pagecache) rather than relying on relay-side rate limiting.
|
||||
|
||||
## Test Environment
|
||||
|
||||
- **OS:** Linux 6.8.0-87-generic
|
||||
- **Architecture:** x86_64
|
||||
- **Go Version:** 1.25.3
|
||||
- **Neo4j Version:** 5.26.18 (community)
|
||||
- **Container:** Docker with 14.91GB limit
|
||||
- **Neo4j Settings:**
|
||||
- Heap Initial: 512MB
|
||||
- Heap Max: 1GB
|
||||
- Page Cache: 512MB
|
||||
2
go.mod
2
go.mod
@@ -3,7 +3,7 @@ module next.orly.dev
|
||||
go 1.25.3
|
||||
|
||||
require (
|
||||
git.mleku.dev/mleku/nostr v1.0.8
|
||||
git.mleku.dev/mleku/nostr v1.0.9
|
||||
github.com/adrg/xdg v0.5.3
|
||||
github.com/aperturerobotics/go-indexeddb v0.2.3
|
||||
github.com/dgraph-io/badger/v4 v4.8.0
|
||||
|
||||
4
go.sum
4
go.sum
@@ -1,5 +1,5 @@
|
||||
git.mleku.dev/mleku/nostr v1.0.8 h1:YYREdIxobEqYkzxQ7/5ALACPzLkiHW+CTira+VvSQZk=
|
||||
git.mleku.dev/mleku/nostr v1.0.8/go.mod h1:iYTlg2WKJXJ0kcsM6QBGOJ0UDiJidMgL/i64cHyPjZc=
|
||||
git.mleku.dev/mleku/nostr v1.0.9 h1:aiN0ihnXzEpboXjW4u8qr5XokLQqg4P0XSZ1Y273qM0=
|
||||
git.mleku.dev/mleku/nostr v1.0.9/go.mod h1:iYTlg2WKJXJ0kcsM6QBGOJ0UDiJidMgL/i64cHyPjZc=
|
||||
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
|
||||
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/ImVexed/fasturl v0.0.0-20230304231329-4e41488060f3 h1:ClzzXMDDuUbWfNNZqGeYq4PnYOlwlOVIvSyNaIy0ykg=
|
||||
|
||||
@@ -96,7 +96,7 @@ func TestBugReproduction_WithPolicyManager(t *testing.T) {
|
||||
|
||||
// Create policy with manager (enabled)
|
||||
ctx := context.Background()
|
||||
policy := NewWithManager(ctx, "ORLY", true)
|
||||
policy := NewWithManager(ctx, "ORLY", true, "")
|
||||
|
||||
// Load policy from file
|
||||
if err := policy.LoadFromFile(policyPath); err != nil {
|
||||
|
||||
@@ -31,7 +31,7 @@ func setupTestPolicy(t *testing.T, appName string) (*P, func()) {
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
|
||||
policy := NewWithManager(ctx, appName, true)
|
||||
policy := NewWithManager(ctx, appName, true, "")
|
||||
if policy == nil {
|
||||
cancel()
|
||||
os.RemoveAll(configDir)
|
||||
|
||||
@@ -29,7 +29,7 @@ func setupHotreloadTestPolicy(t *testing.T, appName string) (*P, func()) {
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
|
||||
policy := NewWithManager(ctx, appName, true)
|
||||
policy := NewWithManager(ctx, appName, true, "")
|
||||
if policy == nil {
|
||||
cancel()
|
||||
os.RemoveAll(configDir)
|
||||
|
||||
@@ -514,12 +514,19 @@ type PolicyManager struct {
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
configDir string
|
||||
configPath string // Path to policy.json file
|
||||
scriptPath string // Default script path for backward compatibility
|
||||
enabled bool
|
||||
mutex sync.RWMutex
|
||||
runners map[string]*ScriptRunner // Map of script path -> runner
|
||||
}
|
||||
|
||||
// ConfigPath returns the path to the policy configuration file.
|
||||
// This is used by hot-reload handlers to know where to save updated policy.
|
||||
func (pm *PolicyManager) ConfigPath() string {
|
||||
return pm.configPath
|
||||
}
|
||||
|
||||
// P represents a complete policy configuration for a Nostr relay.
|
||||
// It defines access control rules, kind filtering, and default behavior.
|
||||
// Policies are evaluated in order: global rules, kind filtering, specific rules, then default policy.
|
||||
@@ -695,6 +702,15 @@ func (p *P) IsEnabled() bool {
|
||||
return p != nil && p.manager != nil && p.manager.IsEnabled()
|
||||
}
|
||||
|
||||
// ConfigPath returns the path to the policy configuration file.
|
||||
// Delegates to the internal PolicyManager.
|
||||
func (p *P) ConfigPath() string {
|
||||
if p == nil || p.manager == nil {
|
||||
return ""
|
||||
}
|
||||
return p.manager.ConfigPath()
|
||||
}
|
||||
|
||||
// getDefaultPolicyAction returns true if the default policy is "allow", false if "deny"
|
||||
func (p *P) getDefaultPolicyAction() (allowed bool) {
|
||||
switch p.DefaultPolicy {
|
||||
@@ -711,10 +727,29 @@ func (p *P) getDefaultPolicyAction() (allowed bool) {
|
||||
// NewWithManager creates a new policy with a policy manager for script execution.
|
||||
// It initializes the policy manager, loads configuration from files, and starts
|
||||
// background processes for script management and periodic health checks.
|
||||
func NewWithManager(ctx context.Context, appName string, enabled bool) *P {
|
||||
//
|
||||
// The customPolicyPath parameter allows overriding the default policy file location.
|
||||
// If empty, uses the default path: $HOME/.config/{appName}/policy.json
|
||||
// If provided, it MUST be an absolute path (starting with /) or the function will panic.
|
||||
func NewWithManager(ctx context.Context, appName string, enabled bool, customPolicyPath string) *P {
|
||||
configDir := filepath.Join(xdg.ConfigHome, appName)
|
||||
scriptPath := filepath.Join(configDir, "policy.sh")
|
||||
configPath := filepath.Join(configDir, "policy.json")
|
||||
|
||||
// Determine the policy config path
|
||||
var configPath string
|
||||
if customPolicyPath != "" {
|
||||
// Validate that custom path is absolute
|
||||
if !filepath.IsAbs(customPolicyPath) {
|
||||
panic(fmt.Sprintf("FATAL: ORLY_POLICY_PATH must be an ABSOLUTE path (starting with /), got: %q", customPolicyPath))
|
||||
}
|
||||
configPath = customPolicyPath
|
||||
// Update configDir to match the custom path's directory for script resolution
|
||||
configDir = filepath.Dir(customPolicyPath)
|
||||
scriptPath = filepath.Join(configDir, "policy.sh")
|
||||
log.I.F("using custom policy path: %s", configPath)
|
||||
} else {
|
||||
configPath = filepath.Join(configDir, "policy.json")
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
|
||||
@@ -722,6 +757,7 @@ func NewWithManager(ctx context.Context, appName string, enabled bool) *P {
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
configDir: configDir,
|
||||
configPath: configPath,
|
||||
scriptPath: scriptPath,
|
||||
enabled: enabled,
|
||||
runners: make(map[string]*ScriptRunner),
|
||||
|
||||
@@ -825,7 +825,7 @@ func TestNewWithManager(t *testing.T) {
|
||||
// Test with disabled policy (doesn't require policy.json file)
|
||||
t.Run("disabled policy", func(t *testing.T) {
|
||||
enabled := false
|
||||
policy := NewWithManager(ctx, appName, enabled)
|
||||
policy := NewWithManager(ctx, appName, enabled, "")
|
||||
|
||||
if policy == nil {
|
||||
t.Fatal("Expected policy but got nil")
|
||||
|
||||
@@ -31,7 +31,7 @@ func setupTagValidationTestPolicy(t *testing.T, appName string) (*P, func()) {
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
|
||||
policy := NewWithManager(ctx, appName, true)
|
||||
policy := NewWithManager(ctx, appName, true, "")
|
||||
if policy == nil {
|
||||
cancel()
|
||||
os.RemoveAll(configDir)
|
||||
|
||||
@@ -69,8 +69,11 @@ func (c *NIP11Cache) Get(ctx context.Context, relayURL string) (*relayinfo.T, er
|
||||
|
||||
// fetchNIP11 fetches relay information document from a given URL
|
||||
func (c *NIP11Cache) fetchNIP11(ctx context.Context, relayURL string) (*relayinfo.T, error) {
|
||||
// Construct NIP-11 URL
|
||||
// Convert WebSocket URL to HTTP URL for NIP-11 fetch
|
||||
// wss:// -> https://, ws:// -> http://
|
||||
nip11URL := relayURL
|
||||
nip11URL = strings.Replace(nip11URL, "wss://", "https://", 1)
|
||||
nip11URL = strings.Replace(nip11URL, "ws://", "http://", 1)
|
||||
if !strings.HasSuffix(nip11URL, "/") {
|
||||
nip11URL += "/"
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
v0.35.0
|
||||
v0.35.3
|
||||
Reference in New Issue
Block a user