Add Neo4j memory tuning config and query result limits (v0.43.0)
Some checks failed
Go / build-and-release (push) Has been cancelled
Some checks failed
Go / build-and-release (push) Has been cancelled
- Add Neo4j driver config options for memory management: - ORLY_NEO4J_MAX_CONN_POOL (default: 25) - connection pool size - ORLY_NEO4J_FETCH_SIZE (default: 1000) - records per batch - ORLY_NEO4J_MAX_TX_RETRY_SEC (default: 30) - transaction retry timeout - ORLY_NEO4J_QUERY_RESULT_LIMIT (default: 10000) - max results per query - Apply driver settings when creating Neo4j connection (pool size, fetch size, retry time) - Enforce query result limit as safety cap on all Cypher queries - Fix QueryForSerials and QueryForIds to preserve LIMIT clauses - Add comprehensive memory tuning documentation with sizing guidelines - Add NIP-46 signer-based authentication for bunker connections - Update go.mod with new dependencies Files modified: - app/config/config.go: Add Neo4j driver tuning config vars - main.go: Pass new config values to database factory - pkg/database/factory.go: Add Neo4j tuning fields to DatabaseConfig - pkg/database/factory_wasm.go: Mirror factory.go changes for WASM - pkg/neo4j/neo4j.go: Apply driver config, add getter methods - pkg/neo4j/query-events.go: Enforce query result limit, fix LIMIT preservation - docs/NEO4J_BACKEND.md: Add Memory Tuning section, update Docker example - CLAUDE.md: Add Neo4j memory tuning quick reference - app/handle-req.go: NIP-46 signer authentication - app/publisher.go: HasActiveNIP46Signer check - pkg/protocol/publish/publisher.go: NIP46SignerChecker interface - go.mod: Add dependencies 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -105,6 +105,12 @@ type C struct {
|
||||
Neo4jUser string `env:"ORLY_NEO4J_USER" default:"neo4j" usage:"Neo4j authentication username (only used when ORLY_DB_TYPE=neo4j)"`
|
||||
Neo4jPassword string `env:"ORLY_NEO4J_PASSWORD" default:"password" usage:"Neo4j authentication password (only used when ORLY_DB_TYPE=neo4j)"`
|
||||
|
||||
// Neo4j driver tuning (memory and connection management)
|
||||
Neo4jMaxConnPoolSize int `env:"ORLY_NEO4J_MAX_CONN_POOL" default:"25" usage:"max Neo4j connection pool size (driver default: 100, lower reduces memory)"`
|
||||
Neo4jFetchSize int `env:"ORLY_NEO4J_FETCH_SIZE" default:"1000" usage:"max records per fetch batch (prevents memory overflow, -1=fetch all)"`
|
||||
Neo4jMaxTxRetrySeconds int `env:"ORLY_NEO4J_MAX_TX_RETRY_SEC" default:"30" usage:"max seconds for retryable transaction attempts"`
|
||||
Neo4jQueryResultLimit int `env:"ORLY_NEO4J_QUERY_RESULT_LIMIT" default:"10000" usage:"max results returned per query (prevents unbounded memory usage, 0=unlimited)"`
|
||||
|
||||
// Advanced database tuning
|
||||
SerialCachePubkeys int `env:"ORLY_SERIAL_CACHE_PUBKEYS" default:"100000" usage:"max pubkeys to cache for compact event storage (default: 100000, ~3.2MB memory)"`
|
||||
SerialCacheEventIds int `env:"ORLY_SERIAL_CACHE_EVENT_IDS" default:"500000" usage:"max event IDs to cache for compact event storage (default: 500000, ~16MB memory)"`
|
||||
@@ -472,6 +478,7 @@ func (cfg *C) GetDatabaseConfigValues() (
|
||||
serialCachePubkeys, serialCacheEventIds int,
|
||||
zstdLevel int,
|
||||
neo4jURI, neo4jUser, neo4jPassword string,
|
||||
neo4jMaxConnPoolSize, neo4jFetchSize, neo4jMaxTxRetrySeconds, neo4jQueryResultLimit int,
|
||||
) {
|
||||
// Parse query cache max age from string to duration
|
||||
queryCacheMaxAge = 5 * time.Minute // Default
|
||||
@@ -487,7 +494,8 @@ func (cfg *C) GetDatabaseConfigValues() (
|
||||
cfg.QueryCacheDisabled,
|
||||
cfg.SerialCachePubkeys, cfg.SerialCacheEventIds,
|
||||
cfg.DBZSTDLevel,
|
||||
cfg.Neo4jURI, cfg.Neo4jUser, cfg.Neo4jPassword
|
||||
cfg.Neo4jURI, cfg.Neo4jUser, cfg.Neo4jPassword,
|
||||
cfg.Neo4jMaxConnPoolSize, cfg.Neo4jFetchSize, cfg.Neo4jMaxTxRetrySeconds, cfg.Neo4jQueryResultLimit
|
||||
}
|
||||
|
||||
// GetRateLimitConfigValues returns the rate limiting configuration values.
|
||||
|
||||
@@ -27,6 +27,7 @@ import (
|
||||
"next.orly.dev/pkg/policy"
|
||||
"next.orly.dev/pkg/protocol/graph"
|
||||
"next.orly.dev/pkg/protocol/nip43"
|
||||
"next.orly.dev/pkg/protocol/publish"
|
||||
"git.mleku.dev/mleku/nostr/utils/normalize"
|
||||
"git.mleku.dev/mleku/nostr/utils/pointers"
|
||||
)
|
||||
@@ -52,6 +53,51 @@ func (l *Listener) HandleReq(msg []byte) (err error) {
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
// NIP-46 signer-based authentication:
|
||||
// If client is not authenticated and requests kind 24133 with exactly one #p tag,
|
||||
// check if there's an active signer subscription for that pubkey.
|
||||
// If so, authenticate the client as that pubkey.
|
||||
const kindNIP46 = 24133
|
||||
if len(l.authedPubkey.Load()) == 0 && len(*env.Filters) == 1 {
|
||||
f := (*env.Filters)[0]
|
||||
if f != nil && f.Kinds != nil && f.Kinds.Len() == 1 {
|
||||
isNIP46Kind := false
|
||||
for _, k := range f.Kinds.K {
|
||||
if k.K == kindNIP46 {
|
||||
isNIP46Kind = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if isNIP46Kind && f.Tags != nil {
|
||||
pTag := f.Tags.GetFirst([]byte("p"))
|
||||
// Must have exactly one pubkey in the #p tag
|
||||
if pTag != nil && pTag.Len() == 2 {
|
||||
signerPubkey := pTag.Value()
|
||||
// Convert to binary if hex
|
||||
var signerPubkeyBin []byte
|
||||
if len(signerPubkey) == 64 {
|
||||
signerPubkeyBin, _ = hexenc.Dec(string(signerPubkey))
|
||||
} else if len(signerPubkey) == 32 {
|
||||
signerPubkeyBin = signerPubkey
|
||||
}
|
||||
if len(signerPubkeyBin) == 32 {
|
||||
// Check if there's an active signer for this pubkey
|
||||
if socketPub := l.publishers.GetSocketPublisher(); socketPub != nil {
|
||||
if checker, ok := socketPub.(publish.NIP46SignerChecker); ok {
|
||||
if checker.HasActiveNIP46Signer(signerPubkeyBin) {
|
||||
log.I.F("NIP-46 auth: client %s authenticated via active signer %s",
|
||||
l.remote, hexenc.Enc(signerPubkeyBin))
|
||||
l.authedPubkey.Store(signerPubkeyBin)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// send a challenge to the client to auth if an ACL is active, auth is required, or AuthToWrite is enabled
|
||||
if len(l.authedPubkey.Load()) == 0 && (acl.Registry.Active.Load() != "none" || l.Config.AuthRequired || l.Config.AuthToWrite) {
|
||||
if err = authenvelope.NewChallengeWith(l.challenge.Load()).
|
||||
|
||||
@@ -320,6 +320,67 @@ func (p *P) removeSubscriber(ws *websocket.Conn) {
|
||||
delete(p.WriteChans, ws)
|
||||
}
|
||||
|
||||
// HasActiveNIP46Signer checks if there's an active subscription for kind 24133
|
||||
// where the given pubkey is involved (either as author filter or in #p tag filter).
|
||||
// This is used to authenticate clients by proving a signer is connected for that pubkey.
|
||||
func (p *P) HasActiveNIP46Signer(signerPubkey []byte) bool {
|
||||
const kindNIP46 = 24133
|
||||
p.Mx.RLock()
|
||||
defer p.Mx.RUnlock()
|
||||
|
||||
for _, subs := range p.Map {
|
||||
for _, sub := range subs {
|
||||
if sub.S == nil {
|
||||
continue
|
||||
}
|
||||
for _, f := range *sub.S {
|
||||
if f == nil || f.Kinds == nil {
|
||||
continue
|
||||
}
|
||||
// Check if filter is for kind 24133
|
||||
hasNIP46Kind := false
|
||||
for _, k := range f.Kinds.K {
|
||||
if k.K == kindNIP46 {
|
||||
hasNIP46Kind = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasNIP46Kind {
|
||||
continue
|
||||
}
|
||||
// Check if the signer pubkey matches the #p tag filter
|
||||
if f.Tags != nil {
|
||||
pTag := f.Tags.GetFirst([]byte("p"))
|
||||
if pTag != nil && pTag.Len() >= 2 {
|
||||
for i := 1; i < pTag.Len(); i++ {
|
||||
tagValue := pTag.T[i]
|
||||
// Compare - handle both binary and hex formats
|
||||
if len(tagValue) == 32 && len(signerPubkey) == 32 {
|
||||
if utils.FastEqual(tagValue, signerPubkey) {
|
||||
return true
|
||||
}
|
||||
} else if len(tagValue) == 64 && len(signerPubkey) == 32 {
|
||||
// tagValue is hex, signerPubkey is binary
|
||||
if string(tagValue) == hex.Enc(signerPubkey) {
|
||||
return true
|
||||
}
|
||||
} else if len(tagValue) == 32 && len(signerPubkey) == 64 {
|
||||
// tagValue is binary, signerPubkey is hex
|
||||
if hex.Enc(tagValue) == string(signerPubkey) {
|
||||
return true
|
||||
}
|
||||
} else if utils.FastEqual(tagValue, signerPubkey) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// canSeePrivateEvent checks if the authenticated user can see an event with a private tag
|
||||
func (p *P) canSeePrivateEvent(
|
||||
authedPubkey, privatePubkey []byte, remote string,
|
||||
|
||||
Reference in New Issue
Block a user