Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
7f5bd3960c
|
|||
|
8287035920
|
|||
|
54a01e1255
|
|||
|
0bcd83bde3
|
|||
|
26c754bb2e
|
|||
|
da66e26614
|
|||
|
8609e9dc22
|
|||
|
3cb05a451c
|
@@ -431,6 +431,44 @@ privCheck:
|
||||
allEvents = aclFilteredEvents
|
||||
}
|
||||
|
||||
// Apply private tag filtering - only show events with "private" tags to authorized users
|
||||
var privateFilteredEvents event.S
|
||||
authedPubkey := l.authedPubkey.Load()
|
||||
for _, ev := range allEvents {
|
||||
// Check if event has private tags
|
||||
hasPrivateTag := false
|
||||
var privatePubkey []byte
|
||||
|
||||
if ev.Tags != nil && ev.Tags.Len() > 0 {
|
||||
for _, t := range *ev.Tags {
|
||||
if t.Len() >= 2 {
|
||||
keyBytes := t.Key()
|
||||
if len(keyBytes) == 7 && string(keyBytes) == "private" {
|
||||
hasPrivateTag = true
|
||||
privatePubkey = t.Value()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If no private tag, include the event
|
||||
if !hasPrivateTag {
|
||||
privateFilteredEvents = append(privateFilteredEvents, ev)
|
||||
continue
|
||||
}
|
||||
|
||||
// Event has private tag - check if user is authorized to see it
|
||||
canSeePrivate := l.canSeePrivateEvent(authedPubkey, privatePubkey)
|
||||
if canSeePrivate {
|
||||
privateFilteredEvents = append(privateFilteredEvents, ev)
|
||||
log.D.F("private tag: allowing event %s for authorized user", hexenc.Enc(ev.ID))
|
||||
} else {
|
||||
log.D.F("private tag: filtering out event %s from unauthorized user", hexenc.Enc(ev.ID))
|
||||
}
|
||||
}
|
||||
allEvents = privateFilteredEvents
|
||||
|
||||
seen := make(map[string]struct{})
|
||||
for _, ev := range allEvents {
|
||||
log.T.C(
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"next.orly.dev/pkg/database"
|
||||
"next.orly.dev/pkg/encoders/event"
|
||||
"next.orly.dev/pkg/encoders/filter"
|
||||
"next.orly.dev/pkg/utils"
|
||||
"next.orly.dev/pkg/utils/atomic"
|
||||
)
|
||||
|
||||
@@ -133,3 +134,25 @@ func (l *Listener) QueryEvents(ctx context.Context, f *filter.F) (event.S, error
|
||||
func (l *Listener) QueryAllVersions(ctx context.Context, f *filter.F) (event.S, error) {
|
||||
return l.D.QueryAllVersions(ctx, f)
|
||||
}
|
||||
|
||||
// canSeePrivateEvent checks if the authenticated user can see an event with a private tag
|
||||
func (l *Listener) canSeePrivateEvent(authedPubkey, privatePubkey []byte) (canSee bool) {
|
||||
// If no authenticated user, deny access
|
||||
if len(authedPubkey) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
// If the authenticated user matches the private tag pubkey, allow access
|
||||
if len(privatePubkey) > 0 && utils.FastEqual(authedPubkey, privatePubkey) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check if user is an admin or owner (they can see all private events)
|
||||
accessLevel := acl.Registry.GetAccessLevel(authedPubkey, l.remote)
|
||||
if accessLevel == "admin" || accessLevel == "owner" {
|
||||
return true
|
||||
}
|
||||
|
||||
// Default deny
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -349,6 +349,8 @@ Log in to the relay dashboard to access your configuration at: %s`,
|
||||
if len(authorizedNpubs) > 0 {
|
||||
privateTagValue := strings.Join(authorizedNpubs, ",")
|
||||
*ev.Tags = append(*ev.Tags, tag.NewFromAny("private", privateTagValue))
|
||||
// Add protected "-" tag to mark this event as protected
|
||||
*ev.Tags = append(*ev.Tags, tag.NewFromAny("-", ""))
|
||||
}
|
||||
|
||||
// Add a special tag to mark this as an expiry warning
|
||||
@@ -465,6 +467,8 @@ Log in to the relay dashboard to access your configuration at: %s`,
|
||||
if len(authorizedNpubs) > 0 {
|
||||
privateTagValue := strings.Join(authorizedNpubs, ",")
|
||||
*ev.Tags = append(*ev.Tags, tag.NewFromAny("private", privateTagValue))
|
||||
// Add protected "-" tag to mark this event as protected
|
||||
*ev.Tags = append(*ev.Tags, tag.NewFromAny("-", ""))
|
||||
}
|
||||
|
||||
// Add a special tag to mark this as a trial reminder
|
||||
@@ -691,6 +695,8 @@ func (pp *PaymentProcessor) createPaymentNote(
|
||||
if len(authorizedNpubs) > 0 {
|
||||
privateTagValue := strings.Join(authorizedNpubs, ",")
|
||||
*ev.Tags = append(*ev.Tags, tag.NewFromAny("private", privateTagValue))
|
||||
// Add protected "-" tag to mark this event as protected
|
||||
*ev.Tags = append(*ev.Tags, tag.NewFromAny("-", ""))
|
||||
}
|
||||
|
||||
// Sign and save the event
|
||||
@@ -731,9 +737,19 @@ func (pp *PaymentProcessor) CreateWelcomeNote(userPubkey []byte) error {
|
||||
return fmt.Errorf("failed to encode relay npub: %w", err)
|
||||
}
|
||||
|
||||
// Create the welcome note content with nostr:npub link
|
||||
// Get user npub for personalized greeting
|
||||
userNpub, err := bech32encoding.BinToNpub(userPubkey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to encode user npub: %w", err)
|
||||
}
|
||||
|
||||
// Create the welcome note content with privacy notice and personalized greeting
|
||||
content := fmt.Sprintf(
|
||||
`Welcome to the relay! 🎉
|
||||
`This note is only visible to you
|
||||
|
||||
Hi nostr:%s
|
||||
|
||||
Welcome to the relay! 🎉
|
||||
|
||||
You have a FREE 30-day trial that started when you first logged in.
|
||||
|
||||
@@ -753,7 +769,7 @@ Relay: nostr:%s
|
||||
|
||||
Log in to the relay dashboard to access your configuration at: %s
|
||||
|
||||
Enjoy your time on the relay!`, monthlyPrice, monthlyPrice,
|
||||
Enjoy your time on the relay!`, string(userNpub), monthlyPrice, monthlyPrice,
|
||||
string(relayNpubForContent), pp.getDashboardURL(),
|
||||
)
|
||||
|
||||
@@ -765,8 +781,8 @@ Enjoy your time on the relay!`, monthlyPrice, monthlyPrice,
|
||||
ev.Content = []byte(content)
|
||||
ev.Tags = tag.NewS()
|
||||
|
||||
// Add "p" tag for the user
|
||||
*ev.Tags = append(*ev.Tags, tag.NewFromAny("p", hex.Enc(userPubkey)))
|
||||
// Add "p" tag for the user with mention in third field
|
||||
*ev.Tags = append(*ev.Tags, tag.NewFromAny("p", hex.Enc(userPubkey), "", "mention"))
|
||||
|
||||
// Add expiration tag (5 days from creation)
|
||||
noteExpiry := time.Now().AddDate(0, 0, 5)
|
||||
@@ -778,11 +794,8 @@ Enjoy your time on the relay!`, monthlyPrice, monthlyPrice,
|
||||
// Add "private" tag with authorized npubs (user and relay)
|
||||
var authorizedNpubs []string
|
||||
|
||||
// Add user npub
|
||||
userNpub, err := bech32encoding.BinToNpub(userPubkey)
|
||||
if err == nil {
|
||||
authorizedNpubs = append(authorizedNpubs, string(userNpub))
|
||||
}
|
||||
// Add user npub (already encoded above)
|
||||
authorizedNpubs = append(authorizedNpubs, string(userNpub))
|
||||
|
||||
// Add relay npub
|
||||
relayNpub, err := bech32encoding.BinToNpub(sign.Pub())
|
||||
@@ -794,6 +807,8 @@ Enjoy your time on the relay!`, monthlyPrice, monthlyPrice,
|
||||
if len(authorizedNpubs) > 0 {
|
||||
privateTagValue := strings.Join(authorizedNpubs, ",")
|
||||
*ev.Tags = append(*ev.Tags, tag.NewFromAny("private", privateTagValue))
|
||||
// Add protected "-" tag to mark this event as protected
|
||||
*ev.Tags = append(*ev.Tags, tag.NewFromAny("-", ""))
|
||||
}
|
||||
|
||||
// Add a special tag to mark this as a welcome note
|
||||
|
||||
173
app/publisher.go
173
app/publisher.go
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/coder/websocket"
|
||||
"lol.mleku.dev/chk"
|
||||
"lol.mleku.dev/log"
|
||||
"next.orly.dev/pkg/acl"
|
||||
"next.orly.dev/pkg/encoders/envelopes/eventenvelope"
|
||||
"next.orly.dev/pkg/encoders/event"
|
||||
"next.orly.dev/pkg/encoders/filter"
|
||||
@@ -211,68 +212,96 @@ func (p *P) Deliver(ev *event.E) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if !allowed {
|
||||
log.D.F("subscription delivery DENIED for privileged event %s to %s (auth mismatch)",
|
||||
hex.Enc(ev.ID), d.sub.remote)
|
||||
// Skip delivery for this subscriber
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
var res *eventenvelope.Result
|
||||
if res, err = eventenvelope.NewResultWith(d.id, ev); chk.E(err) {
|
||||
log.E.F("failed to create event envelope for %s to %s: %v",
|
||||
hex.Enc(ev.ID), d.sub.remote, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Log delivery attempt
|
||||
msgData := res.Marshal(nil)
|
||||
log.D.F("attempting delivery of event %s (kind=%d, len=%d) to subscription %s @ %s",
|
||||
hex.Enc(ev.ID), ev.Kind, len(msgData), d.id, d.sub.remote)
|
||||
|
||||
// Use a separate context with timeout for writes to prevent race conditions
|
||||
// where the publisher context gets cancelled while writing events
|
||||
writeCtx, cancel := context.WithTimeout(
|
||||
context.Background(), DefaultWriteTimeout,
|
||||
)
|
||||
defer cancel()
|
||||
}
|
||||
if !allowed {
|
||||
log.D.F("subscription delivery DENIED for privileged event %s to %s (auth mismatch)",
|
||||
hex.Enc(ev.ID), d.sub.remote)
|
||||
// Skip delivery for this subscriber
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
deliveryStart := time.Now()
|
||||
if err = d.w.Write(
|
||||
writeCtx, websocket.MessageText, msgData,
|
||||
); err != nil {
|
||||
deliveryDuration := time.Since(deliveryStart)
|
||||
|
||||
// Log detailed failure information
|
||||
log.E.F("subscription delivery FAILED: event=%s to=%s sub=%s duration=%v error=%v",
|
||||
hex.Enc(ev.ID), d.sub.remote, d.id, deliveryDuration, err)
|
||||
|
||||
// Check for timeout specifically
|
||||
if writeCtx.Err() != nil {
|
||||
log.E.F("subscription delivery TIMEOUT: event=%s to=%s after %v (limit=%v)",
|
||||
hex.Enc(ev.ID), d.sub.remote, deliveryDuration, DefaultWriteTimeout)
|
||||
}
|
||||
|
||||
// Log connection cleanup
|
||||
log.D.F("removing failed subscriber connection: %s", d.sub.remote)
|
||||
|
||||
// On error, remove the subscriber connection safely
|
||||
p.removeSubscriber(d.w)
|
||||
_ = d.w.CloseNow()
|
||||
continue
|
||||
}
|
||||
|
||||
deliveryDuration := time.Since(deliveryStart)
|
||||
log.D.F("subscription delivery SUCCESS: event=%s to=%s sub=%s duration=%v len=%d",
|
||||
hex.Enc(ev.ID), d.sub.remote, d.id, deliveryDuration, len(msgData))
|
||||
|
||||
// Log slow deliveries for performance monitoring
|
||||
if deliveryDuration > time.Millisecond*50 {
|
||||
log.D.F("SLOW subscription delivery: event=%s to=%s duration=%v (>50ms)",
|
||||
hex.Enc(ev.ID), d.sub.remote, deliveryDuration)
|
||||
}
|
||||
// Check for private tags - only deliver to authorized users
|
||||
if ev.Tags != nil && ev.Tags.Len() > 0 {
|
||||
hasPrivateTag := false
|
||||
var privatePubkey []byte
|
||||
|
||||
for _, t := range *ev.Tags {
|
||||
if t.Len() >= 2 {
|
||||
keyBytes := t.Key()
|
||||
if len(keyBytes) == 7 && string(keyBytes) == "private" {
|
||||
hasPrivateTag = true
|
||||
privatePubkey = t.Value()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if hasPrivateTag {
|
||||
canSeePrivate := p.canSeePrivateEvent(d.sub.AuthedPubkey, privatePubkey, d.sub.remote)
|
||||
if !canSeePrivate {
|
||||
log.D.F("subscription delivery DENIED for private event %s to %s (unauthorized)",
|
||||
hex.Enc(ev.ID), d.sub.remote)
|
||||
continue
|
||||
}
|
||||
log.D.F("subscription delivery ALLOWED for private event %s to %s (authorized)",
|
||||
hex.Enc(ev.ID), d.sub.remote)
|
||||
}
|
||||
}
|
||||
|
||||
var res *eventenvelope.Result
|
||||
if res, err = eventenvelope.NewResultWith(d.id, ev); chk.E(err) {
|
||||
log.E.F("failed to create event envelope for %s to %s: %v",
|
||||
hex.Enc(ev.ID), d.sub.remote, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Log delivery attempt
|
||||
msgData := res.Marshal(nil)
|
||||
log.D.F("attempting delivery of event %s (kind=%d, len=%d) to subscription %s @ %s",
|
||||
hex.Enc(ev.ID), ev.Kind, len(msgData), d.id, d.sub.remote)
|
||||
|
||||
// Use a separate context with timeout for writes to prevent race conditions
|
||||
// where the publisher context gets cancelled while writing events
|
||||
writeCtx, cancel := context.WithTimeout(
|
||||
context.Background(), DefaultWriteTimeout,
|
||||
)
|
||||
defer cancel()
|
||||
|
||||
deliveryStart := time.Now()
|
||||
if err = d.w.Write(
|
||||
writeCtx, websocket.MessageText, msgData,
|
||||
); err != nil {
|
||||
deliveryDuration := time.Since(deliveryStart)
|
||||
|
||||
// Log detailed failure information
|
||||
log.E.F("subscription delivery FAILED: event=%s to=%s sub=%s duration=%v error=%v",
|
||||
hex.Enc(ev.ID), d.sub.remote, d.id, deliveryDuration, err)
|
||||
|
||||
// Check for timeout specifically
|
||||
if writeCtx.Err() != nil {
|
||||
log.E.F("subscription delivery TIMEOUT: event=%s to=%s after %v (limit=%v)",
|
||||
hex.Enc(ev.ID), d.sub.remote, deliveryDuration, DefaultWriteTimeout)
|
||||
}
|
||||
|
||||
// Log connection cleanup
|
||||
log.D.F("removing failed subscriber connection: %s", d.sub.remote)
|
||||
|
||||
// On error, remove the subscriber connection safely
|
||||
p.removeSubscriber(d.w)
|
||||
_ = d.w.CloseNow()
|
||||
continue
|
||||
}
|
||||
|
||||
deliveryDuration := time.Since(deliveryStart)
|
||||
log.D.F("subscription delivery SUCCESS: event=%s to=%s sub=%s duration=%v len=%d",
|
||||
hex.Enc(ev.ID), d.sub.remote, d.id, deliveryDuration, len(msgData))
|
||||
|
||||
// Log slow deliveries for performance monitoring
|
||||
if deliveryDuration > time.Millisecond*50 {
|
||||
log.D.F("SLOW subscription delivery: event=%s to=%s duration=%v (>50ms)",
|
||||
hex.Enc(ev.ID), d.sub.remote, deliveryDuration)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -299,3 +328,25 @@ func (p *P) removeSubscriber(ws *websocket.Conn) {
|
||||
clear(p.Map[ws])
|
||||
delete(p.Map, ws)
|
||||
}
|
||||
|
||||
// canSeePrivateEvent checks if the authenticated user can see an event with a private tag
|
||||
func (p *P) canSeePrivateEvent(authedPubkey, privatePubkey []byte, remote string) (canSee bool) {
|
||||
// If no authenticated user, deny access
|
||||
if len(authedPubkey) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
// If the authenticated user matches the private tag pubkey, allow access
|
||||
if len(privatePubkey) > 0 && utils.FastEqual(authedPubkey, privatePubkey) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check if user is an admin or owner (they can see all private events)
|
||||
accessLevel := acl.Registry.GetAccessLevel(authedPubkey, remote)
|
||||
if accessLevel == "admin" || accessLevel == "owner" {
|
||||
return true
|
||||
}
|
||||
|
||||
// Default deny
|
||||
return false
|
||||
}
|
||||
|
||||
144
main.go
144
main.go
@@ -9,6 +9,8 @@ import (
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"runtime"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/profile"
|
||||
@@ -21,6 +23,7 @@ import (
|
||||
"next.orly.dev/pkg/database"
|
||||
"next.orly.dev/pkg/encoders/hex"
|
||||
"next.orly.dev/pkg/spider"
|
||||
"next.orly.dev/pkg/utils/interrupt"
|
||||
"next.orly.dev/pkg/version"
|
||||
)
|
||||
|
||||
@@ -83,16 +86,31 @@ func main() {
|
||||
log.I.F("enabling HTTP pprof server to support web viewer")
|
||||
cfg.PprofHTTP = true
|
||||
}
|
||||
// Ensure profiling is stopped on interrupts (SIGINT/SIGTERM) as well as on normal exit
|
||||
var profileStopOnce sync.Once
|
||||
profileStop := func() {}
|
||||
switch cfg.Pprof {
|
||||
case "cpu":
|
||||
if cfg.PprofPath != "" {
|
||||
prof := profile.Start(
|
||||
profile.CPUProfile, profile.ProfilePath(cfg.PprofPath),
|
||||
)
|
||||
defer prof.Stop()
|
||||
profileStop = func() {
|
||||
profileStopOnce.Do(func() {
|
||||
prof.Stop()
|
||||
log.I.F("cpu profiling stopped and flushed")
|
||||
})
|
||||
}
|
||||
defer profileStop()
|
||||
} else {
|
||||
prof := profile.Start(profile.CPUProfile)
|
||||
defer prof.Stop()
|
||||
profileStop = func() {
|
||||
profileStopOnce.Do(func() {
|
||||
prof.Stop()
|
||||
log.I.F("cpu profiling stopped and flushed")
|
||||
})
|
||||
}
|
||||
defer profileStop()
|
||||
}
|
||||
case "memory":
|
||||
if cfg.PprofPath != "" {
|
||||
@@ -100,10 +118,22 @@ func main() {
|
||||
profile.MemProfile, profile.MemProfileRate(32),
|
||||
profile.ProfilePath(cfg.PprofPath),
|
||||
)
|
||||
defer prof.Stop()
|
||||
profileStop = func() {
|
||||
profileStopOnce.Do(func() {
|
||||
prof.Stop()
|
||||
log.I.F("memory profiling stopped and flushed")
|
||||
})
|
||||
}
|
||||
defer profileStop()
|
||||
} else {
|
||||
prof := profile.Start(profile.MemProfile)
|
||||
defer prof.Stop()
|
||||
profileStop = func() {
|
||||
profileStopOnce.Do(func() {
|
||||
prof.Stop()
|
||||
log.I.F("memory profiling stopped and flushed")
|
||||
})
|
||||
}
|
||||
defer profileStop()
|
||||
}
|
||||
case "allocation":
|
||||
if cfg.PprofPath != "" {
|
||||
@@ -111,30 +141,66 @@ func main() {
|
||||
profile.MemProfileAllocs, profile.MemProfileRate(32),
|
||||
profile.ProfilePath(cfg.PprofPath),
|
||||
)
|
||||
defer prof.Stop()
|
||||
profileStop = func() {
|
||||
profileStopOnce.Do(func() {
|
||||
prof.Stop()
|
||||
log.I.F("allocation profiling stopped and flushed")
|
||||
})
|
||||
}
|
||||
defer profileStop()
|
||||
} else {
|
||||
prof := profile.Start(profile.MemProfileAllocs)
|
||||
defer prof.Stop()
|
||||
profileStop = func() {
|
||||
profileStopOnce.Do(func() {
|
||||
prof.Stop()
|
||||
log.I.F("allocation profiling stopped and flushed")
|
||||
})
|
||||
}
|
||||
defer profileStop()
|
||||
}
|
||||
case "heap":
|
||||
if cfg.PprofPath != "" {
|
||||
prof := profile.Start(
|
||||
profile.MemProfileHeap, profile.ProfilePath(cfg.PprofPath),
|
||||
)
|
||||
defer prof.Stop()
|
||||
profileStop = func() {
|
||||
profileStopOnce.Do(func() {
|
||||
prof.Stop()
|
||||
log.I.F("heap profiling stopped and flushed")
|
||||
})
|
||||
}
|
||||
defer profileStop()
|
||||
} else {
|
||||
prof := profile.Start(profile.MemProfileHeap)
|
||||
defer prof.Stop()
|
||||
profileStop = func() {
|
||||
profileStopOnce.Do(func() {
|
||||
prof.Stop()
|
||||
log.I.F("heap profiling stopped and flushed")
|
||||
})
|
||||
}
|
||||
defer profileStop()
|
||||
}
|
||||
case "mutex":
|
||||
if cfg.PprofPath != "" {
|
||||
prof := profile.Start(
|
||||
profile.MutexProfile, profile.ProfilePath(cfg.PprofPath),
|
||||
)
|
||||
defer prof.Stop()
|
||||
profileStop = func() {
|
||||
profileStopOnce.Do(func() {
|
||||
prof.Stop()
|
||||
log.I.F("mutex profiling stopped and flushed")
|
||||
})
|
||||
}
|
||||
defer profileStop()
|
||||
} else {
|
||||
prof := profile.Start(profile.MutexProfile)
|
||||
defer prof.Stop()
|
||||
profileStop = func() {
|
||||
profileStopOnce.Do(func() {
|
||||
prof.Stop()
|
||||
log.I.F("mutex profiling stopped and flushed")
|
||||
})
|
||||
}
|
||||
defer profileStop()
|
||||
}
|
||||
case "threadcreate":
|
||||
if cfg.PprofPath != "" {
|
||||
@@ -142,33 +208,75 @@ func main() {
|
||||
profile.ThreadcreationProfile,
|
||||
profile.ProfilePath(cfg.PprofPath),
|
||||
)
|
||||
defer prof.Stop()
|
||||
profileStop = func() {
|
||||
profileStopOnce.Do(func() {
|
||||
prof.Stop()
|
||||
log.I.F("threadcreate profiling stopped and flushed")
|
||||
})
|
||||
}
|
||||
defer profileStop()
|
||||
} else {
|
||||
prof := profile.Start(profile.ThreadcreationProfile)
|
||||
defer prof.Stop()
|
||||
profileStop = func() {
|
||||
profileStopOnce.Do(func() {
|
||||
prof.Stop()
|
||||
log.I.F("threadcreate profiling stopped and flushed")
|
||||
})
|
||||
}
|
||||
defer profileStop()
|
||||
}
|
||||
case "goroutine":
|
||||
if cfg.PprofPath != "" {
|
||||
prof := profile.Start(
|
||||
profile.GoroutineProfile, profile.ProfilePath(cfg.PprofPath),
|
||||
)
|
||||
defer prof.Stop()
|
||||
profileStop = func() {
|
||||
profileStopOnce.Do(func() {
|
||||
prof.Stop()
|
||||
log.I.F("goroutine profiling stopped and flushed")
|
||||
})
|
||||
}
|
||||
defer profileStop()
|
||||
} else {
|
||||
prof := profile.Start(profile.GoroutineProfile)
|
||||
defer prof.Stop()
|
||||
profileStop = func() {
|
||||
profileStopOnce.Do(func() {
|
||||
prof.Stop()
|
||||
log.I.F("goroutine profiling stopped and flushed")
|
||||
})
|
||||
}
|
||||
defer profileStop()
|
||||
}
|
||||
case "block":
|
||||
if cfg.PprofPath != "" {
|
||||
prof := profile.Start(
|
||||
profile.BlockProfile, profile.ProfilePath(cfg.PprofPath),
|
||||
)
|
||||
defer prof.Stop()
|
||||
profileStop = func() {
|
||||
profileStopOnce.Do(func() {
|
||||
prof.Stop()
|
||||
log.I.F("block profiling stopped and flushed")
|
||||
})
|
||||
}
|
||||
defer profileStop()
|
||||
} else {
|
||||
prof := profile.Start(profile.BlockProfile)
|
||||
defer prof.Stop()
|
||||
profileStop = func() {
|
||||
profileStopOnce.Do(func() {
|
||||
prof.Stop()
|
||||
log.I.F("block profiling stopped and flushed")
|
||||
})
|
||||
}
|
||||
defer profileStop()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Register a handler so profiling is stopped when an interrupt is received
|
||||
interrupt.AddHandler(func() {
|
||||
log.I.F("interrupt received: stopping profiling")
|
||||
profileStop()
|
||||
})
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
var db *database.D
|
||||
if db, err = database.New(
|
||||
@@ -277,7 +385,7 @@ func main() {
|
||||
|
||||
quit := app.Run(ctx, cfg, db)
|
||||
sigs := make(chan os.Signal, 1)
|
||||
signal.Notify(sigs, os.Interrupt)
|
||||
signal.Notify(sigs, os.Interrupt, syscall.SIGTERM)
|
||||
for {
|
||||
select {
|
||||
case <-sigs:
|
||||
@@ -296,5 +404,5 @@ func main() {
|
||||
return
|
||||
}
|
||||
}
|
||||
log.I.F("exiting")
|
||||
// log.I.F("exiting")
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
v0.17.5
|
||||
v0.17.11
|
||||
147
scripts/run-relay-pprof.sh
Executable file
147
scripts/run-relay-pprof.sh
Executable file
@@ -0,0 +1,147 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Run the relay with CPU profiling enabled, wait 60s, then open the
|
||||
# generated profile using `go tool pprof` web UI.
|
||||
#
|
||||
# Notes:
|
||||
# - Builds a temporary relay binary in /tmp and deletes it on exit.
|
||||
# - Uses the exact env requested, plus ORLY_PPROF=cpu and a deterministic
|
||||
# ORLY_PPROF_PATH inside a temp dir.
|
||||
# - Profiles for DURATION seconds (default 60).
|
||||
# - Launches pprof web UI at http://localhost:8000 and attempts to open browser.
|
||||
|
||||
DURATION="${DURATION:-60}"
|
||||
HEALTH_PORT="${HEALTH_PORT:-18081}"
|
||||
ROOT_DIR="/home/mleku/src/next.orly.dev"
|
||||
LISTEN_HOST="${LISTEN_HOST:-10.0.0.2}"
|
||||
|
||||
cd "$ROOT_DIR"
|
||||
|
||||
# Refresh embedded web assets
|
||||
reset || true
|
||||
./scripts/update-embedded-web.sh || true
|
||||
|
||||
TMP_DIR="$(mktemp -d -t orly-pprof-XXXXXX)"
|
||||
BIN_PATH="$TMP_DIR/next.orly.dev"
|
||||
LOG_FILE="$TMP_DIR/relay.log"
|
||||
PPROF_FILE=""
|
||||
RELAY_PID=""
|
||||
PPROF_DIR="$TMP_DIR/profiles"
|
||||
mkdir -p "$PPROF_DIR"
|
||||
|
||||
cleanup() {
|
||||
# Try to stop relay if still running
|
||||
if [[ -n "${RELAY_PID}" ]] && kill -0 "${RELAY_PID}" 2>/dev/null; then
|
||||
kill "${RELAY_PID}" || true
|
||||
wait "${RELAY_PID}" || true
|
||||
fi
|
||||
rm -f "$BIN_PATH" 2>/dev/null || true
|
||||
rm -rf "$TMP_DIR" 2>/dev/null || true
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
echo "[run-relay-pprof] Building relay binary ..."
|
||||
GOFLAGS="${GOFLAGS:-}" go build -o "$BIN_PATH" .
|
||||
|
||||
echo "[run-relay-pprof] Starting relay with CPU profiling ..."
|
||||
(
|
||||
ORLY_LOG_LEVEL=debug \
|
||||
ORLY_LISTEN="$LISTEN_HOST" \
|
||||
ORLY_PORT=3334 \
|
||||
ORLY_ADMINS=npub1fjqqy4a93z5zsjwsfxqhc2764kvykfdyttvldkkkdera8dr78vhsmmleku \
|
||||
ORLY_ACL_MODE=follows \
|
||||
ORLY_SPIDER_MODE=none \
|
||||
ORLY_RELAY_ADDRESSES=test.orly.dev \
|
||||
ORLY_IP_BLACKLIST=192.71.213.188 \
|
||||
ORLY_HEALTH_PORT="$HEALTH_PORT" \
|
||||
ORLY_ENABLE_SHUTDOWN=true \
|
||||
ORLY_PPROF_HTTP=true \
|
||||
ORLY_OPEN_PPROF_WEB=true \
|
||||
"$BIN_PATH"
|
||||
) >"$LOG_FILE" 2>&1 &
|
||||
RELAY_PID=$!
|
||||
|
||||
# Wait for pprof HTTP server readiness
|
||||
PPROF_BASE="http://${LISTEN_HOST}:6060"
|
||||
echo "[run-relay-pprof] Waiting for pprof at ${PPROF_BASE} ..."
|
||||
for i in {1..100}; do
|
||||
if curl -fsS "${PPROF_BASE}/debug/pprof/" -o /dev/null 2>/dev/null; then
|
||||
READY=1
|
||||
break
|
||||
fi
|
||||
sleep 0.2
|
||||
done
|
||||
if [[ -z "${READY:-}" ]]; then
|
||||
echo "[run-relay-pprof] ERROR: pprof HTTP server not reachable at ${PPROF_BASE}." >&2
|
||||
echo "[run-relay-pprof] Check that ${LISTEN_HOST} is a local bindable address." >&2
|
||||
# Attempt to dump recent logs for context
|
||||
tail -n 100 "$LOG_FILE" || true
|
||||
# Try INT to clean up
|
||||
killall -INT next.orly.dev 2>/dev/null || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Open the HTTP pprof UI
|
||||
( xdg-open "${PPROF_BASE}/debug/pprof/" >/dev/null 2>&1 || true ) &
|
||||
|
||||
echo "[run-relay-pprof] Collecting CPU profile via HTTP for ${DURATION}s ..."
|
||||
# The HTTP /debug/pprof/profile endpoint records CPU for the provided seconds
|
||||
# and returns a pprof file without needing to stop the process.
|
||||
curl -fsS --max-time $((DURATION+10)) \
|
||||
"${PPROF_BASE}/debug/pprof/profile?seconds=${DURATION}" \
|
||||
-o "$PPROF_DIR/cpu.pprof" || true
|
||||
|
||||
echo "[run-relay-pprof] Sending SIGINT (Ctrl+C) for graceful shutdown ..."
|
||||
killall -INT next.orly.dev 2>/dev/null || true
|
||||
|
||||
# Wait up to ~60s for graceful shutdown so defers (pprof Stop) can run
|
||||
for i in {1..300}; do
|
||||
if ! pgrep -x next.orly.dev >/dev/null 2>&1; then
|
||||
break
|
||||
fi
|
||||
sleep 0.2
|
||||
done
|
||||
|
||||
# Try HTTP shutdown if still running (ensures defer paths can run)
|
||||
if pgrep -x next.orly.dev >/dev/null 2>&1; then
|
||||
echo "[run-relay-pprof] Still running, requesting /shutdown ..."
|
||||
curl -fsS --max-time 2 "http://10.0.0.2:${HEALTH_PORT}/shutdown" >/dev/null 2>&1 || true
|
||||
for i in {1..150}; do
|
||||
if ! pgrep -x next.orly.dev >/dev/null 2>&1; then
|
||||
break
|
||||
fi
|
||||
sleep 0.2
|
||||
done
|
||||
fi
|
||||
if pgrep -x next.orly.dev >/dev/null 2>&1; then
|
||||
echo "[run-relay-pprof] Escalating: sending SIGTERM ..."
|
||||
killall -TERM next.orly.dev 2>/dev/null || true
|
||||
for i in {1..150}; do
|
||||
if ! pgrep -x next.orly.dev >/dev/null 2>&1; then
|
||||
break
|
||||
fi
|
||||
sleep 0.2
|
||||
done
|
||||
fi
|
||||
if pgrep -x next.orly.dev >/dev/null 2>&1; then
|
||||
echo "[run-relay-pprof] Force kill: sending SIGKILL ..."
|
||||
killall -KILL next.orly.dev 2>/dev/null || true
|
||||
fi
|
||||
|
||||
PPROF_FILE="$PPROF_DIR/cpu.pprof"
|
||||
if [[ ! -s "$PPROF_FILE" ]]; then
|
||||
echo "[run-relay-pprof] ERROR: HTTP CPU profile not captured (file empty)." >&2
|
||||
echo "[run-relay-pprof] Hint: Ensure ORLY_PPROF_HTTP=true and port 6060 is reachable." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "[run-relay-pprof] Detected profile file: $PPROF_FILE"
|
||||
echo "[run-relay-pprof] Launching 'go tool pprof' web UI on :8000 ..."
|
||||
|
||||
# Try to open a browser automatically, ignore failures
|
||||
( sleep 0.6; xdg-open "http://localhost:8000" >/dev/null 2>&1 || true ) &
|
||||
|
||||
exec go tool pprof -http=:8000 "$BIN_PATH" "$PPROF_FILE"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user