tangled
alpha
login
or
join now
lesbian.skin
/
glimit
4
fork
atom
this repo has no description
4
fork
atom
overview
issues
pulls
pipelines
🎨 Refactor the code
Joris Hartog
2 years ago
e896b03b
55b5bfdf
+127
-103
2 changed files
expand all
collapse all
unified
split
src
glimit
actor.gleam
glimit.gleam
+7
-103
src/glimit.gleam
···
35
35
//// ```
36
36
////
37
37
38
38
-
import gleam/dict
39
38
import gleam/erlang/process.{type Subject}
40
40
-
import gleam/list
41
39
import gleam/option.{type Option, None, Some}
42
42
-
import gleam/otp/actor
43
43
-
import gleam/result
44
44
-
import glimit/utils
45
45
-
46
46
-
/// The messages that the actor can receive.
47
47
-
///
48
48
-
pub type Message(id) {
49
49
-
/// Stop the actor.
50
50
-
Shutdown
51
51
-
52
52
-
/// Mark a hit for a given identifier.
53
53
-
Hit(identifier: id, reply_with: Subject(Result(Nil, Nil)))
54
54
-
}
40
40
+
import glimit/actor
55
41
56
42
/// The rate limiter's public interface.
57
43
///
58
44
pub type RateLimiter(a, b, id) {
59
45
RateLimiter(
60
60
-
subject: Subject(Message(id)),
46
46
+
subject: Subject(actor.Message(id)),
61
47
handler: fn(a) -> b,
62
48
identifier: fn(a) -> id,
63
49
)
64
50
}
65
51
66
66
-
/// A rate limiter.
52
52
+
/// A builder for configuring the rate limiter.
67
53
///
68
54
pub type RateLimiterBuilder(a, b, id) {
69
55
RateLimiterBuilder(
···
75
61
)
76
62
}
77
63
78
78
-
/// The actor state.
79
79
-
///
80
80
-
type State(a, b, id) {
81
81
-
RateLimiterState(
82
82
-
hit_log: dict.Dict(id, List(Int)),
83
83
-
per_second: Option(Int),
84
84
-
per_minute: Option(Int),
85
85
-
per_hour: Option(Int),
86
86
-
)
87
87
-
}
88
88
-
89
89
-
fn handle_message(
90
90
-
message: Message(id),
91
91
-
state: State(a, b, id),
92
92
-
) -> actor.Next(Message(id), State(a, b, id)) {
93
93
-
case message {
94
94
-
Shutdown -> actor.Stop(process.Normal)
95
95
-
Hit(identifier, client) -> {
96
96
-
// Update hit log
97
97
-
let timestamp = utils.now()
98
98
-
let hits =
99
99
-
state.hit_log
100
100
-
|> dict.get(identifier)
101
101
-
|> result.unwrap([])
102
102
-
|> list.filter(fn(hit) { hit >= timestamp - 60 * 60 })
103
103
-
|> list.append([timestamp])
104
104
-
let hit_log =
105
105
-
state.hit_log
106
106
-
|> dict.insert(identifier, hits)
107
107
-
let state = RateLimiterState(..state, hit_log: hit_log)
108
108
-
109
109
-
// Check rate limits
110
110
-
// TODO: optimize into a single loop
111
111
-
let hits_last_hour = hits |> list.length()
112
112
-
113
113
-
let hits_last_minute =
114
114
-
hits
115
115
-
|> list.filter(fn(hit) { hit >= timestamp - 60 })
116
116
-
|> list.length()
117
117
-
118
118
-
let hits_last_second =
119
119
-
hits
120
120
-
|> list.filter(fn(hit) { hit >= timestamp - 1 })
121
121
-
|> list.length()
122
122
-
123
123
-
let limit_reached = {
124
124
-
case state.per_hour {
125
125
-
Some(limit) -> hits_last_hour > limit
126
126
-
None -> False
127
127
-
}
128
128
-
|| case state.per_minute {
129
129
-
Some(limit) -> hits_last_minute > limit
130
130
-
None -> False
131
131
-
}
132
132
-
|| case state.per_second {
133
133
-
Some(limit) -> hits_last_second > limit
134
134
-
None -> False
135
135
-
}
136
136
-
}
137
137
-
138
138
-
case limit_reached {
139
139
-
True -> process.send(client, Error(Nil))
140
140
-
False -> process.send(client, Ok(Nil))
141
141
-
}
142
142
-
143
143
-
actor.continue(state)
144
144
-
}
145
145
-
}
146
146
-
}
147
147
-
148
64
/// Create a new rate limiter builder.
149
65
///
150
66
pub fn new() -> RateLimiterBuilder(a, b, id) {
···
208
124
/// function or handler function is missing.
209
125
///
210
126
pub fn build(config: RateLimiterBuilder(a, b, id)) -> RateLimiter(a, b, id) {
211
211
-
let state =
212
212
-
RateLimiterState(
213
213
-
hit_log: dict.new(),
214
214
-
per_second: config.per_second,
215
215
-
per_minute: config.per_minute,
216
216
-
per_hour: config.per_hour,
217
217
-
)
218
218
-
219
127
RateLimiter(
220
220
-
subject: case actor.start(state, handle_message) {
128
128
+
subject: case
129
129
+
actor.new(config.per_second, config.per_minute, config.per_hour)
130
130
+
{
221
131
Ok(subject) -> subject
222
132
Error(_) -> panic as "Failed to start rate limiter actor"
223
133
},
···
237
147
pub fn apply(func: fn(a) -> b, limiter: RateLimiter(a, b, id)) -> fn(a) -> b {
238
148
fn(input: a) -> b {
239
149
let identifier = limiter.identifier(input)
240
240
-
case actor.call(limiter.subject, Hit(identifier, _), 10) {
150
150
+
case actor.hit(limiter.subject, identifier) {
241
151
Ok(Nil) -> func(input)
242
152
Error(Nil) -> limiter.handler(input)
243
153
}
244
154
}
245
155
}
246
246
-
247
247
-
/// Stop the rate limiter agent.
248
248
-
///
249
249
-
pub fn stop(limiter: RateLimiter(a, b, id)) {
250
250
-
actor.send(limiter.subject, Shutdown)
251
251
-
}
+120
src/glimit/actor.gleam
···
1
1
+
//// The rate limiter actor.
2
2
+
////
3
3
+
4
4
+
import gleam/dict
5
5
+
import gleam/erlang/process.{type Subject}
6
6
+
import gleam/list
7
7
+
import gleam/option.{type Option, None, Some}
8
8
+
import gleam/otp/actor
9
9
+
import gleam/result
10
10
+
import glimit/utils
11
11
+
12
12
+
/// The messages that the actor can receive.
13
13
+
///
14
14
+
pub type Message(id) {
15
15
+
/// Stop the actor.
16
16
+
Shutdown
17
17
+
18
18
+
/// Mark a hit for a given identifier.
19
19
+
Hit(identifier: id, reply_with: Subject(Result(Nil, Nil)))
20
20
+
}
21
21
+
22
22
+
/// The actor state.
23
23
+
///
24
24
+
type State(a, b, id) {
25
25
+
RateLimiterState(
26
26
+
hit_log: dict.Dict(id, List(Int)),
27
27
+
per_second: Option(Int),
28
28
+
per_minute: Option(Int),
29
29
+
per_hour: Option(Int),
30
30
+
)
31
31
+
}
32
32
+
33
33
+
fn handle_message(
34
34
+
message: Message(id),
35
35
+
state: State(a, b, id),
36
36
+
) -> actor.Next(Message(id), State(a, b, id)) {
37
37
+
case message {
38
38
+
Shutdown -> actor.Stop(process.Normal)
39
39
+
Hit(identifier, client) -> {
40
40
+
// Update hit log
41
41
+
let timestamp = utils.now()
42
42
+
let hits =
43
43
+
state.hit_log
44
44
+
|> dict.get(identifier)
45
45
+
|> result.unwrap([])
46
46
+
|> list.filter(fn(hit) { hit >= timestamp - 60 * 60 })
47
47
+
|> list.append([timestamp])
48
48
+
let hit_log =
49
49
+
state.hit_log
50
50
+
|> dict.insert(identifier, hits)
51
51
+
let state = RateLimiterState(..state, hit_log: hit_log)
52
52
+
53
53
+
// Check rate limits
54
54
+
// TODO: optimize into a single loop
55
55
+
let hits_last_hour = hits |> list.length()
56
56
+
57
57
+
let hits_last_minute =
58
58
+
hits
59
59
+
|> list.filter(fn(hit) { hit >= timestamp - 60 })
60
60
+
|> list.length()
61
61
+
62
62
+
let hits_last_second =
63
63
+
hits
64
64
+
|> list.filter(fn(hit) { hit >= timestamp - 1 })
65
65
+
|> list.length()
66
66
+
67
67
+
let limit_reached = {
68
68
+
case state.per_hour {
69
69
+
Some(limit) -> hits_last_hour > limit
70
70
+
None -> False
71
71
+
}
72
72
+
|| case state.per_minute {
73
73
+
Some(limit) -> hits_last_minute > limit
74
74
+
None -> False
75
75
+
}
76
76
+
|| case state.per_second {
77
77
+
Some(limit) -> hits_last_second > limit
78
78
+
None -> False
79
79
+
}
80
80
+
}
81
81
+
82
82
+
case limit_reached {
83
83
+
True -> process.send(client, Error(Nil))
84
84
+
False -> process.send(client, Ok(Nil))
85
85
+
}
86
86
+
87
87
+
actor.continue(state)
88
88
+
}
89
89
+
}
90
90
+
}
91
91
+
92
92
+
/// Create a new rate limiter actor.
93
93
+
///
94
94
+
pub fn new(
95
95
+
per_second: Option(Int),
96
96
+
per_minute: Option(Int),
97
97
+
per_hour: Option(Int),
98
98
+
) -> Result(Subject(Message(id)), Nil) {
99
99
+
let state =
100
100
+
RateLimiterState(
101
101
+
hit_log: dict.new(),
102
102
+
per_second: per_second,
103
103
+
per_minute: per_minute,
104
104
+
per_hour: per_hour,
105
105
+
)
106
106
+
actor.start(state, handle_message)
107
107
+
|> result.nil_error
108
108
+
}
109
109
+
110
110
+
/// Log a hit for a given identifier.
111
111
+
///
112
112
+
pub fn hit(subject: Subject(Message(id)), identifier: id) -> Result(Nil, Nil) {
113
113
+
actor.call(subject, Hit(identifier, _), 10)
114
114
+
}
115
115
+
116
116
+
/// Stop the actor.
117
117
+
///
118
118
+
pub fn stop(subject: Subject(Message(id))) {
119
119
+
actor.send(subject, Shutdown)
120
120
+
}