') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); GitHub - hmac/what: A simple job queue built on PostgreSQL 9.5+ · GitHub
Skip to content

Repository files navigation

What

Que, but for Postgres 9.5+.

CI

Usage

1. Add What to your Gemfile

# This will be easier once What is released/is on rubygemsgem"what",git: "https://github.com/hmac/what"

2. Create the what_jobs table

If you're using ActiveRecord, you can do this by running the migration included with what:

ActiveRecord::Migration.run(What::Migrations::V1)

If you're using Rails, it's recommended you create a new databsase migration which subclasses What::Migrations::V1. This will then be versioned and applied like any other migration.

# db/migrate/*_create_what_jobs.rbrequire"what/migrations/v1"classCreateWhatJobs < What::Migrations::V1end

If you're using Sequel, you'll have to create the table yourself. The structure is as follows:

CREATETABLEwhat_jobs (
id serialNOT NULL,
job_class textNOT NULL,
args json NOT NULL,
queue textNOT NULL,
run_at timestampNOT NULL,
failed_at timestamp,
last_error text,
error_count integer,
runnable booleanNOT NULL,
)

This corresponds to the following in Sequel's migration DSL:

Sequel.migrationdoupdocreate_table(:what_jobs)doprimary_key:id,type: :Bignum,null: falseString:job_class,null: falsejsonb:args,null: falseString:queue,null: falseTime:run_at,null: falseTime:failed_atString:last_errorInteger:error_countTrueClass:runnable,null: falseendenddowndodrop_table(:what_jobs)endend

2. Create an entrypoint file for your project.

This is the file that What workers will load before running your jobs - it should require all the relevant classes and libraries necessary for your jobs to run. For an example, see spec/spec_helper.rb. Loading this file should set up the database connection and configure What to use it.

If you're using Rails, you can configure What in an initializer and use config/environment.rb as your entrypoint.

# config/initializers/what.rbrequire"what"require"what/connection/active_record"What.configuredo |config|
config.connection=What::Connection::ActiveRecord.new(ActiveRecord::Base.connection_pool.checkout)config.logger=Rails.loggerend

If you're using Sequel, pass a reference to the Sequel database object.

DB=Sequel.connect(...)What.configuredo |config|
config.connection=What::Connection::Sequel.new(DB)end

3. Write your jobs as subclasses of What::Job

Your jobs should subclass What::Job and define a run method which will be called by the worker. You should also specify a failure strategy (see below for more information on failure strategies), although if you don't then NoRetry will be used by default. You can also specify a queue, which defaults to "default".

classResetUserPassword < What::JobextendWhat::Failure::NoRetryself.queue="emails"defrun(id)user=User.find(id)ResetPasswordMailer.new(user).deliver!endend

4. Spin up What workers

What workers run in separate processes, and can be launched via the what executable. They take as arguments a comma-separated list of queues to work and an entrypoint file.

bundle exec what default ./entrypoint.rb

The what executable is very small and can be replaced with a custom script if you prefer. The role of the executable is the following:

  • require the entrypoint file to initialise the application
  • set up interrupt handlers to cleanly shut down the worker when asked
  • run the worker in a loop, sleeping for a small period of time between each run

See the code for more information.

Working Jobs

What works jobs in the following way:

  1. Scan the what_jobs table for a qualifying job (runnable, in the right queue etc.)
  2. If that job is locked by another process, skip it and go to the next one.
  3. When a qualifying, unlocked job is found, take a FOR UPDATE lock on it.
  4. Instantiate the job_class and call its run method with the stored arguments.
  5. If run raises no exceptions, destroy the job.
  6. If run raises an exception, call the handle_failure method of the class.

Steps 1-3 happen atomically via PostgreSQL's FOR UPDATE SKIP LOCKED clause (see here for more info).

If the job fails, it is typically left in the queue. It might be rescheduled to run again or left to be handled manually. This behaviour is governed by the failure strategy (see below).

Queues

What supports multiple queues, which are effectively labels that are applied to jobs. A job class can specify its queue using the class-level attribute writer inherited from What::Job.

classMyJobself.queue="my_custom_queue"
...
end

What workers are given a single specific queue to work on, which is specified at startup, so you'll probably want at least one worker process for each queue you use.

Failure Strategies

What provides several ways for dealing with job failure. These are defined as failure strategies, and can be configured on a per-job basis. To use a particular strategy, set it in your job class by extending the strategy (they're modules).

classMyJobextendWhat::Failure::NoRetry
...
end

The details of the different failure strategies are outlined below.

NoRetry

NoRetry is for jobs which shouldn't automatically retried. On failure, the job will be left in the queue. The following attributes will be set:

  • runnable: false - this means the job won't get picked up by any worker
  • failed_at: [timestamp] - the time that the job failed
  • last_error: [string] - the exception and backtrace that caused the failure

To handle a failed job under NoRetry, you'll need to either manually reschedule it (by updating runnable to true) or destroy it.

VariableRetry

VariableRetry is for jobs which can be retried under certain conditions. It is configured with two class-level attributes: retryable_exceptions and retry_intervals.

classMyJobextendWhat::Failure::VariableRetryself.retryable_exceptions=[AnError,AnotherError]self.retry_intervals=[5,30,60]
...
end

retryable_exceptions defines a list of exceptions for which this job can be retried. If the job fails due to one of these exceptions, it will be rescheduled to run at a certain point in the future, defined by retry_intervals.

retry_intervals defines a list of intervals, in seconds, at which this job will be rescheduled if it fails (with a retryable exception). The first interval will be used after the first failure, the second after a second failure, and so on. If the intervals are exhausted, the job falls back to NoRetry behaviour and will not be rescheduled.

As an example, take the job above. If it fails with AnError, it will be rescheduled to run in 5 seconds time. If it fails again with AnError, it will be rescheduled to run in 30 seconds time. If it fails again with AnotherError, it will be rescheduled to run in 60 seconds time. If it fails again, it will not be rescheduled.

What jobs use the error_count column to keep track of the number of failures they have had. The last_error column will only show the most recent error, and is intended for diagnostic purposes.

References

What is heavily inspired by Que, but aims for simplicity and small code size over feature richness.

About

A simple job queue built on PostgreSQL 9.5+

Resources

Stars

5 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages