|
|
@@ -114,6 +114,7 @@ type Matcher struct {
|
|
|
// MatchProperties specifies the compartment, GeoIP, and network topology
|
|
|
// matching roperties of clients and proxies.
|
|
|
type MatchProperties struct {
|
|
|
+ IsPriority bool
|
|
|
CommonCompartmentIDs []ID
|
|
|
PersonalCompartmentIDs []ID
|
|
|
GeoIPData common.GeoIPData
|
|
|
@@ -271,7 +272,7 @@ type MatcherConfig struct {
|
|
|
// Logger is used to log events.
|
|
|
Logger common.Logger
|
|
|
|
|
|
- // Accouncement queue limits.
|
|
|
+ // Announcement queue limits.
|
|
|
AnnouncementLimitEntryCount int
|
|
|
AnnouncementRateLimitQuantity int
|
|
|
AnnouncementRateLimitInterval time.Duration
|
|
|
@@ -281,6 +282,9 @@ type MatcherConfig struct {
|
|
|
OfferLimitEntryCount int
|
|
|
OfferRateLimitQuantity int
|
|
|
OfferRateLimitInterval time.Duration
|
|
|
+
|
|
|
+ // Broker process load limit state callback. See Broker.Config.
|
|
|
+ IsLoadLimiting func() bool
|
|
|
}
|
|
|
|
|
|
// NewMatcher creates a new Matcher.
|
|
|
@@ -431,6 +435,12 @@ func (m *Matcher) Announce(
|
|
|
return nil, nil, errors.TraceNew("unexpected compartment ID count")
|
|
|
}
|
|
|
|
|
|
+ isAnnouncement := true
|
|
|
+ err := m.applyLoadLimit(isAnnouncement)
|
|
|
+ if err != nil {
|
|
|
+ return nil, nil, errors.Trace(err)
|
|
|
+ }
|
|
|
+
|
|
|
announcementEntry := &announcementEntry{
|
|
|
ctx: ctx,
|
|
|
limitIP: getRateLimitIP(proxyIP),
|
|
|
@@ -438,7 +448,7 @@ func (m *Matcher) Announce(
|
|
|
offerChan: make(chan *MatchOffer, 1),
|
|
|
}
|
|
|
|
|
|
- err := m.addAnnouncementEntry(announcementEntry)
|
|
|
+ err = m.addAnnouncementEntry(announcementEntry)
|
|
|
if err != nil {
|
|
|
return nil, nil, errors.Trace(err)
|
|
|
}
|
|
|
@@ -485,6 +495,12 @@ func (m *Matcher) Offer(
|
|
|
return nil, nil, nil, errors.TraceNew("unexpected missing compartment IDs")
|
|
|
}
|
|
|
|
|
|
+ isAnnouncement := false
|
|
|
+ err := m.applyLoadLimit(isAnnouncement)
|
|
|
+ if err != nil {
|
|
|
+ return nil, nil, nil, errors.Trace(err)
|
|
|
+ }
|
|
|
+
|
|
|
offerEntry := &offerEntry{
|
|
|
ctx: ctx,
|
|
|
limitIP: getRateLimitIP(clientIP),
|
|
|
@@ -492,7 +508,7 @@ func (m *Matcher) Offer(
|
|
|
answerChan: make(chan *answerInfo, 1),
|
|
|
}
|
|
|
|
|
|
- err := m.addOfferEntry(offerEntry)
|
|
|
+ err = m.addOfferEntry(offerEntry)
|
|
|
if err != nil {
|
|
|
return nil, nil, nil, errors.Trace(err)
|
|
|
}
|
|
|
@@ -775,16 +791,27 @@ func (m *Matcher) matchOffer(offerEntry *offerEntry) (*announcementEntry, int) {
|
|
|
|
|
|
var bestMatch *announcementEntry
|
|
|
bestMatchIndex := -1
|
|
|
+ bestMatchIsPriority := false
|
|
|
bestMatchNAT := false
|
|
|
|
|
|
candidateIndex := -1
|
|
|
for {
|
|
|
|
|
|
- announcementEntry := matchIterator.getNext()
|
|
|
+ announcementEntry, isPriority := matchIterator.getNext()
|
|
|
if announcementEntry == nil {
|
|
|
break
|
|
|
}
|
|
|
|
|
|
+ if !isPriority && bestMatchIsPriority {
|
|
|
+
|
|
|
+ // There is a priority match, but it wasn't bestMatchNAT and we
|
|
|
+ // continued to iterate. Now that isPriority is false, we're past the
|
|
|
+ // end of the priority items, so stop looking for any best NAT match
|
|
|
+ // and return the previous priority match. When there are zero
|
|
|
+ // priority items to begin with, this case should not be hit.
|
|
|
+ break
|
|
|
+ }
|
|
|
+
|
|
|
candidateIndex += 1
|
|
|
|
|
|
// Skip and remove this announcement if its deadline has already
|
|
|
@@ -829,8 +856,8 @@ func (m *Matcher) matchOffer(offerEntry *offerEntry) (*announcementEntry, int) {
|
|
|
|
|
|
bestMatch = announcementEntry
|
|
|
bestMatchIndex = candidateIndex
|
|
|
+ bestMatchIsPriority = isPriority
|
|
|
bestMatchNAT = matchNAT
|
|
|
-
|
|
|
}
|
|
|
|
|
|
// Stop as soon as we have the best possible match, or have reached
|
|
|
@@ -847,6 +874,79 @@ func (m *Matcher) matchOffer(offerEntry *offerEntry) (*announcementEntry, int) {
|
|
|
return bestMatch, bestMatchIndex
|
|
|
}
|
|
|
|
|
|
+// applyLoadLimit checks if the broker process is in the load limiting state
|
|
|
+// and, in order to reduce load, determines if new proxy announces or client
|
|
|
+// offers should be rejected immediately instead of enqueued.
|
|
|
+func (m *Matcher) applyLoadLimit(isAnnouncement bool) error {
|
|
|
+
|
|
|
+ if m.config.IsLoadLimiting == nil || !m.config.IsLoadLimiting() {
|
|
|
+ return nil
|
|
|
+ }
|
|
|
+
|
|
|
+ // Acquire the queue locks only when in the load limit state, and in the
|
|
|
+ // same order as matchAllOffers.
|
|
|
+
|
|
|
+ m.announcementQueueMutex.Lock()
|
|
|
+ defer m.announcementQueueMutex.Unlock()
|
|
|
+ m.offerQueueMutex.Lock()
|
|
|
+ defer m.offerQueueMutex.Unlock()
|
|
|
+
|
|
|
+ announcementLen := m.announcementQueue.getLen()
|
|
|
+ offerLen := m.offerQueue.Len()
|
|
|
+
|
|
|
+ // When the load limit had been reached, and assuming the broker process
|
|
|
+ // is running only an in-proxy broker, it's likely, in practise, that
|
|
|
+ // only one of the two queues has hundreds of thousands of entries while
|
|
|
+ // the other has few, and there are no matches clearing the queue.
|
|
|
+ //
|
|
|
+ // Instead of simply rejecting all enqueue requests, allow the request
|
|
|
+ // type, announce or offer, that is in shorter supply as these are likely
|
|
|
+ // to match and draw down the larger queue. This attempts to make
|
|
|
+ // productive use of enqueued items, and also attempts to avoid simply
|
|
|
+ // emptying both queues -- as will happen in any case due to timeouts --
|
|
|
+ // and then have the same larger queue refill again after the load limit
|
|
|
+ // state exits.
|
|
|
+ //
|
|
|
+ // This approach assumes some degree of slack in available system memory
|
|
|
+ // and CPU in the load limiting state, similar to how the tunnel server
|
|
|
+ // continues to operate existing tunnels in the same state.
|
|
|
+ //
|
|
|
+ // The heuristic below of allowing when less than half the size of the
|
|
|
+ // larger queue puts a cap on the amount the shorter queue can continue
|
|
|
+ // to grow in the load limiting state, in the worst case.
|
|
|
+ //
|
|
|
+ // Limitation: in some scenarios that are expected to be rare, it can
|
|
|
+ // happen that allowed requests don't result in a match and memory
|
|
|
+ // consumption continues to grow, leading to a broker process OOM kill.
|
|
|
+
|
|
|
+ var allow bool
|
|
|
+ if isAnnouncement {
|
|
|
+ allow = announcementLen < offerLen/2
|
|
|
+ } else {
|
|
|
+ allow = offerLen < announcementLen/2
|
|
|
+ }
|
|
|
+ if allow {
|
|
|
+ return nil
|
|
|
+ }
|
|
|
+
|
|
|
+ // Do not return a MatcherLimitError, as is done in applyIPLimits. A
|
|
|
+ // MatcherLimitError results in a Response.Limited error response, which
|
|
|
+ // causes a proxy to back off and a client to abort its dial; but in
|
|
|
+ // neither case is the broker client reset. The error returned here will
|
|
|
+ // result in a fast 404 response to the proxy or client, which will
|
|
|
+ // instead trigger a broker client reset, and a chance of moving to a
|
|
|
+ // different broker that is not overloaded.
|
|
|
+ //
|
|
|
+ // Limitation: the 404 response won't be distinguishable, in client or
|
|
|
+ // proxy diagnostics, from other error conditions.
|
|
|
+ //
|
|
|
+ // TODO: add a new Response.LoadLimited flag which the proxy/client can
|
|
|
+ // use use log a distinct error and also ensure that it doesn't reselect
|
|
|
+ // the same broker again in the broker client reset random selection.
|
|
|
+
|
|
|
+ return errors.TraceNew("load limited")
|
|
|
+}
|
|
|
+
|
|
|
// MatcherLimitError is the error type returned by Announce or Offer when the
|
|
|
// caller has exceeded configured queue entry or rate limits.
|
|
|
type MatcherLimitError struct {
|
|
|
@@ -861,9 +961,11 @@ func (e MatcherLimitError) Error() string {
|
|
|
return e.err.Error()
|
|
|
}
|
|
|
|
|
|
-func (m *Matcher) applyLimits(isAnnouncement bool, limitIP string, proxyID ID) error {
|
|
|
+// applyIPLimits checks per-proxy or per-client -- as determined by peer IP
|
|
|
+// address -- rate limits and queue entry limits.
|
|
|
+func (m *Matcher) applyIPLimits(isAnnouncement bool, limitIP string, proxyID ID) error {
|
|
|
|
|
|
- // Assumes the m.announcementQueueMutex or m.offerQueue mutex is locked.
|
|
|
+ // Assumes m.announcementQueueMutex or m.offerQueueMutex is locked.
|
|
|
|
|
|
var entryCountByIP map[string]int
|
|
|
var queueRateLimiters *lrucache.Cache
|
|
|
@@ -946,7 +1048,7 @@ func (m *Matcher) addAnnouncementEntry(announcementEntry *announcementEntry) err
|
|
|
// Ensure no single peer IP can enqueue a large number of entries or
|
|
|
// rapidly enqueue beyond the configured rate.
|
|
|
isAnnouncement := true
|
|
|
- err := m.applyLimits(
|
|
|
+ err := m.applyIPLimits(
|
|
|
isAnnouncement, announcementEntry.limitIP, announcementEntry.announcement.ProxyID)
|
|
|
if err != nil {
|
|
|
return errors.Trace(err)
|
|
|
@@ -1029,7 +1131,7 @@ func (m *Matcher) addOfferEntry(offerEntry *offerEntry) error {
|
|
|
// Ensure no single peer IP can enqueue a large number of entries or
|
|
|
// rapidly enqueue beyond the configured rate.
|
|
|
isAnnouncement := false
|
|
|
- err := m.applyLimits(
|
|
|
+ err := m.applyIPLimits(
|
|
|
isAnnouncement, offerEntry.limitIP, ID{})
|
|
|
if err != nil {
|
|
|
return errors.Trace(err)
|
|
|
@@ -1107,9 +1209,10 @@ func getRateLimitIP(strIP string) string {
|
|
|
// matching a specified list of compartment IDs. announcementMultiQueue and
|
|
|
// its underlying data structures are not safe for concurrent access.
|
|
|
type announcementMultiQueue struct {
|
|
|
- commonCompartmentQueues map[ID]*announcementCompartmentQueue
|
|
|
- personalCompartmentQueues map[ID]*announcementCompartmentQueue
|
|
|
- totalEntries int
|
|
|
+ priorityCommonCompartmentQueues map[ID]*announcementCompartmentQueue
|
|
|
+ commonCompartmentQueues map[ID]*announcementCompartmentQueue
|
|
|
+ personalCompartmentQueues map[ID]*announcementCompartmentQueue
|
|
|
+ totalEntries int
|
|
|
}
|
|
|
|
|
|
// announcementCompartmentQueue is a single compartment queue within an
|
|
|
@@ -1120,6 +1223,7 @@ type announcementMultiQueue struct {
|
|
|
// matches may be possible.
|
|
|
type announcementCompartmentQueue struct {
|
|
|
isCommonCompartment bool
|
|
|
+ isPriority bool
|
|
|
compartmentID ID
|
|
|
entries *list.List
|
|
|
unlimitedNATCount int
|
|
|
@@ -1131,11 +1235,10 @@ type announcementCompartmentQueue struct {
|
|
|
// subset of announcementMultiQueue compartment queues. Concurrent
|
|
|
// announcementMatchIterators are not supported.
|
|
|
type announcementMatchIterator struct {
|
|
|
- multiQueue *announcementMultiQueue
|
|
|
- isCommonCompartments bool
|
|
|
- compartmentQueues []*announcementCompartmentQueue
|
|
|
- compartmentIDs []ID
|
|
|
- nextEntries []*list.Element
|
|
|
+ multiQueue *announcementMultiQueue
|
|
|
+ compartmentQueues []*announcementCompartmentQueue
|
|
|
+ compartmentIDs []ID
|
|
|
+ nextEntries []*list.Element
|
|
|
}
|
|
|
|
|
|
// announcementQueueReference represents the queue position for a given
|
|
|
@@ -1148,8 +1251,9 @@ type announcementQueueReference struct {
|
|
|
|
|
|
func newAnnouncementMultiQueue() *announcementMultiQueue {
|
|
|
return &announcementMultiQueue{
|
|
|
- commonCompartmentQueues: make(map[ID]*announcementCompartmentQueue),
|
|
|
- personalCompartmentQueues: make(map[ID]*announcementCompartmentQueue),
|
|
|
+ priorityCommonCompartmentQueues: make(map[ID]*announcementCompartmentQueue),
|
|
|
+ commonCompartmentQueues: make(map[ID]*announcementCompartmentQueue),
|
|
|
+ personalCompartmentQueues: make(map[ID]*announcementCompartmentQueue),
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -1179,22 +1283,31 @@ func (q *announcementMultiQueue) enqueue(announcementEntry *announcementEntry) e
|
|
|
return errors.TraceNew("announcement must specify exactly one compartment ID")
|
|
|
}
|
|
|
|
|
|
+ isPriority := announcementEntry.announcement.Properties.IsPriority
|
|
|
+
|
|
|
isCommonCompartment := true
|
|
|
var compartmentID ID
|
|
|
var compartmentQueues map[ID]*announcementCompartmentQueue
|
|
|
if len(commonCompartmentIDs) > 0 {
|
|
|
compartmentID = commonCompartmentIDs[0]
|
|
|
compartmentQueues = q.commonCompartmentQueues
|
|
|
+ if isPriority {
|
|
|
+ compartmentQueues = q.priorityCommonCompartmentQueues
|
|
|
+ }
|
|
|
} else {
|
|
|
isCommonCompartment = false
|
|
|
compartmentID = personalCompartmentIDs[0]
|
|
|
compartmentQueues = q.personalCompartmentQueues
|
|
|
+ if isPriority {
|
|
|
+ return errors.TraceNew("priority not supported for personal compartments")
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
compartmentQueue, ok := compartmentQueues[compartmentID]
|
|
|
if !ok {
|
|
|
compartmentQueue = &announcementCompartmentQueue{
|
|
|
isCommonCompartment: isCommonCompartment,
|
|
|
+ isPriority: isPriority,
|
|
|
compartmentID: compartmentID,
|
|
|
entries: list.New(),
|
|
|
}
|
|
|
@@ -1250,9 +1363,14 @@ func (r *announcementQueueReference) dequeue() bool {
|
|
|
|
|
|
if r.compartmentQueue.entries.Len() == 0 {
|
|
|
// Remove empty compartment queue.
|
|
|
- queues := r.multiQueue.commonCompartmentQueues
|
|
|
- if !r.compartmentQueue.isCommonCompartment {
|
|
|
- queues = r.multiQueue.personalCompartmentQueues
|
|
|
+ queues := r.multiQueue.personalCompartmentQueues
|
|
|
+ if r.compartmentQueue.isCommonCompartment {
|
|
|
+ if r.compartmentQueue.isPriority {
|
|
|
+ queues = r.multiQueue.priorityCommonCompartmentQueues
|
|
|
+
|
|
|
+ } else {
|
|
|
+ queues = r.multiQueue.commonCompartmentQueues
|
|
|
+ }
|
|
|
}
|
|
|
delete(queues, r.compartmentQueue.compartmentID)
|
|
|
}
|
|
|
@@ -1270,8 +1388,7 @@ func (q *announcementMultiQueue) startMatching(
|
|
|
compartmentIDs []ID) *announcementMatchIterator {
|
|
|
|
|
|
iter := &announcementMatchIterator{
|
|
|
- multiQueue: q,
|
|
|
- isCommonCompartments: isCommonCompartments,
|
|
|
+ multiQueue: q,
|
|
|
}
|
|
|
|
|
|
// Find the matching compartment queues and initialize iteration over
|
|
|
@@ -1280,16 +1397,29 @@ func (q *announcementMultiQueue) startMatching(
|
|
|
// maxCompartmentIDs, as enforced in
|
|
|
// ClientOfferRequest.ValidateAndGetLogFields).
|
|
|
|
|
|
- compartmentQueues := q.commonCompartmentQueues
|
|
|
- if !isCommonCompartments {
|
|
|
- compartmentQueues = q.personalCompartmentQueues
|
|
|
+ // Priority queues, when in use, must all be added to the beginning of
|
|
|
+ // iter.compartmentQueues in order to ensure that the iteration logic in
|
|
|
+ // getNext visits all priority items first.
|
|
|
+
|
|
|
+ var compartmentQueuesList []map[ID]*announcementCompartmentQueue
|
|
|
+ if isCommonCompartments {
|
|
|
+ compartmentQueuesList = append(
|
|
|
+ compartmentQueuesList,
|
|
|
+ q.priorityCommonCompartmentQueues,
|
|
|
+ q.commonCompartmentQueues)
|
|
|
+ } else {
|
|
|
+ compartmentQueuesList = append(
|
|
|
+ compartmentQueuesList,
|
|
|
+ q.personalCompartmentQueues)
|
|
|
}
|
|
|
|
|
|
- for _, ID := range compartmentIDs {
|
|
|
- if compartmentQueue, ok := compartmentQueues[ID]; ok {
|
|
|
- iter.compartmentQueues = append(iter.compartmentQueues, compartmentQueue)
|
|
|
- iter.compartmentIDs = append(iter.compartmentIDs, ID)
|
|
|
- iter.nextEntries = append(iter.nextEntries, compartmentQueue.entries.Front())
|
|
|
+ for _, compartmentQueues := range compartmentQueuesList {
|
|
|
+ for _, ID := range compartmentIDs {
|
|
|
+ if compartmentQueue, ok := compartmentQueues[ID]; ok {
|
|
|
+ iter.compartmentQueues = append(iter.compartmentQueues, compartmentQueue)
|
|
|
+ iter.compartmentIDs = append(iter.compartmentIDs, ID)
|
|
|
+ iter.nextEntries = append(iter.nextEntries, compartmentQueue.entries.Front())
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -1327,10 +1457,16 @@ func (iter *announcementMatchIterator) getNATCounts() (int, int, int) {
|
|
|
// are not supported during iteration. Iteration and dequeue should all be
|
|
|
// performed with a lock over the entire announcementMultiQueue, and with
|
|
|
// only one concurrent announcementMatchIterator.
|
|
|
-func (iter *announcementMatchIterator) getNext() *announcementEntry {
|
|
|
+//
|
|
|
+// getNext returns a nil *announcementEntry when there are no more items.
|
|
|
+// getNext also returns an isPriority flag, indicating the announcement is a
|
|
|
+// priority candidate. All priority candidates are guaranteed to be returned
|
|
|
+// before any non-priority candidates.
|
|
|
+func (iter *announcementMatchIterator) getNext() (*announcementEntry, bool) {
|
|
|
|
|
|
// Assumes announcements are enqueued in announcementEntry.ctx.Deadline
|
|
|
- // order.
|
|
|
+ // order. Also assumes that any priority queues are all at the front of
|
|
|
+ // iter.compartmentQueues.
|
|
|
|
|
|
// Select the oldest item, by deadline, from all the candidate queue head
|
|
|
// items. This operation is linear in the number of matching compartment
|
|
|
@@ -1338,6 +1474,10 @@ func (iter *announcementMatchIterator) getNext() *announcementEntry {
|
|
|
// compartment IDs (no more than maxCompartmentIDs, as enforced in
|
|
|
// ClientOfferRequest.ValidateAndGetLogFields).
|
|
|
//
|
|
|
+ // When there are priority candidates, they are selected first, regardless
|
|
|
+ // of the deadlines of non-priority candidates. Multiple priority
|
|
|
+ // candidates are processed in FIFO deadline order.
|
|
|
+ //
|
|
|
// A potential future enhancement is to add more iterator state to track
|
|
|
// which queue has the next oldest time to select on the following
|
|
|
// getNext call. Another potential enhancement is to remove fully
|
|
|
@@ -1345,14 +1485,22 @@ func (iter *announcementMatchIterator) getNext() *announcementEntry {
|
|
|
|
|
|
var selectedCandidate *announcementEntry
|
|
|
selectedIndex := -1
|
|
|
+ selectedPriority := false
|
|
|
|
|
|
for i := 0; i < len(iter.compartmentQueues); i++ {
|
|
|
if iter.nextEntries[i] == nil {
|
|
|
continue
|
|
|
}
|
|
|
+ isPriority := iter.compartmentQueues[i].isPriority
|
|
|
+ if selectedPriority && !isPriority {
|
|
|
+ // Ignore older of non-priority entries when there are priority
|
|
|
+ // candidates.
|
|
|
+ break
|
|
|
+ }
|
|
|
if selectedCandidate == nil {
|
|
|
selectedCandidate = iter.nextEntries[i].Value.(*announcementEntry)
|
|
|
selectedIndex = i
|
|
|
+ selectedPriority = isPriority
|
|
|
} else {
|
|
|
candidate := iter.nextEntries[i].Value.(*announcementEntry)
|
|
|
deadline, deadlineOk := candidate.ctx.Deadline()
|
|
|
@@ -1360,6 +1508,7 @@ func (iter *announcementMatchIterator) getNext() *announcementEntry {
|
|
|
if deadlineOk && selectedDeadlineOk && deadline.Before(selectedDeadline) {
|
|
|
selectedCandidate = candidate
|
|
|
selectedIndex = i
|
|
|
+ selectedPriority = isPriority
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
@@ -1371,5 +1520,5 @@ func (iter *announcementMatchIterator) getNext() *announcementEntry {
|
|
|
iter.nextEntries[selectedIndex] = iter.nextEntries[selectedIndex].Next()
|
|
|
}
|
|
|
|
|
|
- return selectedCandidate
|
|
|
+ return selectedCandidate, selectedPriority
|
|
|
}
|