Class: Hookshot
- Inherits:
-
Object
- Object
- Hookshot
- Defined in:
- lib/hookshot.rb,
lib/hookshot/version.rb
Constant Summary collapse
- PREFIX =
"hookshot"- NEW_JOBS_LIST =
"#{PREFIX}:jobs"- DELAYED_SET =
"#{PREFIX}:delayed"- FAILURES_LIST =
"#{PREFIX}:failures"- BLACKLIST =
"#{PREFIX}:throttle:blacklist"- WHITELIST =
"#{PREFIX}:throttle:whitelist"- FINAL_FAILURE =
Special sentinel value returned from
get_next_failurewhen the job will no longer be retried. -1
- JOB_KEY_LIFETIME =
Jobs are retried for up to 48 hours. Though we delete the job info when hookshot is done with each job, it’s still a good idea to clean up any keys that have managed to stick around somehow.
86400 * 4
- FailureQueueEmpty =
Raised by
get_next_failureif there are no pending failures. Class.new(StandardError)
- VERSION =
"0.3.2"
Instance Attribute Summary collapse
-
#redis ⇒ Object
readonly
Returns the value of attribute redis.
Instance Method Summary collapse
-
#blacklist ⇒ Object
blacklist returns an array of currently-blacklisted domains.
-
#blacklist!(domain) ⇒ Object
blacklist! adds a domain to the list of currently blacklisted domains.
-
#enqueue(url:, headers:, context:, payload:, activate_at: nil) ⇒ Object
enqueue takes a URL, a hash of headers, a payload (request body), and a
contextvalue. -
#enqueue_in(duration, url:, headers:, context:, payload:) ⇒ Object
enqeueue_in calls enqueue with an
activate_atparameter to delay the job’s execution. -
#get_next_failure ⇒ Object
Jobs that fail many times in a row are returned back to the application.
-
#initialize(redis) ⇒ Hookshot
constructor
Hookshot expects to be initialized with an instance of
Redisprovided by theredisrubygem. -
#queue_stats ⇒ Object
Hookshot writes a lot of statistics to statsd/datadog, but to quickly check the current queue sizes, use
queue_stats. -
#remove_blacklist(domain) ⇒ Object
remove_blacklist removes a currently-blacklisted domain from the blacklist.
-
#remove_whitelist(domain) ⇒ Object
remove_whitelist removes a currently-whitelisted domain from the whitelist.
-
#whitelist ⇒ Object
whitelist returns an array of currently-whitelisted domains.
-
#whitelist!(domain) ⇒ Object
whitelist! adds a domain to the list of currently whitelisted domains.
Constructor Details
#initialize(redis) ⇒ Hookshot
Hookshot expects to be initialized with an instance of Redis provided by the redis rubygem.
Example:
require 'hookshot'
require 'redis'
hookshot = Hookshot.new(Redis.new(port: 6379, host: 'localhost'))
37 38 39 |
# File 'lib/hookshot.rb', line 37 def initialize(redis) @redis = redis end |
Instance Attribute Details
#redis ⇒ Object (readonly)
Returns the value of attribute redis.
27 28 29 |
# File 'lib/hookshot.rb', line 27 def redis @redis end |
Instance Method Details
#blacklist ⇒ Object
blacklist returns an array of currently-blacklisted domains.
Example:
hookshot.blacklist
#=> ["example.com"]
203 204 205 |
# File 'lib/hookshot.rb', line 203 def blacklist redis.smembers(BLACKLIST) end |
#blacklist!(domain) ⇒ Object
blacklist! adds a domain to the list of currently blacklisted domains. Any jobs submitted with a domain in this list will be automatically dropped by hookshot.
The format of the domain should be the full domain. The port should not be included if it is 80 or 443. For example:
| URL | Domain | |—————————|———————| | example.com/post | example.com | | example.com/post | example.com | | example.com:8000 | example.com:8000 | | example.com:80 | example.com |
Example:
hookshot.blacklist!("example.com")
172 173 174 |
# File 'lib/hookshot.rb', line 172 def blacklist!(domain) redis.sadd(BLACKLIST, domain) end |
#enqueue(url:, headers:, context:, payload:, activate_at: nil) ⇒ Object
enqueue takes a URL, a hash of headers, a payload (request body), and a context value. It submits these values to hookshot for processing.
The context value can be any string, and will be returned to you via get_next_failure if the job can’t be successfully completed after about 48 hours of retries.
In Shopify, we pass our WebhookSubscription object ID for the context value, so that we can notify merchants when their webhooks are failing, and delete subscriptions that fail consistently.
activate_at is an optional parameter that specifies a number of seconds to wait before making this job active in hookshot. You should normally call enqueue_in rather than pass activate_at explicitly.
Example:
hookshot.enqueue(
url: 'http://localhost:8080/post',
headers: {"X-My-Header" => "value"},
context: "42",
payload: "request body")
63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 |
# File 'lib/hookshot.rb', line 63 def enqueue(url:, headers:, context:, payload:, activate_at: nil) uuid = SecureRandom.uuid redis.pipelined do redis.hmset( job_key(uuid), "url", url, "headers", serialize_headers(headers), "context", context, "payload", payload, "failures", 0) redis.expire(job_key(uuid), JOB_KEY_LIFETIME) if activate_at redis.zadd(DELAYED_SET, activate_at, uuid) else redis.lpush(NEW_JOBS_LIST, uuid) end end uuid end |
#enqueue_in(duration, url:, headers:, context:, payload:) ⇒ Object
enqeueue_in calls enqueue with an activate_at parameter to delay the job’s execution.
Example:
hookshot.enqueue_in(60, # seconds
url: 'http://localhost:8080/post',
headers: {"X-My-Header" => "value"},
context: "42",
payload: "request body")
94 95 96 97 98 99 100 101 102 |
# File 'lib/hookshot.rb', line 94 def enqueue_in(duration, url:, headers:, context:, payload:) enqueue_time = (Time.now + duration).to_i enqueue( url: url, headers: headers, context: context, payload: payload, activate_at: enqueue_time) end |
#get_next_failure ⇒ Object
Jobs that fail many times in a row are returned back to the application. Specifically, the context value passed in via the enqueue* methods is returned.
get_next_failure returns two values: the number of failures so far for this job, and the context passed in with the job. If nfailures is equal to Hookshot::FINAL_FAILURE, the job will not be retried. However, if nfailures is any other value, the job will still be retried; this is just an advisory because the job has been failing for at least 24 hours.
This method is non-blocking: if there is no item present in the failures queue, it will raise FailureQueueEmpty.
Example:
loop {
nfailures, context = hookshot.get_next_failure
if nfailures == Hookshot::FINAL_FAILURE
delete_webhook_subscription(context)
end
}
125 126 127 128 129 130 131 132 133 134 135 136 137 |
# File 'lib/hookshot.rb', line 125 def get_next_failure # block indefinitely waiting for the next failure. line = redis.lpop(FAILURES_LIST) raise FailureQueueEmpty if line.nil? nfailures, failed_id = line.split('|', 2) if nfailures.to_i == 0 || failed_id.empty? raise "Invalid line from hookshot: #{line}" end [nfailures.to_i, failed_id] end |
#queue_stats ⇒ Object
Hookshot writes a lot of statistics to statsd/datadog, but to quickly check the current queue sizes, use queue_stats.
Example:
hookshot.queue_stats
# => { pending: 42, delayed: 42, failures: 42 }
146 147 148 149 150 151 152 153 |
# File 'lib/hookshot.rb', line 146 def queue_stats pending, delayed, failures = redis.pipelined do redis.llen NEW_JOBS_LIST redis.zcard DELAYED_SET redis.llen FAILURES_LIST end { pending: pending, delayed: delayed, failures: failures } end |
#remove_blacklist(domain) ⇒ Object
remove_blacklist removes a currently-blacklisted domain from the blacklist. If the domain was not blacklisted, this method has no effect.
Example:
hookshot.remove_blacklist("google.com")
223 224 225 |
# File 'lib/hookshot.rb', line 223 def remove_blacklist(domain) redis.srem(BLACKLIST, domain) end |
#remove_whitelist(domain) ⇒ Object
remove_whitelist removes a currently-whitelisted domain from the whitelist. If the domain was not whitelisted, this method has no effect.
Example:
hookshot.remove_whitelist("google.com")
233 234 235 |
# File 'lib/hookshot.rb', line 233 def remove_whitelist(domain) redis.srem(WHITELIST, domain) end |
#whitelist ⇒ Object
whitelist returns an array of currently-whitelisted domains.
Example:
hookshot.whitelist
#=> ["example.com"]
213 214 215 |
# File 'lib/hookshot.rb', line 213 def whitelist redis.smembers(WHITELIST) end |
#whitelist!(domain) ⇒ Object
whitelist! adds a domain to the list of currently whitelisted domains. Normally, jobs are throttled to a maximum requests per second per domain. Whitelisted domains are granted a much higher initial rate.
The format of the domain should be the full domain. The port should not be included if it is 80 or 443. For example:
| URL | Domain | |—————————|———————| | example.com/post | example.com | | example.com/post | example.com | | example.com:8000 | example.com:8000 | | example.com:80 | example.com |
Example:
hookshot.whitelist!("google.com")
193 194 195 |
# File 'lib/hookshot.rb', line 193 def whitelist!(domain) redis.sadd(WHITELIST, domain) end |