Skip to content

Commit

Permalink
add autodelete + ttl
Browse files Browse the repository at this point in the history
  • Loading branch information
alex-goodisman committed Apr 5, 2024
1 parent 3a8f826 commit c821a05
Showing 1 changed file with 52 additions and 4 deletions.
56 changes: 52 additions & 4 deletions lib/denylist/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,16 @@ import (
"encoding/json"
"net/http"
"sync"
"time"
)

const denylistTTL = 10 * time.Minute

type deletionTimeout struct {
timer *time.Timer
doneCh chan struct{}
}

// CollectionEndpoint serves the endpoints for the whole Denylist at /denylist
func CollectionEndpoint(denylist *sync.Map) func(http.ResponseWriter, *http.Request) {
return func(response http.ResponseWriter, request *http.Request) {
Expand Down Expand Up @@ -72,14 +80,54 @@ func getDenylistEntry(response http.ResponseWriter, request *http.Request, denyl

// PUT /denylist/...
func createDenylistEntry(response http.ResponseWriter, request *http.Request, denylist *sync.Map) {

// We want to make the denylist entries autodelete after a TTL. However, if a second PUT operation
// happens for the same entry, we want to refresh its remaining time. This produces three possible cases:
// 1. The entry is never refreshed. The timer expires, and deletes its own entry and itself.
// 2. The entry is refreshed with time still left on the timer. The timer is stopped and deleted, and a new timer is created.
// 3. The entry is refreshed as the timer expires, which means it fails to become stopped and therefore deletes itself.
// After this happens, a new timer is created.

id := request.URL.Path
_, exists := denylist.Load(id)
existing, exists := denylist.Load(id)

// handle the case where an entry already exists
if exists {
response.WriteHeader(http.StatusNoContent)
return
if existingTimeout, ok := existing.(deletionTimeout); ok {
// try to stop the existing deletion timer
if !existingTimeout.timer.Stop() {
// couldn't stop the timer because it already ran out of time and started the deletion operation
// instead, simply wait for that to complete
<-existingTimeout.doneCh
}
// regardless of whether the timer deleted on its own or not, we want to replace it with a new one.
// We don't return here, and instead recreate a new timer as though it never existed.
} else {
http.Error(response, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return

}
}

// Create the new entry. The channel needs to be buffered because most of the time, nobody is listening to it, and
// it will be deleted as soon as the timer runs out. But to handle the case where a refresh overlapped with the timer's expiry,
// we do need to publish regardless.
timeoutDoneCh := make(chan struct{}, 1)
newTimeout := deletionTimeout{
// in a separate coroutine, wait the TTL, then delete this entry
timer: time.AfterFunc(denylistTTL, func() {
denylist.Delete(id)
// this will signal any pending refreshes that they can go ahead.
timeoutDoneCh <- struct{}{}
}),
doneCh: timeoutDoneCh,
}

denylist.Store(id, true)
// Store the new entry. If an old entry existed and was stopped, we overwrite it here.
// If this happened concurrently with the old entry expiring on its own, then it deleted itself and we replace it here.
// This creates a short interruption between the deletion and recreation where the entry is missing from the denylist, during
// which time oplog entries will not be denied. This is expected behavior when the refresh happens at the same time as the expiry.
denylist.Store(id, newTimeout)
response.WriteHeader(http.StatusCreated)
}

Expand Down

0 comments on commit c821a05

Please sign in to comment.