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.
Files changed (5) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +101 -16
  3. data/lib/hookshot.rb +167 -10
  4. data/lib/hookshot/version.rb +1 -1
  5. metadata +2 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 50a6ddf79d002197181d01e453820959a59c24fd
4
- data.tar.gz: 2af7e0bb07e0d6810cb2e1e8722f44b97ea891c0
3
+ metadata.gz: 50a5202fd2cf12d2977095bd54a10af1284bf61b
4
+ data.tar.gz: 094bb40322e173db3a428db8bfdb61b40c7a7388
5
5
  SHA512:
6
- metadata.gz: 6af6ecbb0d55181f235d3b9a9c7f79124e3c29e575bb0856dee3d038b1d8c5375119014906f0d5640d89dfddde2bf6d6024ae4de2fcd0e9274d9ec89ef8a7edc
7
- data.tar.gz: bd380b8d21b96d091698e5fdc35e86193a8b334cd268fcee7e86e5f71d43b3ebc124683ea6b5a1549cdfeffb91854316e5667e7b75299fd89b14ad2104ad499b
6
+ metadata.gz: 6233b77583a4bb4ea005f2d6b7e43b5fa1503edca65e1af206ce3507a489de1e3866b8a3f3d817c902a79eaab9bee3510f9e8560f0554448e47a421c09fede65
7
+ data.tar.gz: 3d3b2b0aee201ec4bfedb319f19db5a409c0667bc6c548ba39cb6825e21ae2933d5f0f08643c41fb91a86d985e5c8343238ed576a86471a67a80c4547a62ffd6
data/README.md CHANGED
@@ -1,29 +1,114 @@
1
- # Hookshot::Client
1
+ # Hookshot
2
2
 
3
- TODO: Write a gem description
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
- Add this line to your application's Gemfile:
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
- gem 'hookshot'
12
+ ## Usage
10
13
 
11
- And then execute:
14
+ #### Initialize a connection
12
15
 
13
- $ bundle
16
+ ```ruby
17
+ require 'hookshot'
18
+ require 'redis'
19
+ hookshot = Hookshot.new(Redis.new(port: 6379, host: 'localhost'))
20
+ ```
14
21
 
15
- Or install it yourself as:
22
+ #### Enqueue a webhook
16
23
 
17
- $ gem install hookshot
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
- ## Usage
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
- TODO: Write usage instructions here
104
+ ```ruby
105
+ hookshot.blacklist!("example.com")
106
+ hookshot.whitelist!("google.com")
107
+ ```
22
108
 
23
- ## Contributing
109
+ #### Remove an item from the list
24
110
 
25
- 1. Fork it
26
- 2. Create your feature branch (`git checkout -b my-new-feature`)
27
- 3. Commit your changes (`git commit -am 'Add some feature'`)
28
- 4. Push to the branch (`git push origin my-new-feature`)
29
- 5. Create new Pull Request
111
+ ````ruby
112
+ hookshot.remove_blacklist("example.com")
113
+ hookshot.remove_whitelist("google.com")
114
+ ```
@@ -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
- MANUAL_BLACKLIST = "#{PREFIX}:throttle:blacklist"
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.blpop(FAILURES_LIST, 0)
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
- def force_blacklist(domain, duration)
64
- redis.zadd(MANUAL_BLACKLIST, Time.now.to_i+duration, domain)
65
- end
66
-
67
- def remove_override(domain)
68
- redis.zrem(MANUAL_BLACKLIST, domain)
69
- end
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)
@@ -1,3 +1,3 @@
1
1
  class Hookshot
2
- VERSION = "0.3.0"
2
+ VERSION = "0.3.2"
3
3
  end
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.0
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 00:00:00.000000000 Z
11
+ date: 2014-09-12 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Hookshot client library for ruby
14
14
  email: