More RWLock. Jeff wins again
parent
057c4a3239
commit
d8dd4c92bf
12
cmd/app.go
12
cmd/app.go
|
@ -28,7 +28,7 @@ var flagsDefault = []cli.Flag{
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
logLevelOverrideRegex = regexp.MustCompile(`(?i)^([^=]+)\s*=\s*(\S+)\s*->\s*(TRACE|DEBUG|INFO|WARN|ERROR)$`)
|
logLevelOverrideRegex = regexp.MustCompile(`(?i)^([^=\s]+)(?:\s*=\s*(\S+))?\s*->\s*(TRACE|DEBUG|INFO|WARN|ERROR)$`)
|
||||||
)
|
)
|
||||||
|
|
||||||
// New creates a new CLI application
|
// New creates a new CLI application
|
||||||
|
@ -76,11 +76,15 @@ func initLogFunc(c *cli.Context) error {
|
||||||
func applyLogLevelOverrides(rawOverrides []string) error {
|
func applyLogLevelOverrides(rawOverrides []string) error {
|
||||||
for _, override := range rawOverrides {
|
for _, override := range rawOverrides {
|
||||||
m := logLevelOverrideRegex.FindStringSubmatch(override)
|
m := logLevelOverrideRegex.FindStringSubmatch(override)
|
||||||
if len(m) != 4 {
|
if len(m) == 4 {
|
||||||
return fmt.Errorf(`invalid log level override "%s", must be "field=value -> loglevel", e.g. "user_id=u_123 -> DEBUG"`, override)
|
|
||||||
}
|
|
||||||
field, value, level := m[1], m[2], m[3]
|
field, value, level := m[1], m[2], m[3]
|
||||||
log.SetLevelOverride(field, value, log.ToLevel(level))
|
log.SetLevelOverride(field, value, log.ToLevel(level))
|
||||||
|
} else if len(m) == 3 {
|
||||||
|
field, level := m[1], m[2]
|
||||||
|
log.SetLevelOverride(field, "", log.ToLevel(level)) // Matches any value
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf(`invalid log level override "%s", must be "field=value -> loglevel", e.g. "user_id=u_123 -> DEBUG"`, override)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -206,9 +206,7 @@ func (e *Event) globalLevelWithOverride() Level {
|
||||||
for field, override := range ov {
|
for field, override := range ov {
|
||||||
value, exists := e.fields[field]
|
value, exists := e.fields[field]
|
||||||
if exists {
|
if exists {
|
||||||
if value == override.value {
|
if override.value == "" || override.value == value || override.value == fmt.Sprintf("%v", value) {
|
||||||
return override.level
|
|
||||||
} else if fmt.Sprintf("%v", value) == override.value {
|
|
||||||
return override.level
|
return override.level
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -151,6 +151,27 @@ func TestLog_Timing(t *testing.T) {
|
||||||
require.Contains(t, out.String(), `{"time":"1970-01-01T00:00:12Z","level":"INFO","message":"A thing that takes a while","time_taken_ms":`)
|
require.Contains(t, out.String(), `{"time":"1970-01-01T00:00:12Z","level":"INFO","message":"A thing that takes a while","time_taken_ms":`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLog_LevelOverrideAny(t *testing.T) {
|
||||||
|
t.Cleanup(resetState)
|
||||||
|
|
||||||
|
var out bytes.Buffer
|
||||||
|
SetOutput(&out)
|
||||||
|
SetFormat(JSONFormat)
|
||||||
|
SetLevelOverride("this_one", "", DebugLevel)
|
||||||
|
SetLevelOverride("time_taken_ms", "", TraceLevel)
|
||||||
|
|
||||||
|
Time(time.Unix(11, 0).UTC()).Field("this_one", "11").Debug("this is logged")
|
||||||
|
Time(time.Unix(12, 0).UTC()).Field("not_this", "11").Debug("this is not logged")
|
||||||
|
Time(time.Unix(13, 0).UTC()).Field("this_too", "11").Info("this is also logged")
|
||||||
|
Time(time.Unix(14, 0).UTC()).Field("time_taken_ms", 0).Info("this is also logged")
|
||||||
|
|
||||||
|
expected := `{"time":"1970-01-01T00:00:11Z","level":"DEBUG","message":"this is logged","this_one":"11"}
|
||||||
|
{"time":"1970-01-01T00:00:13Z","level":"INFO","message":"this is also logged","this_too":"11"}
|
||||||
|
{"time":"1970-01-01T00:00:14Z","level":"INFO","message":"this is also logged","time_taken_ms":0}
|
||||||
|
`
|
||||||
|
require.Equal(t, expected, out.String())
|
||||||
|
}
|
||||||
|
|
||||||
type fakeError struct {
|
type fakeError struct {
|
||||||
Code int
|
Code int
|
||||||
Message string
|
Message string
|
||||||
|
|
|
@ -44,6 +44,7 @@ import (
|
||||||
- MEDIUM: Test new token endpoints & never-expiring token
|
- MEDIUM: Test new token endpoints & never-expiring token
|
||||||
- LOW: UI: Flickering upgrade banner when logging in
|
- LOW: UI: Flickering upgrade banner when logging in
|
||||||
- LOW: Menu item -> popup click should not open page
|
- LOW: Menu item -> popup click should not open page
|
||||||
|
- LOW: get rid of reservation id, replace with DELETE X-Topic: ...
|
||||||
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
@ -143,8 +144,8 @@ const (
|
||||||
tagPublish = "publish"
|
tagPublish = "publish"
|
||||||
tagSubscribe = "subscribe"
|
tagSubscribe = "subscribe"
|
||||||
tagFirebase = "firebase"
|
tagFirebase = "firebase"
|
||||||
tagEmail = "email" // Send email
|
|
||||||
tagSMTP = "smtp" // Receive email
|
tagSMTP = "smtp" // Receive email
|
||||||
|
tagEmail = "email" // Send email
|
||||||
tagFileCache = "file_cache"
|
tagFileCache = "file_cache"
|
||||||
tagMessageCache = "message_cache"
|
tagMessageCache = "message_cache"
|
||||||
tagStripe = "stripe"
|
tagStripe = "stripe"
|
||||||
|
@ -323,16 +324,30 @@ func (s *Server) closeDatabases() {
|
||||||
s.messageCache.Close()
|
s.messageCache.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handle is the main entry point for all HTTP requests
|
||||||
func (s *Server) handle(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handle(w http.ResponseWriter, r *http.Request) {
|
||||||
v, err := s.maybeAuthenticate(r) // Note: Always returns v, even when error is returned
|
v, err := s.maybeAuthenticate(r) // Note: Always returns v, even when error is returned
|
||||||
if err == nil {
|
|
||||||
logvr(v, r).Debug("Dispatching request")
|
|
||||||
if log.IsTrace() {
|
|
||||||
logvr(v, r).Trace("Entire request (headers and body):\n%s", renderHTTPRequest(r))
|
|
||||||
}
|
|
||||||
err = s.handleInternal(w, r, v)
|
|
||||||
}
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
s.handleError(w, r, v, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if log.IsTrace() {
|
||||||
|
logvr(v, r).Field("http_request", renderHTTPRequest(r)).Trace("HTTP request started")
|
||||||
|
} else if log.IsDebug() {
|
||||||
|
logvr(v, r).Debug("HTTP request started")
|
||||||
|
}
|
||||||
|
logvr(v, r).
|
||||||
|
Timing(func() {
|
||||||
|
if err := s.handleInternal(w, r, v); err != nil {
|
||||||
|
s.handleError(w, r, v, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}).
|
||||||
|
Debug("HTTP request finished")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleError(w http.ResponseWriter, r *http.Request, v *visitor, err error) {
|
||||||
if websocket.IsWebSocketUpgrade(r) {
|
if websocket.IsWebSocketUpgrade(r) {
|
||||||
isNormalError := strings.Contains(err.Error(), "i/o timeout")
|
isNormalError := strings.Contains(err.Error(), "i/o timeout")
|
||||||
if isNormalError {
|
if isNormalError {
|
||||||
|
@ -365,7 +380,6 @@ func (s *Server) handle(w http.ResponseWriter, r *http.Request) {
|
||||||
w.WriteHeader(httpErr.HTTPCode)
|
w.WriteHeader(httpErr.HTTPCode)
|
||||||
io.WriteString(w, httpErr.JSON()+"\n")
|
io.WriteString(w, httpErr.JSON()+"\n")
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request, v *visitor) error {
|
func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request, v *visitor) error {
|
||||||
if r.Method == http.MethodGet && r.URL.Path == "/" {
|
if r.Method == http.MethodGet && r.URL.Path == "/" {
|
||||||
|
|
|
@ -257,7 +257,9 @@
|
||||||
# Be aware that "debug" (and particularly "trace") can be VERY CHATTY. Only turn them on briefly for debugging purposes.
|
# Be aware that "debug" (and particularly "trace") can be VERY CHATTY. Only turn them on briefly for debugging purposes.
|
||||||
# - log-level-overrides lets you override the log level if certain fields match. This is incredibly powerful
|
# - log-level-overrides lets you override the log level if certain fields match. This is incredibly powerful
|
||||||
# for debugging certain parts of the system (e.g. only the account management, or only a certain visitor).
|
# for debugging certain parts of the system (e.g. only the account management, or only a certain visitor).
|
||||||
# This is an array of strings in the format "field=value -> level", e.g. "tag=manager -> trace".
|
# This is an array of strings in the format:
|
||||||
|
# - "field=value -> level" to match a value exactly, e.g. "tag=manager -> trace"
|
||||||
|
# - "field -> level" to match any value, e.g. "time_taken_ms -> debug"
|
||||||
# Warning: Using log-level-overrides has a performance penalty. Only use it for temporary debugging.
|
# Warning: Using log-level-overrides has a performance penalty. Only use it for temporary debugging.
|
||||||
#
|
#
|
||||||
# Example (good for production):
|
# Example (good for production):
|
||||||
|
@ -269,6 +271,7 @@
|
||||||
# log-level-overrides:
|
# log-level-overrides:
|
||||||
# - "tag=manager -> trace"
|
# - "tag=manager -> trace"
|
||||||
# - "visitor_ip=1.2.3.4 -> debug"
|
# - "visitor_ip=1.2.3.4 -> debug"
|
||||||
|
# - "time_taken_ms -> debug"
|
||||||
#
|
#
|
||||||
# log-level: info
|
# log-level: info
|
||||||
# log-level-overrides:
|
# log-level-overrides:
|
||||||
|
|
|
@ -62,7 +62,7 @@ type visitor struct {
|
||||||
authLimiter *rate.Limiter // Limiter for incorrect login attempts, may be nil
|
authLimiter *rate.Limiter // Limiter for incorrect login attempts, may be nil
|
||||||
firebase time.Time // Next allowed Firebase message
|
firebase time.Time // Next allowed Firebase message
|
||||||
seen time.Time // Last seen time of this visitor (needed for removal of stale visitors)
|
seen time.Time // Last seen time of this visitor (needed for removal of stale visitors)
|
||||||
mu sync.Mutex
|
mu sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
type visitorInfo struct {
|
type visitorInfo struct {
|
||||||
|
@ -133,8 +133,8 @@ func newVisitor(conf *Config, messageCache *messageCache, userManager *user.Mana
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *visitor) Context() log.Context {
|
func (v *visitor) Context() log.Context {
|
||||||
v.mu.Lock()
|
v.mu.RLock()
|
||||||
defer v.mu.Unlock()
|
defer v.mu.RUnlock()
|
||||||
return v.contextNoLock()
|
return v.contextNoLock()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -184,14 +184,14 @@ func visitorExtendedInfoContext(info *visitorInfo) log.Context {
|
||||||
|
|
||||||
}
|
}
|
||||||
func (v *visitor) RequestAllowed() bool {
|
func (v *visitor) RequestAllowed() bool {
|
||||||
v.mu.Lock() // limiters could be replaced!
|
v.mu.RLock() // limiters could be replaced!
|
||||||
defer v.mu.Unlock()
|
defer v.mu.RUnlock()
|
||||||
return v.requestLimiter.Allow()
|
return v.requestLimiter.Allow()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *visitor) FirebaseAllowed() bool {
|
func (v *visitor) FirebaseAllowed() bool {
|
||||||
v.mu.Lock()
|
v.mu.RLock()
|
||||||
defer v.mu.Unlock()
|
defer v.mu.RUnlock()
|
||||||
return !time.Now().Before(v.firebase)
|
return !time.Now().Before(v.firebase)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -202,27 +202,27 @@ func (v *visitor) FirebaseTemporarilyDeny() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *visitor) MessageAllowed() bool {
|
func (v *visitor) MessageAllowed() bool {
|
||||||
v.mu.Lock() // limiters could be replaced!
|
v.mu.RLock() // limiters could be replaced!
|
||||||
defer v.mu.Unlock()
|
defer v.mu.RUnlock()
|
||||||
return v.messagesLimiter.Allow()
|
return v.messagesLimiter.Allow()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *visitor) EmailAllowed() bool {
|
func (v *visitor) EmailAllowed() bool {
|
||||||
v.mu.Lock() // limiters could be replaced!
|
v.mu.RLock() // limiters could be replaced!
|
||||||
defer v.mu.Unlock()
|
defer v.mu.RUnlock()
|
||||||
return v.emailsLimiter.Allow()
|
return v.emailsLimiter.Allow()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *visitor) SubscriptionAllowed() bool {
|
func (v *visitor) SubscriptionAllowed() bool {
|
||||||
v.mu.Lock() // limiters could be replaced!
|
v.mu.RLock() // limiters could be replaced!
|
||||||
defer v.mu.Unlock()
|
defer v.mu.RUnlock()
|
||||||
return v.subscriptionLimiter.Allow()
|
return v.subscriptionLimiter.Allow()
|
||||||
}
|
}
|
||||||
|
|
||||||
// AuthAllowed returns true if an auth request can be attempted (> 1 token available)
|
// AuthAllowed returns true if an auth request can be attempted (> 1 token available)
|
||||||
func (v *visitor) AuthAllowed() bool {
|
func (v *visitor) AuthAllowed() bool {
|
||||||
v.mu.Lock() // limiters could be replaced!
|
v.mu.RLock() // limiters could be replaced!
|
||||||
defer v.mu.Unlock()
|
defer v.mu.RUnlock()
|
||||||
if v.authLimiter == nil {
|
if v.authLimiter == nil {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -231,8 +231,8 @@ func (v *visitor) AuthAllowed() bool {
|
||||||
|
|
||||||
// AuthFailed records an auth failure
|
// AuthFailed records an auth failure
|
||||||
func (v *visitor) AuthFailed() {
|
func (v *visitor) AuthFailed() {
|
||||||
v.mu.Lock() // limiters could be replaced!
|
v.mu.RLock() // limiters could be replaced!
|
||||||
defer v.mu.Unlock()
|
defer v.mu.RUnlock()
|
||||||
if v.authLimiter != nil {
|
if v.authLimiter != nil {
|
||||||
v.authLimiter.Allow()
|
v.authLimiter.Allow()
|
||||||
}
|
}
|
||||||
|
@ -240,8 +240,8 @@ func (v *visitor) AuthFailed() {
|
||||||
|
|
||||||
// AccountCreationAllowed returns true if a new account can be created
|
// AccountCreationAllowed returns true if a new account can be created
|
||||||
func (v *visitor) AccountCreationAllowed() bool {
|
func (v *visitor) AccountCreationAllowed() bool {
|
||||||
v.mu.Lock() // limiters could be replaced!
|
v.mu.RLock() // limiters could be replaced!
|
||||||
defer v.mu.Unlock()
|
defer v.mu.RUnlock()
|
||||||
if v.accountLimiter == nil || (v.accountLimiter != nil && v.accountLimiter.Tokens() < 1) {
|
if v.accountLimiter == nil || (v.accountLimiter != nil && v.accountLimiter.Tokens() < 1) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -250,22 +250,22 @@ func (v *visitor) AccountCreationAllowed() bool {
|
||||||
|
|
||||||
// AccountCreated decreases the account limiter. This is to be called after an account was created.
|
// AccountCreated decreases the account limiter. This is to be called after an account was created.
|
||||||
func (v *visitor) AccountCreated() {
|
func (v *visitor) AccountCreated() {
|
||||||
v.mu.Lock() // limiters could be replaced!
|
v.mu.RLock() // limiters could be replaced!
|
||||||
defer v.mu.Unlock()
|
defer v.mu.RUnlock()
|
||||||
if v.accountLimiter != nil {
|
if v.accountLimiter != nil {
|
||||||
v.accountLimiter.Allow()
|
v.accountLimiter.Allow()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *visitor) BandwidthAllowed(bytes int64) bool {
|
func (v *visitor) BandwidthAllowed(bytes int64) bool {
|
||||||
v.mu.Lock() // limiters could be replaced!
|
v.mu.RLock() // limiters could be replaced!
|
||||||
defer v.mu.Unlock()
|
defer v.mu.RUnlock()
|
||||||
return v.bandwidthLimiter.AllowN(bytes)
|
return v.bandwidthLimiter.AllowN(bytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *visitor) RemoveSubscription() {
|
func (v *visitor) RemoveSubscription() {
|
||||||
v.mu.Lock()
|
v.mu.RLock()
|
||||||
defer v.mu.Unlock()
|
defer v.mu.RUnlock()
|
||||||
v.subscriptionLimiter.AllowN(-1)
|
v.subscriptionLimiter.AllowN(-1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -276,20 +276,20 @@ func (v *visitor) Keepalive() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *visitor) BandwidthLimiter() util.Limiter {
|
func (v *visitor) BandwidthLimiter() util.Limiter {
|
||||||
v.mu.Lock() // limiters could be replaced!
|
v.mu.RLock() // limiters could be replaced!
|
||||||
defer v.mu.Unlock()
|
defer v.mu.RUnlock()
|
||||||
return v.bandwidthLimiter
|
return v.bandwidthLimiter
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *visitor) Stale() bool {
|
func (v *visitor) Stale() bool {
|
||||||
v.mu.Lock()
|
v.mu.RLock()
|
||||||
defer v.mu.Unlock()
|
defer v.mu.RUnlock()
|
||||||
return time.Since(v.seen) > visitorExpungeAfter
|
return time.Since(v.seen) > visitorExpungeAfter
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *visitor) Stats() *user.Stats {
|
func (v *visitor) Stats() *user.Stats {
|
||||||
v.mu.Lock() // limiters could be replaced!
|
v.mu.RLock() // limiters could be replaced!
|
||||||
defer v.mu.Unlock()
|
defer v.mu.RUnlock()
|
||||||
return &user.Stats{
|
return &user.Stats{
|
||||||
Messages: v.messagesLimiter.Value(),
|
Messages: v.messagesLimiter.Value(),
|
||||||
Emails: v.emailsLimiter.Value(),
|
Emails: v.emailsLimiter.Value(),
|
||||||
|
@ -297,30 +297,30 @@ func (v *visitor) Stats() *user.Stats {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *visitor) ResetStats() {
|
func (v *visitor) ResetStats() {
|
||||||
v.mu.Lock() // limiters could be replaced!
|
v.mu.RLock() // limiters could be replaced!
|
||||||
defer v.mu.Unlock()
|
defer v.mu.RUnlock()
|
||||||
v.emailsLimiter.Reset()
|
v.emailsLimiter.Reset()
|
||||||
v.messagesLimiter.Reset()
|
v.messagesLimiter.Reset()
|
||||||
}
|
}
|
||||||
|
|
||||||
// User returns the visitor user, or nil if there is none
|
// User returns the visitor user, or nil if there is none
|
||||||
func (v *visitor) User() *user.User {
|
func (v *visitor) User() *user.User {
|
||||||
v.mu.Lock()
|
v.mu.RLock()
|
||||||
defer v.mu.Unlock()
|
defer v.mu.RUnlock()
|
||||||
return v.user // May be nil
|
return v.user // May be nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// IP returns the visitor IP address
|
// IP returns the visitor IP address
|
||||||
func (v *visitor) IP() netip.Addr {
|
func (v *visitor) IP() netip.Addr {
|
||||||
v.mu.Lock()
|
v.mu.RLock()
|
||||||
defer v.mu.Unlock()
|
defer v.mu.RUnlock()
|
||||||
return v.ip
|
return v.ip
|
||||||
}
|
}
|
||||||
|
|
||||||
// Authenticated returns true if a user successfully authenticated
|
// Authenticated returns true if a user successfully authenticated
|
||||||
func (v *visitor) Authenticated() bool {
|
func (v *visitor) Authenticated() bool {
|
||||||
v.mu.Lock()
|
v.mu.RLock()
|
||||||
defer v.mu.Unlock()
|
defer v.mu.RUnlock()
|
||||||
return v.user != nil
|
return v.user != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -338,8 +338,8 @@ func (v *visitor) SetUser(u *user.User) {
|
||||||
// MaybeUserID returns the user ID of the visitor (if any). If this is an anonymous visitor,
|
// MaybeUserID returns the user ID of the visitor (if any). If this is an anonymous visitor,
|
||||||
// an empty string is returned.
|
// an empty string is returned.
|
||||||
func (v *visitor) MaybeUserID() string {
|
func (v *visitor) MaybeUserID() string {
|
||||||
v.mu.Lock()
|
v.mu.RLock()
|
||||||
defer v.mu.Unlock()
|
defer v.mu.RUnlock()
|
||||||
if v.user != nil {
|
if v.user != nil {
|
||||||
return v.user.ID
|
return v.user.ID
|
||||||
}
|
}
|
||||||
|
@ -369,8 +369,8 @@ func (v *visitor) resetLimitersNoLock(messages, emails int64, enqueueUpdate bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *visitor) Limits() *visitorLimits {
|
func (v *visitor) Limits() *visitorLimits {
|
||||||
v.mu.Lock()
|
v.mu.RLock()
|
||||||
defer v.mu.Unlock()
|
defer v.mu.RUnlock()
|
||||||
return v.limitsNoLock()
|
return v.limitsNoLock()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -422,9 +422,9 @@ func configBasedVisitorLimits(conf *Config) *visitorLimits {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *visitor) Info() (*visitorInfo, error) {
|
func (v *visitor) Info() (*visitorInfo, error) {
|
||||||
v.mu.Lock()
|
v.mu.RLock()
|
||||||
info := v.infoLightNoLock()
|
info := v.infoLightNoLock()
|
||||||
v.mu.Unlock()
|
v.mu.RUnlock()
|
||||||
|
|
||||||
// Attachment stats from database
|
// Attachment stats from database
|
||||||
var attachmentsBytesUsed int64
|
var attachmentsBytesUsed int64
|
||||||
|
|
Loading…
Reference in New Issue