c/l/sinbin: init
This adds a sinbin library, useful for temporarily timing out some
processed elements in-memory. We'll use this in the Equinix Shepherd
provisioner loop to keep note of bad hardware reservations.
Change-Id: If68b2c0856364cde70cee68729cfc0203c5a8446
Reviewed-on: https://review.monogon.dev/c/monogon/+/1127
Reviewed-by: Lorenz Brun <lorenz@monogon.tech>
Tested-by: Jenkins CI
diff --git a/cloud/lib/sinbin/BUILD.bazel b/cloud/lib/sinbin/BUILD.bazel
new file mode 100644
index 0000000..df9203f
--- /dev/null
+++ b/cloud/lib/sinbin/BUILD.bazel
@@ -0,0 +1,14 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
+
+go_library(
+ name = "sinbin",
+ srcs = ["sinbin.go"],
+ importpath = "source.monogon.dev/cloud/lib/sinbin",
+ visibility = ["//visibility:public"],
+)
+
+go_test(
+ name = "sinbin_test",
+ srcs = ["sinbin_test.go"],
+ embed = [":sinbin"],
+)
diff --git a/cloud/lib/sinbin/sinbin.go b/cloud/lib/sinbin/sinbin.go
new file mode 100644
index 0000000..7c6bf3c
--- /dev/null
+++ b/cloud/lib/sinbin/sinbin.go
@@ -0,0 +1,90 @@
+// Package sinbin implements a sinbin for naughty processed elements that we wish
+// to time out for a while. This is kept in memory, and effectively implements a
+// simplified version of the Circuit Breaker pattern.
+//
+// “sin bin”, noun, informal: (in sport) a box or bench to which offending
+// players can be sent for a period as a penalty during a game, especially in ice
+// hockey.
+package sinbin
+
+import (
+ "sync"
+ "time"
+)
+
+type entry struct {
+ until time.Time
+}
+
+// A Sinbin contains a set of entries T which are added with a deadline, and will
+// be automatically collected when that deadline expires.
+//
+// The zero value of a Sinbin is ready to use, and can be called from multiple
+// goroutines.
+type Sinbin[T comparable] struct {
+ mu sync.RWMutex
+ bench map[T]*entry
+
+ lastSweep time.Time
+}
+
+func (s *Sinbin[T]) initializeUnlocked() {
+ if s.bench == nil {
+ s.bench = make(map[T]*entry)
+ }
+}
+
+func (s *Sinbin[T]) sweepUnlocked() {
+ if s.lastSweep.Add(time.Minute).After(time.Now()) {
+ return
+ }
+ now := time.Now()
+ for k, e := range s.bench {
+ if now.After(e.until) {
+ delete(s.bench, k)
+ }
+ }
+ s.lastSweep = now
+}
+
+// Add an element 't' to a Sinbin with a given deadline. From now until that
+// deadline Penalized(t) will return true.
+func (s *Sinbin[T]) Add(t T, until time.Time) {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+
+ s.initializeUnlocked()
+ s.sweepUnlocked()
+
+ existing, ok := s.bench[t]
+ if ok {
+ if until.After(existing.until) {
+ existing.until = until
+ }
+ return
+ }
+ s.bench[t] = &entry{
+ until: until,
+ }
+}
+
+// Penalized returns whether the given element is currently sitting on the
+// time-out bench after having been Added previously.
+func (s *Sinbin[T]) Penalized(t T) bool {
+ s.mu.RLock()
+ defer s.mu.RUnlock()
+
+ if s.bench == nil {
+ return false
+ }
+
+ existing, ok := s.bench[t]
+ if !ok {
+ return false
+ }
+ if time.Now().After(existing.until) {
+ delete(s.bench, t)
+ return false
+ }
+ return true
+}
diff --git a/cloud/lib/sinbin/sinbin_test.go b/cloud/lib/sinbin/sinbin_test.go
new file mode 100644
index 0000000..7403bb1
--- /dev/null
+++ b/cloud/lib/sinbin/sinbin_test.go
@@ -0,0 +1,34 @@
+package sinbin
+
+import (
+ "testing"
+ "time"
+)
+
+// TestSinbinBasics performs some basic tests on the Sinbin.
+func TestSinbinBasics(t *testing.T) {
+ var s Sinbin[string]
+
+ if s.Penalized("foo") {
+ t.Errorf("'foo' should not be penalized as it hasn't yet been added")
+ }
+ s.Add("foo", time.Now().Add(-1*time.Second))
+ if s.Penalized("foo") {
+ t.Errorf("'foo' should not be penalized as it has been added with an expired time")
+ }
+ s.Add("bar", time.Now().Add(time.Hour))
+ if !s.Penalized("bar") {
+ t.Errorf("'bar' should be penalized as it has been added with an hour long penalty")
+ }
+
+ // Force sweep.
+ s.lastSweep = time.Now().Add(-1 * time.Hour)
+ s.sweepUnlocked()
+
+ if len(s.bench) != 1 {
+ t.Errorf("there should only be one element on the bench")
+ }
+ if _, ok := s.bench["bar"]; !ok {
+ t.Errorf("the bench should contain 'bar'")
+ }
+}