hookshot 0.3.0 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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:
|