From 0dac41e35ead1f0b2fa542e6505273236229d387 Mon Sep 17 00:00:00 2001 From: woikos Date: Tue, 6 Jan 2026 15:37:06 +0100 Subject: [PATCH] Add documentation and improve BBolt import memory efficiency (v0.48.8) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add README.md table of contents for easier navigation - Add Curation ACL documentation section to README.md - Create detailed Curation Mode Guide (docs/CURATION_MODE_GUIDE.md) - Fix OOM during BBolt index building by closing temp file before build - Add GC calls before index building to reclaim batch buffer memory - Improve import-export.go with processJSONLEventsReturningCount - Add policy-aware import path for sync operations Files modified: - README.md: Added TOC and curation ACL documentation - docs/CURATION_MODE_GUIDE.md: New comprehensive curation mode guide - pkg/bbolt/import-export.go: Memory-safe import with deferred cleanup - pkg/bbolt/import-minimal.go: Added GC before index build - pkg/version/version: Bump to v0.48.8 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- README.md | 69 +++++- docs/CURATION_MODE_GUIDE.md | 411 ++++++++++++++++++++++++++++++++++++ pkg/bbolt/import-export.go | 126 +++++++++-- pkg/bbolt/import-minimal.go | 9 +- pkg/version/version | 2 +- 5 files changed, 598 insertions(+), 19 deletions(-) create mode 100644 docs/CURATION_MODE_GUIDE.md diff --git a/README.md b/README.md index 34defe4..472a97f 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,36 @@ zap me: �mlekudev@getalby.com follow me on [nostr](https://jumble.social/users/npub1fjqqy4a93z5zsjwsfxqhc2764kvykfdyttvldkkkdera8dr78vhsmmleku) +## Table of Contents + +- [Bug Reports & Feature Requests](#%EF%B8%8F-bug-reports--feature-requests) +- [System Requirements](#%EF%B8%8F-system-requirements) +- [About](#about) +- [Performance & Cryptography](#performance--cryptography) +- [Building](#building) + - [Prerequisites](#prerequisites) + - [Basic Build](#basic-build) + - [Building with Web UI](#building-with-web-ui) +- [Core Features](#core-features) + - [Web UI](#web-ui) + - [Sprocket Event Processing](#sprocket-event-processing) + - [Policy System](#policy-system) +- [Deployment](#deployment) + - [Automated Deployment](#automated-deployment) + - [TLS Configuration](#tls-configuration) + - [systemd Service Management](#systemd-service-management) + - [Remote Deployment](#remote-deployment) + - [Configuration](#configuration) + - [Firewall Configuration](#firewall-configuration) + - [Monitoring](#monitoring) +- [Testing](#testing) +- [Command-Line Tools](#command-line-tools) +- [Access Control](#access-control) + - [Follows ACL](#follows-acl) + - [Curation ACL](#curation-acl) + - [Cluster Replication](#cluster-replication) +- [Developer Notes](#developer-notes) + ## ⚠️ Bug Reports & Feature Requests **Bug reports and feature requests that do not follow the protocol will not be accepted.** @@ -566,9 +596,22 @@ go run ./cmd/subscription-test-simple -url ws://localhost:3334 -duration 120 ## Access Control +ORLY provides four ACL (Access Control List) modes to control who can publish events to your relay: + +| Mode | Description | Best For | +|------|-------------|----------| +| `none` | Open relay, anyone can write | Public relays | +| `follows` | Write access based on admin follow lists | Personal/community relays | +| `managed` | Explicit allow/deny lists via NIP-86 API | Private relays | +| `curating` | Three-tier classification with rate limiting | Curated community relays | + +```bash +export ORLY_ACL_MODE=follows # or: none, managed, curating +``` + ### Follows ACL -The follows ACL (Access Control List) system provides flexible relay access control based on social relationships in the Nostr network. +The follows ACL system provides flexible relay access control based on social relationships in the Nostr network. ```bash export ORLY_ACL_MODE=follows @@ -578,6 +621,30 @@ export ORLY_ADMINS=npub1fjqqy4a93z5zsjwsfxqhc2764kvykfdyttvldkkkdera8dr78vhsmmle The system grants write access to users followed by designated admins, with read-only access for others. Follow lists update dynamically as admins modify their relationships. +### Curation ACL + +The curation ACL mode provides sophisticated content curation with a three-tier publisher classification system: + +- **Trusted**: Unlimited publishing, bypass rate limits +- **Blacklisted**: Blocked from publishing, invisible to regular users +- **Unclassified**: Rate-limited publishing (default 50 events/day) + +Key features: +- **Kind whitelisting**: Only allow specific event kinds (e.g., social, DMs, longform) +- **IP-based flood protection**: Auto-ban IPs that exceed rate limits +- **Spam flagging**: Mark events as spam without deleting +- **Web UI management**: Configure via the built-in curation interface + +```bash +export ORLY_ACL_MODE=curating +export ORLY_OWNERS=npub1your_owner_key +./orly +``` + +After starting, publish a configuration event (kind 30078) to enable the relay. The web UI at `/#curation` provides a complete management interface. + +For detailed configuration and API documentation, see the [Curation Mode Guide](docs/CURATION_MODE_GUIDE.md). + ### Cluster Replication ORLY supports distributed relay clusters using active replication. When configured with peer relays, ORLY will automatically synchronize events between cluster members using efficient HTTP polling. diff --git a/docs/CURATION_MODE_GUIDE.md b/docs/CURATION_MODE_GUIDE.md new file mode 100644 index 0000000..ac1d846 --- /dev/null +++ b/docs/CURATION_MODE_GUIDE.md @@ -0,0 +1,411 @@ +# Curation Mode Guide + +Curation mode is a sophisticated access control system for Nostr relays that provides three-tier publisher classification, rate limiting, IP-based flood protection, and event kind whitelisting. + +## Overview + +Unlike simple allow/deny lists, curation mode classifies publishers into three tiers: + +| Tier | Rate Limited | Daily Limit | Visibility | +|------|--------------|-------------|------------| +| **Trusted** | No | Unlimited | Full | +| **Blacklisted** | N/A (blocked) | 0 | Hidden from regular users | +| **Unclassified** | Yes | 50 events/day (default) | Full | + +This allows relay operators to: +- Reward quality contributors with unlimited publishing +- Block bad actors while preserving their events for admin review +- Allow new users to participate with reasonable rate limits +- Prevent spam floods through automatic IP-based protections + +## Quick Start + +### 1. Start the Relay + +```bash +export ORLY_ACL_MODE=curating +export ORLY_OWNERS=npub1your_owner_pubkey +./orly +``` + +### 2. Publish Configuration + +The relay will not accept events until you publish a configuration event. Use the web UI at `http://your-relay/#curation` or publish a kind 30078 event: + +```json +{ + "kind": 30078, + "tags": [["d", "curating-config"]], + "content": "{\"dailyLimit\":50,\"ipDailyLimit\":500,\"firstBanHours\":1,\"secondBanHours\":168,\"kindCategories\":[\"social\"]}" +} +``` + +### 3. Manage Publishers + +Use the web UI or NIP-86 API to: +- Trust quality publishers +- Blacklist spammers +- Review unclassified users by activity +- Unblock IPs if needed + +## Configuration + +### Environment Variables + +| Variable | Default | Description | +|----------|---------|-------------| +| `ORLY_ACL_MODE` | `none` | Set to `curating` to enable | +| `ORLY_OWNERS` | | Owner pubkeys (can configure relay) | +| `ORLY_ADMINS` | | Admin pubkeys (can manage publishers) | + +### Configuration Event (Kind 30078) + +Configuration is stored as a replaceable Nostr event (kind 30078) with d-tag `curating-config`. Only owners and admins can publish configuration. + +```typescript +interface CuratingConfig { + // Rate Limiting + dailyLimit: number; // Max events/day for unclassified users (default: 50) + ipDailyLimit: number; // Max events/day from single IP (default: 500) + + // IP Ban Durations + firstBanHours: number; // First offense ban duration (default: 1 hour) + secondBanHours: number; // Subsequent offense ban duration (default: 168 hours / 1 week) + + // Kind Filtering (choose one or combine) + allowedKinds: number[]; // Explicit kind numbers: [0, 1, 3, 7] + allowedRanges: string[]; // Kind ranges: ["1000-1999", "30000-39999"] + kindCategories: string[]; // Pre-defined categories: ["social", "dm"] +} +``` + +### Event Kind Categories + +Pre-defined categories for convenient kind whitelisting: + +| Category | Kinds | Description | +|----------|-------|-------------| +| `social` | 0, 1, 3, 6, 7, 10002 | Profiles, notes, contacts, reposts, reactions | +| `dm` | 4, 14, 1059 | Direct messages (NIP-04, NIP-17, gift wraps) | +| `longform` | 30023, 30024 | Long-form articles and drafts | +| `media` | 1063, 20, 21, 22 | File metadata, picture/video/audio events | +| `marketplace` | 30017-30020, 1021, 1022 | Products, stalls, auctions, bids | +| `groups_nip29` | 9-12, 9000-9002, 39000-39002 | NIP-29 relay-based groups | +| `groups_nip72` | 34550, 1111, 4550 | NIP-72 moderated communities | +| `lists` | 10000, 10001, 10003, 30000, 30001, 30003 | Mute, pin, bookmark lists | + +Example configuration allowing social interactions and DMs: + +```json +{ + "kindCategories": ["social", "dm"], + "dailyLimit": 100, + "ipDailyLimit": 1000 +} +``` + +## Three-Tier Classification + +### Trusted Publishers + +Trusted publishers have unlimited publishing rights: +- Bypass all rate limiting +- Can publish any allowed kind +- Events always visible to all users + +**Use case**: Known quality contributors, verified community members, partner relays. + +### Blacklisted Publishers + +Blacklisted publishers are blocked from publishing: +- All events rejected with `"pubkey is blacklisted"` error +- Existing events become invisible to regular users +- Admins and owners can still see blacklisted events (for review) + +**Use case**: Spammers, abusive users, bad actors. + +### Unclassified Publishers + +Everyone else falls into the unclassified tier: +- Subject to daily event limit (default: 50 events/day) +- Subject to IP-based flood protection +- Events visible to all users +- Can be promoted to trusted or demoted to blacklisted + +**Use case**: New users, general public. + +## Rate Limiting & Flood Protection + +### Per-Pubkey Limits + +Unclassified publishers are limited to a configurable number of events per day (default: 50). The count resets at midnight UTC. + +When a user exceeds their limit: +1. Event is rejected with `"daily event limit exceeded"` error +2. Their IP is flagged for potential abuse + +### Per-IP Limits + +To prevent Sybil attacks (creating many pubkeys from one IP), there's also an IP-based daily limit (default: 500 events). + +When an IP exceeds its limit: +1. All events from that IP are rejected +2. The IP is temporarily banned + +### Automatic IP Banning + +When rate limits are exceeded: + +| Offense | Ban Duration | Description | +|---------|--------------|-------------| +| First | 1 hour | Quick timeout for accidental over-posting | +| Second+ | 1 week | Extended ban for repeated abuse | + +Ban durations are configurable via `firstBanHours` and `secondBanHours`. + +### Offense Tracking + +The system tracks which pubkeys triggered rate limits from each IP: + +``` +IP 192.168.1.100: + - npub1abc... exceeded limit at 2024-01-15 10:30:00 + - npub1xyz... exceeded limit at 2024-01-15 10:45:00 + Offense count: 2 + Status: Banned until 2024-01-22 10:45:00 +``` + +This helps identify coordinated spam attacks. + +## Spam Flagging + +Events can be flagged as spam without deletion: + +- Flagged events are hidden from regular users +- Admins can review flagged events +- Events can be unflagged if incorrectly marked +- Original event data is preserved + +This is useful for: +- Moderation review queues +- Training spam detection systems +- Preserving evidence of abuse + +## NIP-86 Management API + +All management operations use NIP-98 HTTP authentication. + +### Trust Management + +```bash +# Trust a pubkey +curl -X POST https://relay.example.com \ + -H "Authorization: Nostr " \ + -d '{"method":"trustpubkey","params":[""]}' + +# Untrust a pubkey +curl -X POST https://relay.example.com \ + -H "Authorization: Nostr " \ + -d '{"method":"untrustpubkey","params":[""]}' + +# List trusted pubkeys +curl -X POST https://relay.example.com \ + -H "Authorization: Nostr " \ + -d '{"method":"listtrustedpubkeys","params":[]}' +``` + +### Blacklist Management + +```bash +# Blacklist a pubkey +curl -X POST https://relay.example.com \ + -H "Authorization: Nostr " \ + -d '{"method":"blacklistpubkey","params":[""]}' + +# Remove from blacklist +curl -X POST https://relay.example.com \ + -H "Authorization: Nostr " \ + -d '{"method":"unblacklistpubkey","params":[""]}' + +# List blacklisted pubkeys +curl -X POST https://relay.example.com \ + -H "Authorization: Nostr " \ + -d '{"method":"listblacklistedpubkeys","params":[]}' +``` + +### Unclassified User Management + +```bash +# List unclassified users sorted by event count +curl -X POST https://relay.example.com \ + -H "Authorization: Nostr " \ + -d '{"method":"listunclassifiedusers","params":[]}' +``` + +Response includes pubkey, event count, and last activity for each user. + +### Spam Management + +```bash +# Mark event as spam +curl -X POST https://relay.example.com \ + -H "Authorization: Nostr " \ + -d '{"method":"markspam","params":[""]}' + +# Unmark spam +curl -X POST https://relay.example.com \ + -H "Authorization: Nostr " \ + -d '{"method":"unmarkspam","params":[""]}' + +# List spam events +curl -X POST https://relay.example.com \ + -H "Authorization: Nostr " \ + -d '{"method":"listspamevents","params":[]}' +``` + +### IP Block Management + +```bash +# List blocked IPs +curl -X POST https://relay.example.com \ + -H "Authorization: Nostr " \ + -d '{"method":"listblockedips","params":[]}' + +# Unblock an IP +curl -X POST https://relay.example.com \ + -H "Authorization: Nostr " \ + -d '{"method":"unblockip","params":[""]}' +``` + +### Configuration Management + +```bash +# Get current configuration +curl -X POST https://relay.example.com \ + -H "Authorization: Nostr " \ + -d '{"method":"getcuratingconfig","params":[]}' + +# Set allowed kind categories +curl -X POST https://relay.example.com \ + -H "Authorization: Nostr " \ + -d '{"method":"setallowedkindcategories","params":[["social","dm","longform"]]}' + +# Get allowed kind categories +curl -X POST https://relay.example.com \ + -H "Authorization: Nostr " \ + -d '{"method":"getallowedkindcategories","params":[]}' +``` + +## Web UI + +The curation web UI is available at `/#curation` and provides: + +- **Configuration Panel**: Set rate limits, ban durations, and allowed kinds +- **Publisher Management**: Trust/blacklist pubkeys with one click +- **Unclassified Users**: View users sorted by activity, promote or blacklist +- **IP Blocks**: View and unblock banned IP addresses +- **Spam Queue**: Review flagged events, confirm or unflag + +## Database Storage + +Curation data is stored in the relay database with the following key prefixes: + +| Prefix | Purpose | +|--------|---------| +| `CURATING_ACL_CONFIG` | Current configuration | +| `CURATING_ACL_TRUSTED_PUBKEY_{pubkey}` | Trusted publisher list | +| `CURATING_ACL_BLACKLISTED_PUBKEY_{pubkey}` | Blacklisted publisher list | +| `CURATING_ACL_EVENT_COUNT_{pubkey}_{date}` | Daily event counts per pubkey | +| `CURATING_ACL_IP_EVENT_COUNT_{ip}_{date}` | Daily event counts per IP | +| `CURATING_ACL_IP_OFFENSE_{ip}` | Offense tracking per IP | +| `CURATING_ACL_BLOCKED_IP_{ip}` | Active IP blocks | +| `CURATING_ACL_SPAM_EVENT_{eventID}` | Spam-flagged events | + +## Caching + +For performance, the following data is cached in memory: + +- Trusted pubkey set +- Blacklisted pubkey set +- Allowed kinds set +- Current configuration + +Caches are refreshed every hour by the background cleanup goroutine. + +## Background Maintenance + +A background goroutine runs hourly to: + +1. Remove expired IP blocks +2. Clean up old event count entries (older than 2 days) +3. Refresh in-memory caches +4. Log maintenance statistics + +## Best Practices + +### Starting a New Curated Relay + +1. Start with permissive settings: + ```json + {"dailyLimit": 100, "ipDailyLimit": 1000, "kindCategories": ["social"]} + ``` + +2. Monitor unclassified users for a few days + +3. Trust active, quality contributors + +4. Blacklist obvious spammers + +5. Adjust rate limits based on observed patterns + +### Handling Spam Waves + +During spam attacks: + +1. The IP-based flood protection will auto-ban attack sources +2. Review blocked IPs via web UI or API +3. Blacklist any pubkeys that got through +4. Consider temporarily lowering `ipDailyLimit` + +### Recovering from Mistakes + +- **Accidentally blacklisted someone**: Use `unblacklistpubkey` - their events become visible again +- **Wrongly flagged spam**: Use `unmarkspam` - event becomes visible again +- **Blocked legitimate IP**: Use `unblockip` - IP can publish again immediately + +## Comparison with Other ACL Modes + +| Feature | None | Follows | Managed | Curating | +|---------|------|---------|---------|----------| +| Default Access | Write | Write if followed | Explicit allow | Rate-limited | +| Rate Limiting | No | No | No | Yes | +| Kind Filtering | No | No | Optional | Yes | +| IP Protection | No | No | No | Yes | +| Spam Flagging | No | No | No | Yes | +| Configuration | Env vars | Follow lists | NIP-86 | Kind 30078 events | +| Web UI | Basic | Basic | Basic | Full curation panel | + +## Troubleshooting + +### "Relay not accepting events" + +The relay requires a configuration event before accepting any events. Publish a kind 30078 event with d-tag `curating-config`. + +### "daily event limit exceeded" + +The user has exceeded their daily limit. Options: +1. Wait until midnight UTC for reset +2. Trust the pubkey if they're a quality contributor +3. Increase `dailyLimit` in configuration + +### "pubkey is blacklisted" + +The pubkey is on the blacklist. Use `unblacklistpubkey` if this was a mistake. + +### "IP is blocked" + +The IP has been auto-banned due to rate limit violations. Use `unblockip` if legitimate, or wait for the ban to expire. + +### Events disappearing for users + +Check if the event author has been blacklisted. Blacklisted authors' events are hidden from regular users but visible to admins. diff --git a/pkg/bbolt/import-export.go b/pkg/bbolt/import-export.go index a3e1eaa..6405cc4 100644 --- a/pkg/bbolt/import-export.go +++ b/pkg/bbolt/import-export.go @@ -31,9 +31,10 @@ func (b *B) ImportEventsFromReader(ctx context.Context, rr io.Reader) error { if chk.E(err) { return err } - defer os.Remove(tmp.Name()) // Clean up temp file when done + tmpName := tmp.Name() + defer os.Remove(tmpName) // Clean up temp file when done - log.I.F("bbolt import: buffering upload to %s", tmp.Name()) + log.I.F("bbolt import: buffering upload to %s", tmpName) bufferStart := time.Now() bytesBuffered, err := io.Copy(tmp, rr) if chk.E(err) { @@ -48,12 +49,30 @@ func (b *B) ImportEventsFromReader(ctx context.Context, rr io.Reader) error { return err } - processErr := b.processJSONLEvents(ctx, tmp) + count, processErr := b.processJSONLEventsReturningCount(ctx, tmp) + + // Close temp file to release resources before index building + tmp.Close() + + if processErr != nil { + return processErr + } + + // Build indexes after events are stored (minimal import mode) + if count > 0 { + // Force garbage collection to reclaim memory before index building + debug.FreeOSMemory() + log.I.F("bbolt import: building indexes for %d events...", count) + if err := b.BuildIndexes(ctx); err != nil { + log.E.F("bbolt import: failed to build indexes: %v", err) + return err + } + } totalElapsed := time.Since(startTime) log.I.F("bbolt import: total operation time: %v", totalElapsed.Round(time.Millisecond)) - return processErr + return nil } // ImportEventsFromStrings imports events from a slice of JSON strings with policy filtering @@ -67,7 +86,95 @@ func (b *B) ImportEventsFromStrings(ctx context.Context, eventJSONs []string, po // processJSONLEvents processes JSONL events from a reader func (b *B) processJSONLEvents(ctx context.Context, rr io.Reader) error { - return b.processJSONLEventsWithPolicy(ctx, rr, nil) + _, err := b.processJSONLEventsReturningCount(ctx, rr) + return err +} + +// processJSONLEventsReturningCount processes JSONL events and returns the count saved +// This is used by ImportEventsFromReader for migration mode (minimal import without inline indexes) +func (b *B) processJSONLEventsReturningCount(ctx context.Context, rr io.Reader) (int, error) { + // Create a scanner to read the buffer line by line + scan := bufio.NewScanner(rr) + scanBuf := make([]byte, maxLen) + scan.Buffer(scanBuf, maxLen) + + // Performance tracking + startTime := time.Now() + lastLogTime := startTime + const logInterval = 5 * time.Second + + var count, total, skipped, unmarshalErrors, saveErrors int + for scan.Scan() { + select { + case <-ctx.Done(): + log.I.F("bbolt import: context closed after %d events", count) + return count, ctx.Err() + default: + } + + line := scan.Bytes() + total += len(line) + 1 + if len(line) < 1 { + skipped++ + continue + } + + ev := event.New() + if _, err := ev.Unmarshal(line); err != nil { + ev.Free() + unmarshalErrors++ + log.W.F("bbolt import: failed to unmarshal event: %v", err) + continue + } + + // Minimal path for migration: store events only, indexes built later + if err := b.SaveEventMinimal(ev); err != nil { + ev.Free() + saveErrors++ + log.W.F("bbolt import: failed to save event: %v", err) + continue + } + + ev.Free() + line = nil + count++ + + // Progress logging every logInterval + if time.Since(lastLogTime) >= logInterval { + elapsed := time.Since(startTime) + eventsPerSec := float64(count) / elapsed.Seconds() + mbPerSec := float64(total) / elapsed.Seconds() / 1024 / 1024 + log.I.F("bbolt import: progress %d events saved, %.2f MB read, %.0f events/sec, %.2f MB/sec", + count, float64(total)/1024/1024, eventsPerSec, mbPerSec) + lastLogTime = time.Now() + debug.FreeOSMemory() + } + } + + // Flush any remaining batched events + if b.batcher != nil { + b.batcher.Flush() + } + + // Final summary + elapsed := time.Since(startTime) + eventsPerSec := float64(count) / elapsed.Seconds() + mbPerSec := float64(total) / elapsed.Seconds() / 1024 / 1024 + log.I.F("bbolt import: completed - %d events saved, %.2f MB in %v (%.0f events/sec, %.2f MB/sec)", + count, float64(total)/1024/1024, elapsed.Round(time.Millisecond), eventsPerSec, mbPerSec) + if unmarshalErrors > 0 || saveErrors > 0 || skipped > 0 { + log.I.F("bbolt import: stats - %d unmarshal errors, %d save errors, %d skipped empty lines", + unmarshalErrors, saveErrors, skipped) + } + + if err := scan.Err(); err != nil { + return count, err + } + + // Clear scanner buffer to help GC + scanBuf = nil + + return count, nil } // processJSONLEventsWithPolicy processes JSONL events from a reader with optional policy filtering @@ -181,15 +288,6 @@ func (b *B) processJSONLEventsWithPolicy(ctx context.Context, rr io.Reader, poli return err } - // Build indexes after minimal import (when no policy manager = migration mode) - if policyManager == nil && count > 0 { - log.I.F("bbolt import: building indexes for %d events...", count) - if err := b.BuildIndexes(ctx); err != nil { - log.E.F("bbolt import: failed to build indexes: %v", err) - return err - } - } - return nil } diff --git a/pkg/bbolt/import-minimal.go b/pkg/bbolt/import-minimal.go index 2f150c3..6b65d55 100644 --- a/pkg/bbolt/import-minimal.go +++ b/pkg/bbolt/import-minimal.go @@ -62,9 +62,12 @@ func (b *B) BuildIndexes(ctx context.Context) error { log.I.F("bbolt: starting index build...") startTime := time.Now() - // Process in chunks to avoid OOM - // With ~15 indexes per event and ~50 bytes per key, 200k events = ~150MB - const chunkSize = 200000 + // Force GC before starting to reclaim batch buffer memory + debug.FreeOSMemory() + + // Process in small chunks to avoid OOM on memory-constrained systems + // With ~15 indexes per event and ~50 bytes per key, 50k events = ~37.5MB per chunk + const chunkSize = 50000 var totalEvents int var lastSerial uint64 = 0 diff --git a/pkg/version/version b/pkg/version/version index 58743a3..a1869d2 100644 --- a/pkg/version/version +++ b/pkg/version/version @@ -1 +1 @@ -v0.48.6 +v0.48.8