WIP Reject 507s after a while
This commit is contained in:
		
							parent
							
								
									cd3429842b
								
							
						
					
					
						commit
						4d22ccc7f6
					
				
					 5 changed files with 45 additions and 13 deletions
				
			
		|  | @ -127,5 +127,5 @@ var ( | ||||||
| 	errHTTPInternalError                             = &errHTTP{50001, http.StatusInternalServerError, "internal server error", "", nil} | 	errHTTPInternalError                             = &errHTTP{50001, http.StatusInternalServerError, "internal server error", "", nil} | ||||||
| 	errHTTPInternalErrorInvalidPath                  = &errHTTP{50002, http.StatusInternalServerError, "internal server error: invalid path", "", nil} | 	errHTTPInternalErrorInvalidPath                  = &errHTTP{50002, http.StatusInternalServerError, "internal server error: invalid path", "", nil} | ||||||
| 	errHTTPInternalErrorMissingBaseURL               = &errHTTP{50003, http.StatusInternalServerError, "internal server error: base-url must be be configured for this feature", "https://ntfy.sh/docs/config/", nil} | 	errHTTPInternalErrorMissingBaseURL               = &errHTTP{50003, http.StatusInternalServerError, "internal server error: base-url must be be configured for this feature", "https://ntfy.sh/docs/config/", nil} | ||||||
| 	errHTTPInsufficientStorage                       = &errHTTP{50701, http.StatusInsufficientStorage, "internal server error: cannot publish to UnifiedPush topic without previously active subscriber", "", nil} | 	errHTTPInsufficientStorageUnifiedPush            = &errHTTP{50701, http.StatusInsufficientStorage, "cannot publish to UnifiedPush topic without previously active subscriber", "", nil} | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  | @ -602,7 +602,7 @@ func (s *Server) handlePublishWithoutResponse(r *http.Request, v *visitor) (*mes | ||||||
| 		// Rate-Topics header). The 5xx response is because some app servers (in particular Mastodon) will remove | 		// Rate-Topics header). The 5xx response is because some app servers (in particular Mastodon) will remove | ||||||
| 		// the subscription as invalid if any 400-499 code (except 429/408) is returned. | 		// the subscription as invalid if any 400-499 code (except 429/408) is returned. | ||||||
| 		// See https://github.com/mastodon/mastodon/blob/730bb3e211a84a2f30e3e2bbeae3f77149824a68/app/workers/web/push_notification_worker.rb#L35-L46 | 		// See https://github.com/mastodon/mastodon/blob/730bb3e211a84a2f30e3e2bbeae3f77149824a68/app/workers/web/push_notification_worker.rb#L35-L46 | ||||||
| 		return nil, errHTTPInsufficientStorage.With(t) | 		return nil, errHTTPInsufficientStorageUnifiedPush.With(t) | ||||||
| 	} else if !util.ContainsIP(s.config.VisitorRequestExemptIPAddrs, v.ip) && !vrate.MessageAllowed() { | 	} else if !util.ContainsIP(s.config.VisitorRequestExemptIPAddrs, v.ip) && !vrate.MessageAllowed() { | ||||||
| 		return nil, errHTTPTooManyRequestsLimitMessages.With(t) | 		return nil, errHTTPTooManyRequestsLimitMessages.With(t) | ||||||
| 	} else if email != "" && !vrate.EmailAllowed() { | 	} else if email != "" && !vrate.EmailAllowed() { | ||||||
|  | @ -680,6 +680,9 @@ func (s *Server) handlePublish(w http.ResponseWriter, r *http.Request, v *visito | ||||||
| func (s *Server) handlePublishMatrix(w http.ResponseWriter, r *http.Request, v *visitor) error { | func (s *Server) handlePublishMatrix(w http.ResponseWriter, r *http.Request, v *visitor) error { | ||||||
| 	_, err := s.handlePublishWithoutResponse(r, v) | 	_, err := s.handlePublishWithoutResponse(r, v) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
|  | 		if e, ok := err.(*errHTTP); ok && e.HTTPCode == errHTTPInsufficientStorageUnifiedPush.HTTPCode { | ||||||
|  | 			return writeMatrixResponse(w, e.rejectedPushKey) | ||||||
|  | 		} | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| 	return writeMatrixSuccess(w) | 	return writeMatrixSuccess(w) | ||||||
|  | @ -1036,6 +1039,9 @@ func (s *Server) handleSubscribeHTTP(w http.ResponseWriter, r *http.Request, v * | ||||||
| 		case <-time.After(s.config.KeepaliveInterval): | 		case <-time.After(s.config.KeepaliveInterval): | ||||||
| 			logvr(v, r).Tag(tagSubscribe).Trace("Sending keepalive message") | 			logvr(v, r).Tag(tagSubscribe).Trace("Sending keepalive message") | ||||||
| 			v.Keepalive() | 			v.Keepalive() | ||||||
|  | 			for _, t := range topics { | ||||||
|  | 				t.Keepalive() | ||||||
|  | 			} | ||||||
| 			if err := sub(v, newKeepaliveMessage(topicsStr)); err != nil { // Send keepalive message | 			if err := sub(v, newKeepaliveMessage(topicsStr)); err != nil { // Send keepalive message | ||||||
| 				return err | 				return err | ||||||
| 			} | 			} | ||||||
|  | @ -1123,6 +1129,9 @@ func (s *Server) handleSubscribeWS(w http.ResponseWriter, r *http.Request, v *vi | ||||||
| 				return &websocket.CloseError{Code: websocket.CloseNormalClosure, Text: "subscription was canceled"} | 				return &websocket.CloseError{Code: websocket.CloseNormalClosure, Text: "subscription was canceled"} | ||||||
| 			case <-time.After(s.config.KeepaliveInterval): | 			case <-time.After(s.config.KeepaliveInterval): | ||||||
| 				v.Keepalive() | 				v.Keepalive() | ||||||
|  | 				for _, t := range topics { | ||||||
|  | 					t.Keepalive() | ||||||
|  | 				} | ||||||
| 				if err := ping(); err != nil { | 				if err := ping(); err != nil { | ||||||
| 					return err | 					return err | ||||||
| 				} | 				} | ||||||
|  |  | ||||||
|  | @ -3,6 +3,7 @@ package server | ||||||
| import ( | import ( | ||||||
| 	"heckel.io/ntfy/log" | 	"heckel.io/ntfy/log" | ||||||
| 	"strings" | 	"strings" | ||||||
|  | 	"time" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| func (s *Server) execManager() { | func (s *Server) execManager() { | ||||||
|  | @ -34,16 +35,20 @@ func (s *Server) execManager() { | ||||||
| 			s.mu.Lock() | 			s.mu.Lock() | ||||||
| 			defer s.mu.Unlock() | 			defer s.mu.Unlock() | ||||||
| 			for _, t := range s.topics { | 			for _, t := range s.topics { | ||||||
| 				subs := t.SubscribersCount() | 				subs, lastAccess := t.Stats() | ||||||
| 				log.Tag(tagManager).With(t).Trace("- topic %s: %d subscribers", t.ID, subs) | 				ev := log.Tag(tagManager).With(t) | ||||||
| 				msgs, exists := messageCounts[t.ID] | 				if t.Stale() { | ||||||
| 				if t.Stale() && (!exists || msgs == 0) { | 					if ev.IsTrace() { | ||||||
| 					log.Tag(tagManager).With(t).Trace("Deleting empty topic %s", t.ID) | 						ev.Trace("- topic %s: Deleting stale topic (%d subscribers, accessed %s)", t.ID, subs, lastAccess.Format(time.RFC822)) | ||||||
|  | 					} | ||||||
| 					emptyTopics++ | 					emptyTopics++ | ||||||
| 					delete(s.topics, t.ID) | 					delete(s.topics, t.ID) | ||||||
| 					continue | 				} else { | ||||||
|  | 					if ev.IsTrace() { | ||||||
|  | 						ev.Trace("- topic %s: %d subscribers, accessed %s", t.ID, subs, lastAccess.Format(time.RFC822)) | ||||||
|  | 					} | ||||||
|  | 					subscribers += subs | ||||||
| 				} | 				} | ||||||
| 				subscribers += subs |  | ||||||
| 			} | 			} | ||||||
| 		}). | 		}). | ||||||
| 		Debug("Removed %d empty topic(s)", emptyTopics) | 		Debug("Removed %d empty topic(s)", emptyTopics) | ||||||
|  |  | ||||||
|  | @ -126,6 +126,7 @@ func newRequestFromMatrixJSON(r *http.Request, baseURL string, messageLimit int) | ||||||
| 	if r.Header.Get("X-Forwarded-For") != "" { | 	if r.Header.Get("X-Forwarded-For") != "" { | ||||||
| 		newRequest.Header.Set("X-Forwarded-For", r.Header.Get("X-Forwarded-For")) | 		newRequest.Header.Set("X-Forwarded-For", r.Header.Get("X-Forwarded-For")) | ||||||
| 	} | 	} | ||||||
|  | 	newRequest.Header.Set("X-Matrix-Pushkey", pushKey) | ||||||
| 	return newRequest, nil | 	return newRequest, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -4,6 +4,11 @@ import ( | ||||||
| 	"heckel.io/ntfy/log" | 	"heckel.io/ntfy/log" | ||||||
| 	"math/rand" | 	"math/rand" | ||||||
| 	"sync" | 	"sync" | ||||||
|  | 	"time" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | const ( | ||||||
|  | 	topicExpiryDuration = 6 * time.Hour | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // topic represents a channel to which subscribers can subscribe, and publishers | // topic represents a channel to which subscribers can subscribe, and publishers | ||||||
|  | @ -12,6 +17,7 @@ type topic struct { | ||||||
| 	ID          string | 	ID          string | ||||||
| 	subscribers map[int]*topicSubscriber | 	subscribers map[int]*topicSubscriber | ||||||
| 	rateVisitor *visitor | 	rateVisitor *visitor | ||||||
|  | 	lastAccess  time.Time | ||||||
| 	mu          sync.RWMutex | 	mu          sync.RWMutex | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -29,6 +35,7 @@ func newTopic(id string) *topic { | ||||||
| 	return &topic{ | 	return &topic{ | ||||||
| 		ID:          id, | 		ID:          id, | ||||||
| 		subscribers: make(map[int]*topicSubscriber), | 		subscribers: make(map[int]*topicSubscriber), | ||||||
|  | 		lastAccess:  time.Now(), | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -42,6 +49,7 @@ func (t *topic) Subscribe(s subscriber, userID string, cancel func()) int { | ||||||
| 		subscriber: s, | 		subscriber: s, | ||||||
| 		cancel:     cancel, | 		cancel:     cancel, | ||||||
| 	} | 	} | ||||||
|  | 	t.lastAccess = time.Now() | ||||||
| 	return subscriberID | 	return subscriberID | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -51,13 +59,14 @@ func (t *topic) Stale() bool { | ||||||
| 	if t.rateVisitor != nil && !t.rateVisitor.Stale() { | 	if t.rateVisitor != nil && !t.rateVisitor.Stale() { | ||||||
| 		return false | 		return false | ||||||
| 	} | 	} | ||||||
| 	return len(t.subscribers) == 0 | 	return len(t.subscribers) == 0 && time.Since(t.lastAccess) > topicExpiryDuration | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (t *topic) SetRateVisitor(v *visitor) { | func (t *topic) SetRateVisitor(v *visitor) { | ||||||
| 	t.mu.Lock() | 	t.mu.Lock() | ||||||
| 	defer t.mu.Unlock() | 	defer t.mu.Unlock() | ||||||
| 	t.rateVisitor = v | 	t.rateVisitor = v | ||||||
|  | 	t.lastAccess = time.Now() | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (t *topic) RateVisitor() *visitor { | func (t *topic) RateVisitor() *visitor { | ||||||
|  | @ -96,15 +105,23 @@ func (t *topic) Publish(v *visitor, m *message) error { | ||||||
| 		} else { | 		} else { | ||||||
| 			logvm(v, m).Tag(tagPublish).Trace("No stream or WebSocket subscribers, not forwarding") | 			logvm(v, m).Tag(tagPublish).Trace("No stream or WebSocket subscribers, not forwarding") | ||||||
| 		} | 		} | ||||||
|  | 		t.Keepalive() | ||||||
| 	}() | 	}() | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // SubscribersCount returns the number of subscribers to this topic | // Stats returns the number of subscribers and last access to this topic | ||||||
| func (t *topic) SubscribersCount() int { | func (t *topic) Stats() (int, time.Time) { | ||||||
| 	t.mu.RLock() | 	t.mu.RLock() | ||||||
| 	defer t.mu.RUnlock() | 	defer t.mu.RUnlock() | ||||||
| 	return len(t.subscribers) | 	return len(t.subscribers), t.lastAccess | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Keepalive sets the last access time and ensures that Stale does not return true | ||||||
|  | func (t *topic) Keepalive() { | ||||||
|  | 	t.mu.Lock() | ||||||
|  | 	defer t.mu.Unlock() | ||||||
|  | 	t.lastAccess = time.Now() | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // CancelSubscribers calls the cancel function for all subscribers, forcing | // CancelSubscribers calls the cancel function for all subscribers, forcing | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue