hookshot 0.3.0 → 0.3.2

Sign up to get free protection for your applications and to get access to all the features.
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: