hookshot 0.3.0 → 0.3.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +101 -16
- data/lib/hookshot.rb +167 -10
- data/lib/hookshot/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 50a5202fd2cf12d2977095bd54a10af1284bf61b
|
4
|
+
data.tar.gz: 094bb40322e173db3a428db8bfdb61b40c7a7388
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6233b77583a4bb4ea005f2d6b7e43b5fa1503edca65e1af206ce3507a489de1e3866b8a3f3d817c902a79eaab9bee3510f9e8560f0554448e47a421c09fede65
|
7
|
+
data.tar.gz: 3d3b2b0aee201ec4bfedb319f19db5a409c0667bc6c548ba39cb6825e21ae2933d5f0f08643c41fb91a86d985e5c8343238ed576a86471a67a80c4547a62ffd6
|
data/README.md
CHANGED
@@ -1,29 +1,114 @@
|
|
1
|
-
# Hookshot
|
1
|
+
# Hookshot
|
2
2
|
|
3
|
-
|
3
|
+
Hookshot is a rubygem for submitting jobs to the
|
4
|
+
[Hookshot](https://github.com/Shopify/hookshot) webhook delivery engine.
|
4
5
|
|
5
6
|
## Installation
|
6
7
|
|
7
|
-
|
8
|
+
Hookshot is on rubygems, just add the latest version to your gemfile. Setup of
|
9
|
+
the `hookshot` server is separate and more complex, so [go read about that
|
10
|
+
first](https://github.com/shopify/hookshot).
|
8
11
|
|
9
|
-
|
12
|
+
## Usage
|
10
13
|
|
11
|
-
|
14
|
+
#### Initialize a connection
|
12
15
|
|
13
|
-
|
16
|
+
```ruby
|
17
|
+
require 'hookshot'
|
18
|
+
require 'redis'
|
19
|
+
hookshot = Hookshot.new(Redis.new(port: 6379, host: 'localhost'))
|
20
|
+
```
|
14
21
|
|
15
|
-
|
22
|
+
#### Enqueue a webhook
|
16
23
|
|
17
|
-
|
24
|
+
```ruby
|
25
|
+
hookshot.enqueue(
|
26
|
+
url: 'http://localhost:8080/post',
|
27
|
+
headers: {"X-My-Header" => "value"},
|
28
|
+
context: "42",
|
29
|
+
payload: "request body")
|
30
|
+
```
|
18
31
|
|
19
|
-
|
32
|
+
#### Enqueue a webhook with a delay
|
33
|
+
|
34
|
+
You can enqueue a job but have it activate only after a specified delay.
|
35
|
+
|
36
|
+
```ruby
|
37
|
+
hookshot.enqueue_in(60, # seconds
|
38
|
+
url: 'http://localhost:8080/post',
|
39
|
+
headers: {"X-My-Header" => "value"},
|
40
|
+
context: "42",
|
41
|
+
payload: "request body")
|
42
|
+
```
|
43
|
+
|
44
|
+
#### Read back failed jobs
|
45
|
+
|
46
|
+
Jobs that fail many times in a row are returned back to the application.
|
47
|
+
Specifically, the `context` value passed in via the `enqueue*` methods is
|
48
|
+
returned.
|
49
|
+
|
50
|
+
`get_next_failure` returns two values: the number of failures so far for this
|
51
|
+
job, and the `context` passed in with the job. If `nfailures` is equal to
|
52
|
+
`Hookshot::FINAL_FAILURE`, the job will not be retried. However, if `nfailures`
|
53
|
+
is any other value, the job will still be retried; this is just an advisory
|
54
|
+
because the job has been failing for at least 24 hours.
|
55
|
+
|
56
|
+
```ruby
|
57
|
+
loop {
|
58
|
+
nfailures, context = hookshot.get_next_failure
|
59
|
+
if nfailures == Hookshot::FINAL_FAILURE
|
60
|
+
delete_webhook_subscription(context)
|
61
|
+
end
|
62
|
+
}
|
63
|
+
```
|
64
|
+
|
65
|
+
#### Check queue stats
|
66
|
+
|
67
|
+
Hookshot writes a lot of statistics to statsd/datadog, but to quickly check the
|
68
|
+
current queue sizes, use `queue_stats`.
|
69
|
+
|
70
|
+
```ruby
|
71
|
+
hookshot.queue_stats
|
72
|
+
# => { pending: 42, delayed: 42, failures: 42 }
|
73
|
+
```
|
74
|
+
|
75
|
+
### Blacklist and Whitelist
|
76
|
+
|
77
|
+
Hookshot provides a manual blacklist and a manual whitelist:
|
78
|
+
|
79
|
+
* Jobs for any domain in the blacklist are automatically rejected by hookshot.
|
80
|
+
* Throttles for any domain in the whitelist are automatically allocated the maximum allowed throughput.
|
81
|
+
|
82
|
+
The format of the domain should be the full domain. The port should not be included if it is `80` or `443`.
|
83
|
+
|
84
|
+
Examples:
|
85
|
+
|
86
|
+
| URL | Domain |
|
87
|
+
|---------------------------|---------------------|
|
88
|
+
| http://example.com/post | example.com |
|
89
|
+
| https://example.com/post | example.com |
|
90
|
+
| http://example.com:8000 | example.com:8000 |
|
91
|
+
| http://example.com:80 | example.com |
|
92
|
+
|
93
|
+
#### Check the lists
|
94
|
+
|
95
|
+
```ruby
|
96
|
+
hookshot.blacklist
|
97
|
+
#=> ["example.com"]
|
98
|
+
hookshot.whitelist
|
99
|
+
#=> ["google.com", "shopify.com"]
|
100
|
+
```
|
101
|
+
|
102
|
+
#### Add an item to the list
|
20
103
|
|
21
|
-
|
104
|
+
```ruby
|
105
|
+
hookshot.blacklist!("example.com")
|
106
|
+
hookshot.whitelist!("google.com")
|
107
|
+
```
|
22
108
|
|
23
|
-
|
109
|
+
#### Remove an item from the list
|
24
110
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
5. Create new Pull Request
|
111
|
+
````ruby
|
112
|
+
hookshot.remove_blacklist("example.com")
|
113
|
+
hookshot.remove_whitelist("google.com")
|
114
|
+
```
|
data/lib/hookshot.rb
CHANGED
@@ -9,15 +9,57 @@ class Hookshot
|
|
9
9
|
NEW_JOBS_LIST = "#{PREFIX}:jobs"
|
10
10
|
DELAYED_SET = "#{PREFIX}:delayed"
|
11
11
|
FAILURES_LIST = "#{PREFIX}:failures"
|
12
|
-
|
12
|
+
BLACKLIST = "#{PREFIX}:throttle:blacklist"
|
13
|
+
WHITELIST = "#{PREFIX}:throttle:whitelist"
|
13
14
|
|
15
|
+
# Special sentinel value returned from `get_next_failure` when the job will no
|
16
|
+
# longer be retried.
|
14
17
|
FINAL_FAILURE = -1
|
15
18
|
|
19
|
+
# Jobs are retried for up to 48 hours. Though we delete the job info when
|
20
|
+
# hookshot is done with each job, it's still a good idea to clean up any keys
|
21
|
+
# that have managed to stick around somehow.
|
22
|
+
JOB_KEY_LIFETIME = 86400 * 4
|
23
|
+
|
24
|
+
# Raised by `get_next_failure` if there are no pending failures.
|
25
|
+
FailureQueueEmpty = Class.new(StandardError)
|
26
|
+
|
16
27
|
attr_reader :redis
|
28
|
+
|
29
|
+
# Hookshot expects to be initialized with an instance of `Redis` provided by
|
30
|
+
# the `redis` rubygem.
|
31
|
+
#
|
32
|
+
# Example:
|
33
|
+
#
|
34
|
+
# require 'hookshot'
|
35
|
+
# require 'redis'
|
36
|
+
# hookshot = Hookshot.new(Redis.new(port: 6379, host: 'localhost'))
|
17
37
|
def initialize(redis)
|
18
38
|
@redis = redis
|
19
39
|
end
|
20
40
|
|
41
|
+
# enqueue takes a URL, a hash of headers, a payload (request body), and a
|
42
|
+
# `context` value. It submits these values to hookshot for processing.
|
43
|
+
#
|
44
|
+
# The `context` value can be any string, and will be returned to you via
|
45
|
+
# `get_next_failure` if the job can't be successfully completed after about 48
|
46
|
+
# hours of retries.
|
47
|
+
#
|
48
|
+
# In Shopify, we pass our `WebhookSubscription` object ID for the `context`
|
49
|
+
# value, so that we can notify merchants when their webhooks are failing, and
|
50
|
+
# delete subscriptions that fail consistently.
|
51
|
+
#
|
52
|
+
# `activate_at` is an optional parameter that specifies a number of seconds to
|
53
|
+
# wait before making this job active in hookshot. You should normally call
|
54
|
+
# `enqueue_in` rather than pass `activate_at` explicitly.
|
55
|
+
#
|
56
|
+
# Example:
|
57
|
+
#
|
58
|
+
# hookshot.enqueue(
|
59
|
+
# url: 'http://localhost:8080/post',
|
60
|
+
# headers: {"X-My-Header" => "value"},
|
61
|
+
# context: "42",
|
62
|
+
# payload: "request body")
|
21
63
|
def enqueue(url:, headers:, context:, payload:, activate_at: nil)
|
22
64
|
uuid = SecureRandom.uuid
|
23
65
|
redis.pipelined do
|
@@ -28,6 +70,7 @@ class Hookshot
|
|
28
70
|
"context", context,
|
29
71
|
"payload", payload,
|
30
72
|
"failures", 0)
|
73
|
+
redis.expire(job_key(uuid), JOB_KEY_LIFETIME)
|
31
74
|
if activate_at
|
32
75
|
redis.zadd(DELAYED_SET, activate_at, uuid)
|
33
76
|
else
|
@@ -38,6 +81,16 @@ class Hookshot
|
|
38
81
|
uuid
|
39
82
|
end
|
40
83
|
|
84
|
+
# enqeueue_in calls enqueue with an `activate_at` parameter to delay the job's
|
85
|
+
# execution.
|
86
|
+
#
|
87
|
+
# Example:
|
88
|
+
#
|
89
|
+
# hookshot.enqueue_in(60, # seconds
|
90
|
+
# url: 'http://localhost:8080/post',
|
91
|
+
# headers: {"X-My-Header" => "value"},
|
92
|
+
# context: "42",
|
93
|
+
# payload: "request body")
|
41
94
|
def enqueue_in(duration, url:, headers:, context:, payload:)
|
42
95
|
enqueue_time = (Time.now + duration).to_i
|
43
96
|
enqueue(
|
@@ -48,9 +101,32 @@ class Hookshot
|
|
48
101
|
activate_at: enqueue_time)
|
49
102
|
end
|
50
103
|
|
104
|
+
# Jobs that fail many times in a row are returned back to the application.
|
105
|
+
# Specifically, the `context` value passed in via the `enqueue*` methods is
|
106
|
+
# returned.
|
107
|
+
#
|
108
|
+
# `get_next_failure` returns two values: the number of failures so far for
|
109
|
+
# this job, and the `context` passed in with the job. If `nfailures` is equal
|
110
|
+
# to `Hookshot::FINAL_FAILURE`, the job will not be retried. However, if
|
111
|
+
# `nfailures` is any other value, the job will still be retried; this is just
|
112
|
+
# an advisory because the job has been failing for at least 24 hours.
|
113
|
+
#
|
114
|
+
# This method is non-blocking: if there is no item present in the failures
|
115
|
+
# queue, it will raise FailureQueueEmpty.
|
116
|
+
#
|
117
|
+
# Example:
|
118
|
+
#
|
119
|
+
# loop {
|
120
|
+
# nfailures, context = hookshot.get_next_failure
|
121
|
+
# if nfailures == Hookshot::FINAL_FAILURE
|
122
|
+
# delete_webhook_subscription(context)
|
123
|
+
# end
|
124
|
+
# }
|
51
125
|
def get_next_failure
|
52
126
|
# block indefinitely waiting for the next failure.
|
53
|
-
line = redis.
|
127
|
+
line = redis.lpop(FAILURES_LIST)
|
128
|
+
raise FailureQueueEmpty if line.nil?
|
129
|
+
|
54
130
|
nfailures, failed_id = line.split('|', 2)
|
55
131
|
|
56
132
|
if nfailures.to_i == 0 || failed_id.empty?
|
@@ -60,14 +136,13 @@ class Hookshot
|
|
60
136
|
[nfailures.to_i, failed_id]
|
61
137
|
end
|
62
138
|
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
139
|
+
# Hookshot writes a lot of statistics to statsd/datadog, but to quickly check the
|
140
|
+
# current queue sizes, use `queue_stats`.
|
141
|
+
#
|
142
|
+
# Example:
|
143
|
+
#
|
144
|
+
# hookshot.queue_stats
|
145
|
+
# # => { pending: 42, delayed: 42, failures: 42 }
|
71
146
|
def queue_stats
|
72
147
|
pending, delayed, failures = redis.pipelined do
|
73
148
|
redis.llen NEW_JOBS_LIST
|
@@ -77,6 +152,88 @@ class Hookshot
|
|
77
152
|
{ pending: pending, delayed: delayed, failures: failures }
|
78
153
|
end
|
79
154
|
|
155
|
+
# blacklist! adds a domain to the list of currently blacklisted domains. Any
|
156
|
+
# jobs submitted with a domain in this list will be automatically dropped by
|
157
|
+
# hookshot.
|
158
|
+
#
|
159
|
+
# The format of the domain should be the full domain. The port should not be
|
160
|
+
# included if it is `80` or `443`. For example:
|
161
|
+
#
|
162
|
+
# | URL | Domain |
|
163
|
+
# |---------------------------|---------------------|
|
164
|
+
# | http://example.com/post | example.com |
|
165
|
+
# | https://example.com/post | example.com |
|
166
|
+
# | http://example.com:8000 | example.com:8000 |
|
167
|
+
# | http://example.com:80 | example.com |
|
168
|
+
#
|
169
|
+
# Example:
|
170
|
+
#
|
171
|
+
# hookshot.blacklist!("example.com")
|
172
|
+
def blacklist!(domain)
|
173
|
+
redis.sadd(BLACKLIST, domain)
|
174
|
+
end
|
175
|
+
|
176
|
+
# whitelist! adds a domain to the list of currently whitelisted domains.
|
177
|
+
# Normally, jobs are throttled to a maximum requests per second per domain.
|
178
|
+
# Whitelisted domains are granted a much higher initial rate.
|
179
|
+
#
|
180
|
+
# The format of the domain should be the full domain. The port should not be
|
181
|
+
# included if it is `80` or `443`. For example:
|
182
|
+
#
|
183
|
+
# | URL | Domain |
|
184
|
+
# |---------------------------|---------------------|
|
185
|
+
# | http://example.com/post | example.com |
|
186
|
+
# | https://example.com/post | example.com |
|
187
|
+
# | http://example.com:8000 | example.com:8000 |
|
188
|
+
# | http://example.com:80 | example.com |
|
189
|
+
#
|
190
|
+
# Example:
|
191
|
+
#
|
192
|
+
# hookshot.whitelist!("google.com")
|
193
|
+
def whitelist!(domain)
|
194
|
+
redis.sadd(WHITELIST, domain)
|
195
|
+
end
|
196
|
+
|
197
|
+
# blacklist returns an array of currently-blacklisted domains.
|
198
|
+
#
|
199
|
+
# Example:
|
200
|
+
#
|
201
|
+
# hookshot.blacklist
|
202
|
+
# #=> ["example.com"]
|
203
|
+
def blacklist
|
204
|
+
redis.smembers(BLACKLIST)
|
205
|
+
end
|
206
|
+
|
207
|
+
# whitelist returns an array of currently-whitelisted domains.
|
208
|
+
#
|
209
|
+
# Example:
|
210
|
+
#
|
211
|
+
# hookshot.whitelist
|
212
|
+
# #=> ["example.com"]
|
213
|
+
def whitelist
|
214
|
+
redis.smembers(WHITELIST)
|
215
|
+
end
|
216
|
+
|
217
|
+
# remove_blacklist removes a currently-blacklisted domain from the blacklist.
|
218
|
+
# If the domain was not blacklisted, this method has no effect.
|
219
|
+
#
|
220
|
+
# Example:
|
221
|
+
#
|
222
|
+
# hookshot.remove_blacklist("google.com")
|
223
|
+
def remove_blacklist(domain)
|
224
|
+
redis.srem(BLACKLIST, domain)
|
225
|
+
end
|
226
|
+
|
227
|
+
# remove_whitelist removes a currently-whitelisted domain from the whitelist.
|
228
|
+
# If the domain was not whitelisted, this method has no effect.
|
229
|
+
#
|
230
|
+
# Example:
|
231
|
+
#
|
232
|
+
# hookshot.remove_whitelist("google.com")
|
233
|
+
def remove_whitelist(domain)
|
234
|
+
redis.srem(WHITELIST, domain)
|
235
|
+
end
|
236
|
+
|
80
237
|
private
|
81
238
|
|
82
239
|
def serialize_headers(headers)
|
data/lib/hookshot/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: hookshot
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.3.
|
4
|
+
version: 0.3.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Burke Libbey
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-09-
|
11
|
+
date: 2014-09-12 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: Hookshot client library for ruby
|
14
14
|
email:
|