feat: resource caps, Saved Messages, author walls, docs for node bring-up
Node flags (cmd/node/main.go):
--max-cpu / --max-ram-mb — Go runtime caps (GOMAXPROCS / GOMEMLIMIT)
--feed-disk-limit-mb — hard 507 refusal for new post bodies over quota
--chain-disk-limit-mb — advisory watcher (can't reject blocks without
breaking consensus; logs WARN every minute)
Client — Saved Messages (self-chat):
- Auto-created on sign-in, pinned top of chat list, blue bookmark avatar
- Send short-circuits the relay (no encrypt, no fee, no mailbox hop)
- Empty state rendered outside inverted FlatList — fixes the mirrored
"say hi…" on Android RTL-aware layout builds
- PostCard shows "You" for own posts instead of the self-contact alias
Client — user walls:
- New route /(app)/feed/author/[pub] with infinite-scroll via
`created_at` cursor and pull-to-refresh
- Profile screen gains "View posts" button (universal) next to
"Open chat" (contact-only)
Feed pipeline:
- Bump client JPEG quality 0.5 → 0.75 to match server scrubber (Q=75),
so a 60 KiB compose doesn't balloon past 256 KiB after server re-encode
- ErrPostTooLarge now wraps with the actual size vs cap, errors.Is
preserved in the HTTP layer
- FeedMailbox quota + DiskUsage surface — supports new CLI flag
README:
- Step-by-step "first node / joiner" section on the landing page,
full flag tables incl. the new resource-cap group, minimal
checklists for open/private/low-end deployments
This commit is contained in:
@@ -29,6 +29,8 @@ import (
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
@@ -114,6 +116,16 @@ func main() {
|
||||
// only for intentional migrations (e.g. importing data from another chain
|
||||
// into this network) — very dangerous.
|
||||
allowGenesisMismatch := flag.Bool("allow-genesis-mismatch", false, "skip the safety check that aborts when the local genesis hash differs from the seed's. Use only for explicit chain migration.")
|
||||
// ── Resource caps ───────────────────────────────────────────────────────
|
||||
// All four accept 0 meaning "no limit". Enforcement model:
|
||||
// * CPU — runtime.GOMAXPROCS(n): Go runtime won't use more than n OS threads for Go code.
|
||||
// * RAM — debug.SetMemoryLimit: soft limit, the GC works harder as the heap approaches it.
|
||||
// * Feed disk — hard refuse of new post bodies once the cap is crossed (existing posts keep serving).
|
||||
// * Chain disk — warn-only periodic check; we can't hard-reject new blocks without breaking consensus.
|
||||
maxCPU := flag.Int("max-cpu", int(envUint64Or("DCHAIN_MAX_CPU", 0)), "max CPU cores the node may use (GOMAXPROCS). 0 = all (env: DCHAIN_MAX_CPU)")
|
||||
maxRAMMB := flag.Uint64("max-ram-mb", envUint64Or("DCHAIN_MAX_RAM_MB", 0), "soft Go heap limit in MiB (GOMEMLIMIT). 0 = unlimited (env: DCHAIN_MAX_RAM_MB)")
|
||||
feedDiskMB := flag.Uint64("feed-disk-limit-mb", envUint64Or("DCHAIN_FEED_DISK_LIMIT_MB", 0), "disk quota for post bodies in MiB; new posts are refused with 507 once crossed. 0 = unlimited (env: DCHAIN_FEED_DISK_LIMIT_MB)")
|
||||
chainDiskMB := flag.Uint64("chain-disk-limit-mb", envUint64Or("DCHAIN_CHAIN_DISK_LIMIT_MB", 0), "advisory disk cap for the chain DB dir in MiB; exceeding it logs a loud WARN every minute. 0 = unlimited (env: DCHAIN_CHAIN_DISK_LIMIT_MB)")
|
||||
showVersion := flag.Bool("version", false, "print version info and exit")
|
||||
flag.Parse()
|
||||
|
||||
@@ -128,6 +140,10 @@ func main() {
|
||||
// so subsequent logs inherit the format.
|
||||
setupLogging(*logFormat)
|
||||
|
||||
// Apply CPU / RAM caps before anything else spins up so the runtime
|
||||
// picks them up at first goroutine/heap allocation.
|
||||
applyResourceCaps(*maxCPU, *maxRAMMB)
|
||||
|
||||
// Wire API access-control. A non-empty token gates writes; adding
|
||||
// --api-private also gates reads. Logged up-front so the operator
|
||||
// sees what mode they're in.
|
||||
@@ -641,12 +657,24 @@ func main() {
|
||||
|
||||
// --- Feed mailbox (social-feed post bodies, v2.0.0) ---
|
||||
feedTTL := time.Duration(*feedTTLDays) * 24 * time.Hour
|
||||
feedMailbox, err := relay.OpenFeedMailbox(*feedDB, feedTTL)
|
||||
feedQuotaBytes := int64(*feedDiskMB) * 1024 * 1024
|
||||
feedMailbox, err := relay.OpenFeedMailbox(*feedDB, feedTTL, feedQuotaBytes)
|
||||
if err != nil {
|
||||
log.Fatalf("[NODE] feed mailbox: %v", err)
|
||||
}
|
||||
defer feedMailbox.Close()
|
||||
log.Printf("[NODE] feed mailbox: %s (TTL %d days)", *feedDB, *feedTTLDays)
|
||||
if feedQuotaBytes > 0 {
|
||||
log.Printf("[NODE] feed mailbox: %s (TTL %d days, disk quota %d MiB)", *feedDB, *feedTTLDays, *feedDiskMB)
|
||||
} else {
|
||||
log.Printf("[NODE] feed mailbox: %s (TTL %d days, no disk quota)", *feedDB, *feedTTLDays)
|
||||
}
|
||||
|
||||
// Advisory chain-disk watcher. We can't refuse new blocks (consensus
|
||||
// would stall), so instead we walk the chain DB dir every minute and
|
||||
// log a loud WARN if the operator's budget is exceeded. Zero = disabled.
|
||||
if *chainDiskMB > 0 {
|
||||
go watchChainDisk(*dbPath, int64(*chainDiskMB)*1024*1024)
|
||||
}
|
||||
|
||||
// Push-notify bus consumers whenever a fresh envelope lands in the
|
||||
// mailbox. Clients subscribed to `inbox:<my_x25519>` (via WS) get the
|
||||
@@ -1472,6 +1500,61 @@ func shortKeys(keys []string) []string {
|
||||
// "text" (default) is handler-default human-readable format, same as bare
|
||||
// log.Printf. "json" emits one JSON object per line with `time/level/msg`
|
||||
// + any key=value attrs — what Loki/ELK ingest natively.
|
||||
// applyResourceCaps wires the --max-cpu and --max-ram-mb flags into the Go
|
||||
// runtime. Both are soft-ish: CPU clamps GOMAXPROCS (Go scheduler won't use
|
||||
// more OS threads for Go code, though blocking syscalls can still spawn
|
||||
// more); RAM sets GOMEMLIMIT (the GC tightens its collection schedule as
|
||||
// the heap approaches the cap but cannot *force* a kernel OOM-free). Use
|
||||
// container limits (cgroup / Docker --memory / --cpus) alongside these
|
||||
// for a real ceiling — this is "please play nice", not "hard sandbox".
|
||||
func applyResourceCaps(maxCPU int, maxRAMMB uint64) {
|
||||
if maxCPU > 0 {
|
||||
prev := runtime.GOMAXPROCS(maxCPU)
|
||||
log.Printf("[NODE] CPU cap: GOMAXPROCS %d → %d", prev, maxCPU)
|
||||
}
|
||||
if maxRAMMB > 0 {
|
||||
bytes := int64(maxRAMMB) * 1024 * 1024
|
||||
debug.SetMemoryLimit(bytes)
|
||||
log.Printf("[NODE] RAM cap: GOMEMLIMIT = %d MiB (soft, GC-enforced)", maxRAMMB)
|
||||
}
|
||||
}
|
||||
|
||||
// watchChainDisk periodically walks the chain BadgerDB directory and logs
|
||||
// a WARN line whenever its size exceeds `limitBytes`. Runs forever — the
|
||||
// process lifetime bounds it. We deliberately do *not* stop block
|
||||
// production when the cap is crossed: a validator that refuses to apply
|
||||
// blocks stalls consensus for everyone on the chain, which is worse than
|
||||
// using more disk than the operator wanted. Treat this as a monitoring
|
||||
// signal, e.g. feed it to Prometheus via an alertmanager scrape.
|
||||
func watchChainDisk(dir string, limitBytes int64) {
|
||||
tick := time.NewTicker(60 * time.Second)
|
||||
defer tick.Stop()
|
||||
for ; ; <-tick.C {
|
||||
used := dirSize(dir)
|
||||
if used > limitBytes {
|
||||
log.Printf("[NODE] WARN chain disk over quota: %d MiB used > %d MiB limit at %s",
|
||||
used>>20, limitBytes>>20, dir)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// dirSize returns the total byte size of all regular files under root,
|
||||
// recursively. Errors on individual entries are ignored — this is an
|
||||
// advisory metric, not a filesystem audit.
|
||||
func dirSize(root string) int64 {
|
||||
var total int64
|
||||
_ = filepath.Walk(root, func(_ string, info os.FileInfo, err error) error {
|
||||
if err != nil || info == nil {
|
||||
return nil
|
||||
}
|
||||
if !info.IsDir() {
|
||||
total += info.Size()
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return total
|
||||
}
|
||||
|
||||
func setupLogging(format string) {
|
||||
var handler slog.Handler
|
||||
switch strings.ToLower(format) {
|
||||
|
||||
Reference in New Issue
Block a user