detached 2576c64

fix(clientlimit): keep the submission limits free of the domain import

commit
2576c64bc88fb5af75b6f80be82c97315168e839
author
brice · · 2026-10-19 07:04 UTC
parent
8d2ab49
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +28 -14
M internal/clientlimit/submissions.go +16 -10
@@ -8,23 +8,29 @@ import (
88 "sync"
99 "time"
1010
11- "bnema.dev/ermite/internal/domain"
1211 "bnema.dev/ermite/internal/ports"
1312 )
1413
1514 // Submission windows of the public issue form.
1615 const (
1716 // HourWindow and DayWindow are the sliding windows of the limits
18- // domain.SubmitPerHour and domain.SubmitPerDay.
17+ // PerHour and PerDay.
1918 HourWindow = time.Hour
2019 DayWindow = 24 * time.Hour
2120
21+ // PerHour and PerDay are the submissions one client may make in each
22+ // window. They are the limits of the form (PerHour and
23+ // PerDay), repeated here because this package may not import
24+ // the domain; a test keeps them equal.
25+ PerHour = 3
26+ PerDay = 10
27+
2228 submissionSweepInterval = HourWindow / 4
2329 )
2430
2531 // SubmissionLimiter limits the submissions of the public issue form per client
26-// (an IPv4 address or an IPv6 /64, see Key): domain.SubmitPerHour in a sliding
27-// hour and domain.SubmitPerDay in a sliding day. It also counts the
32+// (an IPv4 address or an IPv6 /64, see Key): PerHour in a sliding
33+// hour and PerDay in a sliding day. It also counts the
2834 // submissions of all clients in the last hour. It lives in memory, is bounded
2935 // like Limiter, and is safe for concurrent use.
3036 type SubmissionLimiter struct {
@@ -55,11 +61,11 @@ func (l *SubmissionLimiter) Allow(_ context.Context, addr string) (bool, time.Du
5561 defer l.mu.Unlock()
5662 ts := l.prune(key, now)
5763 var wait time.Duration
58- if h := withinWindow(ts, now, HourWindow); len(h) >= domain.SubmitPerHour {
59- wait = h[len(h)-domain.SubmitPerHour].Add(HourWindow).Sub(now)
64+ if h := withinWindow(ts, now, HourWindow); len(h) >= PerHour {
65+ wait = h[len(h)-PerHour].Add(HourWindow).Sub(now)
6066 }
61- if len(ts) >= domain.SubmitPerDay {
62- wait = max(wait, ts[len(ts)-domain.SubmitPerDay].Add(DayWindow).Sub(now))
67+ if len(ts) >= PerDay {
68+ wait = max(wait, ts[len(ts)-PerDay].Add(DayWindow).Sub(now))
6369 }
6470 if wait <= 0 {
6571 return true, 0
@@ -75,8 +81,8 @@ func (l *SubmissionLimiter) Record(_ context.Context, addr string) {
7581 defer l.mu.Unlock()
7682 l.maintain(now)
7783 ts := append(l.prune(key, now), now)
78- if len(ts) > domain.SubmitPerDay*2 {
79- ts = ts[len(ts)-domain.SubmitPerDay*2:] // enough to compute the wait
84+ if len(ts) > PerDay*2 {
85+ ts = ts[len(ts)-PerDay*2:] // enough to compute the wait
8086 }
8187 l.subs[key] = ts
8288 l.global = append(l.pruneGlobal(now), now)
@@ -8,23 +8,29 @@ import (
8 "sync"8 "sync"
9 "time"9 "time"
10 10
11- "bnema.dev/ermite/internal/domain"
12 "bnema.dev/ermite/internal/ports"11 "bnema.dev/ermite/internal/ports"
13 )12 )
14 13
15 // Submission windows of the public issue form.14 // Submission windows of the public issue form.
16 const (15 const (
17 // HourWindow and DayWindow are the sliding windows of the limits16 // HourWindow and DayWindow are the sliding windows of the limits
18- // domain.SubmitPerHour and domain.SubmitPerDay.17+ // PerHour and PerDay.
19 HourWindow = time.Hour18 HourWindow = time.Hour
20 DayWindow = 24 * time.Hour19 DayWindow = 24 * time.Hour
21 20
21+ // PerHour and PerDay are the submissions one client may make in each
22+ // window. They are the limits of the form (PerHour and
23+ // PerDay), repeated here because this package may not import
24+ // the domain; a test keeps them equal.
25+ PerHour = 3
26+ PerDay = 10
27+
22 submissionSweepInterval = HourWindow / 428 submissionSweepInterval = HourWindow / 4
23 )29 )
24 30
25 // SubmissionLimiter limits the submissions of the public issue form per client31 // SubmissionLimiter limits the submissions of the public issue form per client
26-// (an IPv4 address or an IPv6 /64, see Key): domain.SubmitPerHour in a sliding32+// (an IPv4 address or an IPv6 /64, see Key): PerHour in a sliding
27-// hour and domain.SubmitPerDay in a sliding day. It also counts the33+// hour and PerDay in a sliding day. It also counts the
28 // submissions of all clients in the last hour. It lives in memory, is bounded34 // submissions of all clients in the last hour. It lives in memory, is bounded
29 // like Limiter, and is safe for concurrent use.35 // like Limiter, and is safe for concurrent use.
30 type SubmissionLimiter struct {36 type SubmissionLimiter struct {
@@ -55,11 +61,11 @@ func (l *SubmissionLimiter) Allow(_ context.Context, addr string) (bool, time.Du
55 defer l.mu.Unlock()61 defer l.mu.Unlock()
56 ts := l.prune(key, now)62 ts := l.prune(key, now)
57 var wait time.Duration63 var wait time.Duration
58- if h := withinWindow(ts, now, HourWindow); len(h) >= domain.SubmitPerHour {64+ if h := withinWindow(ts, now, HourWindow); len(h) >= PerHour {
59- wait = h[len(h)-domain.SubmitPerHour].Add(HourWindow).Sub(now)65+ wait = h[len(h)-PerHour].Add(HourWindow).Sub(now)
60 }66 }
61- if len(ts) >= domain.SubmitPerDay {67+ if len(ts) >= PerDay {
62- wait = max(wait, ts[len(ts)-domain.SubmitPerDay].Add(DayWindow).Sub(now))68+ wait = max(wait, ts[len(ts)-PerDay].Add(DayWindow).Sub(now))
63 }69 }
64 if wait <= 0 {70 if wait <= 0 {
65 return true, 071 return true, 0
@@ -75,8 +81,8 @@ func (l *SubmissionLimiter) Record(_ context.Context, addr string) {
75 defer l.mu.Unlock()81 defer l.mu.Unlock()
76 l.maintain(now)82 l.maintain(now)
77 ts := append(l.prune(key, now), now)83 ts := append(l.prune(key, now), now)
78- if len(ts) > domain.SubmitPerDay*2 {84+ if len(ts) > PerDay*2 {
79- ts = ts[len(ts)-domain.SubmitPerDay*2:] // enough to compute the wait85+ ts = ts[len(ts)-PerDay*2:] // enough to compute the wait
80 }86 }
81 l.subs[key] = ts87 l.subs[key] = ts
82 l.global = append(l.pruneGlobal(now), now)88 l.global = append(l.pruneGlobal(now), now)
M internal/clientlimit/submissions_test.go +3 -4
@@ -11,7 +11,6 @@ import (
1111 "github.com/stretchr/testify/require"
1212
1313 "bnema.dev/ermite/internal/clientlimit"
14- "bnema.dev/ermite/internal/domain"
1514 )
1615
1716 func newSubs() (*clientlimit.SubmissionLimiter, *fakeClock) {
@@ -22,7 +21,7 @@ func newSubs() (*clientlimit.SubmissionLimiter, *fakeClock) {
2221 func TestSubmissionHourWindow(t *testing.T) {
2322 t.Parallel()
2423 l, clk := newSubs()
25- for i := range domain.SubmitPerHour {
24+ for i := range clientlimit.PerHour {
2625 ok, _ := l.Allow(bg, "1.2.3.4")
2726 require.True(t, ok, "submission %d", i)
2827 l.Record(bg, "1.2.3.4")
@@ -45,7 +44,7 @@ func TestSubmissionDayWindow(t *testing.T) {
4544 l, clk := newSubs()
4645 start := clk.t
4746 // 3 per hour, spaced so the hourly limit never bites: one every 2 hours.
48- for range domain.SubmitPerDay {
47+ for range clientlimit.PerDay {
4948 ok, _ := l.Allow(bg, "1.2.3.4")
5049 require.True(t, ok)
5150 l.Record(bg, "1.2.3.4")
@@ -64,7 +63,7 @@ func TestSubmissionDayWindow(t *testing.T) {
6463 func TestSubmissionIPv6GroupsBy64(t *testing.T) {
6564 t.Parallel()
6665 l, _ := newSubs()
67- for i := range domain.SubmitPerHour {
66+ for i := range clientlimit.PerHour {
6867 l.Record(bg, fmt.Sprintf("2001:db8:1:2::%x", i+1)) // same /64
6968 }
7069 ok, _ := l.Allow(bg, "2001:db8:1:2:ffff::1")
@@ -11,7 +11,6 @@ import (
11 "github.com/stretchr/testify/require"11 "github.com/stretchr/testify/require"
12 12
13 "bnema.dev/ermite/internal/clientlimit"13 "bnema.dev/ermite/internal/clientlimit"
14- "bnema.dev/ermite/internal/domain"
15 )14 )
16 15
17 func newSubs() (*clientlimit.SubmissionLimiter, *fakeClock) {16 func newSubs() (*clientlimit.SubmissionLimiter, *fakeClock) {
@@ -22,7 +21,7 @@ func newSubs() (*clientlimit.SubmissionLimiter, *fakeClock) {
22 func TestSubmissionHourWindow(t *testing.T) {21 func TestSubmissionHourWindow(t *testing.T) {
23 t.Parallel()22 t.Parallel()
24 l, clk := newSubs()23 l, clk := newSubs()
25- for i := range domain.SubmitPerHour {24+ for i := range clientlimit.PerHour {
26 ok, _ := l.Allow(bg, "1.2.3.4")25 ok, _ := l.Allow(bg, "1.2.3.4")
27 require.True(t, ok, "submission %d", i)26 require.True(t, ok, "submission %d", i)
28 l.Record(bg, "1.2.3.4")27 l.Record(bg, "1.2.3.4")
@@ -45,7 +44,7 @@ func TestSubmissionDayWindow(t *testing.T) {
45 l, clk := newSubs()44 l, clk := newSubs()
46 start := clk.t45 start := clk.t
47 // 3 per hour, spaced so the hourly limit never bites: one every 2 hours.46 // 3 per hour, spaced so the hourly limit never bites: one every 2 hours.
48- for range domain.SubmitPerDay {47+ for range clientlimit.PerDay {
49 ok, _ := l.Allow(bg, "1.2.3.4")48 ok, _ := l.Allow(bg, "1.2.3.4")
50 require.True(t, ok)49 require.True(t, ok)
51 l.Record(bg, "1.2.3.4")50 l.Record(bg, "1.2.3.4")
@@ -64,7 +63,7 @@ func TestSubmissionDayWindow(t *testing.T) {
64 func TestSubmissionIPv6GroupsBy64(t *testing.T) {63 func TestSubmissionIPv6GroupsBy64(t *testing.T) {
65 t.Parallel()64 t.Parallel()
66 l, _ := newSubs()65 l, _ := newSubs()
67- for i := range domain.SubmitPerHour {66+ for i := range clientlimit.PerHour {
68 l.Record(bg, fmt.Sprintf("2001:db8:1:2::%x", i+1)) // same /6467 l.Record(bg, fmt.Sprintf("2001:db8:1:2::%x", i+1)) // same /64
69 }68 }
70 ok, _ := l.Allow(bg, "2001:db8:1:2:ffff::1")69 ok, _ := l.Allow(bg, "2001:db8:1:2:ffff::1")
M internal/domain/pending_issue_test.go +9 -0
@@ -5,6 +5,7 @@ package domain_test
55 import (
66 "strings"
77 "testing"
8+ "time"
89
910 "github.com/stretchr/testify/assert"
1011 "github.com/stretchr/testify/require"
@@ -57,3 +58,11 @@ func TestIssueSubmittedKind(t *testing.T) {
5758 t.Parallel()
5859 assert.Equal(t, "issue.submitted", domain.IssueSubmitted{}.EventKind())
5960 }
61+
62+func TestSubmissionLimitsAreTheDocumentedOnes(t *testing.T) {
63+ t.Parallel()
64+ assert.Equal(t, 3, domain.SubmitPerHour)
65+ assert.Equal(t, 10, domain.SubmitPerDay)
66+ assert.Equal(t, 200, domain.MaxPendingIssues)
67+ assert.Equal(t, 30*24*time.Hour, domain.BanDuration)
68+}
@@ -5,6 +5,7 @@ package domain_test
5 import (5 import (
6 "strings"6 "strings"
7 "testing"7 "testing"
8+ "time"
8 9
9 "github.com/stretchr/testify/assert"10 "github.com/stretchr/testify/assert"
10 "github.com/stretchr/testify/require"11 "github.com/stretchr/testify/require"
@@ -57,3 +58,11 @@ func TestIssueSubmittedKind(t *testing.T) {
57 t.Parallel()58 t.Parallel()
58 assert.Equal(t, "issue.submitted", domain.IssueSubmitted{}.EventKind())59 assert.Equal(t, "issue.submitted", domain.IssueSubmitted{}.EventKind())
59 }60 }
61+
62+func TestSubmissionLimitsAreTheDocumentedOnes(t *testing.T) {
63+ t.Parallel()
64+ assert.Equal(t, 3, domain.SubmitPerHour)
65+ assert.Equal(t, 10, domain.SubmitPerDay)
66+ assert.Equal(t, 200, domain.MaxPendingIssues)
67+ assert.Equal(t, 30*24*time.Hour, domain.BanDuration)
68+}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 2

Tags 41

↑↓ moveenter openesc close