RegionIO/cmd/regionio/main.go
Master290 65a7445a78 LRU chunk eviction (bounded cache, default 1024 chunks / ~200MB)
The in-memory chunk+frame cache no longer grows unbounded as players
explore. An LRU policy (doubly-linked list + index map, O(1) per op)
evicts least-recently-used chunks when the cache exceeds MaxCachedChunks,
dropping both the chunk and its cached frame. Dirty chunks are skipped
until the autosave flushes them, so no edit is ever lost to eviction.

- world/cache.go: maxChunks field + order/index LRU bookkeeping; touch
  (move-to-front) on every chunkAt/Frame/SetBlock hit; evictIfNeeded on
  miss; NewCacheWithLimit constructor (0 = unbounded, backward-compat).
  Dirty chunks are bumped to MRU and left in place rather than doing
  region I/O under the cache mutex.
- server/server.go: Config.MaxCachedChunks (default 1024); New wires it
  into NewCacheWithLimit when a world dir is set.
- cmd/regionio/main.go: -maxcache flag.
- Tests: limit cap, LRU ordering (touched chunk survives), both-maps
  drop, dirty-keep, reload-on-access, and edits-survive-eviction+reload
  (end-to-end via the store).
2026-06-25 13:56:06 +03:00

79 lines
2.6 KiB
Go

// Command regionio starts a RegionIO Minecraft server core.
package main
import (
"context"
"flag"
"log/slog"
"os"
"os/signal"
"strconv"
"syscall"
"time"
"regionio/internal/network"
"regionio/internal/server"
)
func main() {
log := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelDebug,
}))
cfg := server.DefaultConfig()
// World seed: -seed flag takes precedence, then REGIONIO_SEED env, then the
// default (0). Accepted formats: decimal, or "0x" hex. An invalid value is
// fatal — a wrong seed silently generates a different world than intended.
seedFlag := flag.Int64("seed", parseSeedEnv(os.Getenv("REGIONIO_SEED"), cfg.WorldSeed, log),
"world seed (overrides REGIONIO_SEED)")
worldDir := flag.String("world", cfg.WorldDir, "world directory (empty = in-memory only)")
maxCache := flag.Int("maxcache", cfg.MaxCachedChunks, "max cached chunks, LRU eviction (0 = unbounded)")
flag.Parse()
cfg.WorldSeed = *seedFlag
cfg.WorldDir = *worldDir
cfg.MaxCachedChunks = *maxCache
log.Info("using world seed", "seed", cfg.WorldSeed, "worldDir", cfg.WorldDir, "maxcache", cfg.MaxCachedChunks)
srv, err := server.New(cfg)
if err != nil {
log.Error("failed to create server", "err", err)
os.Exit(1)
}
// Start the chunk autosave loop. It flushes dirty chunks every 30s and does
// a final SaveAll when the context (cancelled by signal) ends.
saveCtx, saveStop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
autosaveDone := srv.Chunks().StartAutosave(saveCtx, log, 30*time.Second)
ln := network.NewListener(srv, log)
// ListenAndServe blocks until the listener stops (on the same signals).
// The autosave context is separate so the final flush runs after the
// listener exits; stop it here to trigger that final SaveAll, then wait for
// the saver to finish before releasing the store file handles.
if err := ln.ListenAndServe(saveCtx); err != nil {
log.Error("server stopped", "err", err)
}
saveStop()
<-autosaveDone
if srv.Store() != nil {
srv.Store().Close()
}
}
// parseSeedEnv parses the REGIONIO_SEED env var. It returns fallback when the
// variable is empty, and logs+returns fallback when parsing fails (so a typo
// does not silently change the world).
func parseSeedEnv(raw string, fallback int64, log *slog.Logger) int64 {
if raw == "" {
return fallback
}
// strconv.ParseInt with base 0 handles decimal and "0x" hex prefixes.
v, err := strconv.ParseInt(raw, 0, 64)
if err != nil {
log.Error("invalid REGIONIO_SEED, falling back to default", "raw", raw, "err", err)
return fallback
}
return v
}