{"id":13878399,"url":"https://github.com/justinhoward/cutoff","last_synced_at":"2025-04-05T23:11:34.683Z","repository":{"id":46631461,"uuid":"384024019","full_name":"justinhoward/cutoff","owner":"justinhoward","description":"Deadlines for Ruby","archived":false,"fork":false,"pushed_at":"2023-04-05T03:55:07.000Z","size":74,"stargazers_count":156,"open_issues_count":0,"forks_count":2,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-03-29T22:09:11.887Z","etag":null,"topics":["deadline","ruby","timeout"],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/justinhoward.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2021-07-08T06:28:50.000Z","updated_at":"2025-02-23T01:04:58.000Z","dependencies_parsed_at":"2024-11-16T23:02:22.443Z","dependency_job_id":"c3b95d02-5441-48f5-a135-d2540cfa0d3f","html_url":"https://github.com/justinhoward/cutoff","commit_stats":{"total_commits":28,"total_committers":3,"mean_commits":9.333333333333334,"dds":0.0714285714285714,"last_synced_commit":"11bb0bc6f2fa097f24ff1328ead595b730512a9d"},"previous_names":[],"tags_count":9,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/justinhoward%2Fcutoff","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/justinhoward%2Fcutoff/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/justinhoward%2Fcutoff/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/justinhoward%2Fcutoff/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/justinhoward","download_url":"https://codeload.github.com/justinhoward/cutoff/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247411239,"owners_count":20934653,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["deadline","ruby","timeout"],"created_at":"2024-08-06T08:01:48.453Z","updated_at":"2025-04-05T23:11:34.662Z","avatar_url":"https://github.com/justinhoward.png","language":"Ruby","funding_links":[],"categories":["Ruby"],"sub_categories":[],"readme":"Cutoff\n==================\n\n[![Gem Version](https://badge.fury.io/rb/cutoff.svg)](https://badge.fury.io/rb/cutoff)\n[![CI](https://github.com/justinhoward/cutoff/workflows/CI/badge.svg)](https://github.com/justinhoward/cutoff/actions?query=workflow%3ACI+branch%3Amaster)\n[![Code Quality](https://app.codacy.com/project/badge/Grade/2748da79ec294f909996a56f11caac4a)](https://www.codacy.com/gh/justinhoward/cutoff/dashboard?utm_source=github.com\u0026amp;utm_medium=referral\u0026amp;utm_content=justinhoward/cutoff\u0026amp;utm_campaign=Badge_Grade)\n[![Code Coverage](https://codecov.io/gh/justinhoward/cutoff/branch/master/graph/badge.svg?token=COVM3D2PTG)](https://codecov.io/gh/justinhoward/cutoff)\n[![Inline docs](http://inch-ci.org/github/justinhoward/cutoff.svg?branch=master)](http://inch-ci.org/github/justinhoward/cutoff)\n\nA deadlines library for Ruby inspired by Shopify and\n[Kir Shatrov's blog series][kir shatrov].\n\n\n```ruby\nCutoff.wrap(5) do\n  sleep(4)\n  Cutoff.checkpoint! # still have time left\n  sleep(2)\n  Cutoff.checkpoint! # raises an error\nend\n```\n\nIt has built-in patches for Mysql2 and Net::HTTP to auto-insert checkpoints and\ntimeouts.\n\n```ruby\nrequire 'cutoff/patch/mysql2'\n\nclient = Mysql2::Client.new\nCutoff.wrap(5) do\n  client.query('SELECT * FROM dual WHERE sleep(2)')\n\n  # Cutoff will automatically insert a /*+ MAX_EXECUTION_TIME(3000) */\n  # hint so that MySQL will terminate the query after the time remaining\n  #\n  # Or if time already expired, this will raise an error and not be executed\n  client.query('SELECT * FROM dual WHERE sleep(1)')\nend\n```\n\nWhy use deadlines?\n------------------------\n\nIf you've already implemented timeouts for your networked dependencies, then you\ncan be sure that no single HTTP request or database query can take longer than\nthe time allotted to it.\n\nFor example, let's say you set a query timeout of 3 seconds. That means no\nsingle query will take longer than 3 seconds. However, imagine a bad controller\naction or background job executes 100 slow queries. In that case, the queries\nadd up to 300 seconds, much too long.\n\nDeadlines keep track of the total elapsed time in a request or job and interrupt\nit if it takes too long.\n\nInstallation\n---------------\n\nAdd it to your `Gemfile`:\n\n```ruby\ngem 'cutoff'\n```\n\nOr install it manually:\n\n```sh\ngem install cutoff\n```\n\nAPI Documentation\n------------------\n\nAPI docs can be read [on rubydoc.info][api docs], inline in the source code, or\nyou can generate them yourself with Ruby `yard`:\n\n```sh\nbin/yardoc\n```\n\nThen open `doc/index.html` in your browser.\n\nUsage\n-----------\n\nThe simplest way to use Cutoff is to use its class methods, although it can be\nused in an object-oriented manner as well.\n\n### Wrapping a block\n\n```ruby\nCutoff.wrap(3.5) do # number of allowed seconds for this block\n  # Do something time-consuming here\n\n  # At a good stopping point, call checkpoint!\n  # If the allowed time is exceeded, this raises a Cutoff::CutoffExceededError\n  # otherwise, it does nothing\n  Cutoff.checkpoint!\n\n  # Now continue executing\nend\n```\n\n### Creating your own instance\n\n```ruby\ncutoff = Cutoff.new(6.4)\nsleep(10)\ncutoff.checkpoint! # Raises Cutoff::CutoffExceededError\n```\n\n### Getting cutoff details\n\nCutoff has some instance methods to get information about the time remaining,\netc.\n\n```ruby\n# If you're using Cutoff class methods, you can get the current instance\ncutoff = Cutoff.current # careful, this will be nil if a cutoff isn't running\n```\n\nOnce you have an instance, either by creating your own or from `.current`, you\nhave access to these methods.\n\n```ruby\ncutoff = Cutoff.current\n\n# These return Floats\ncutoff.allowed_seconds # Total seconds allowed (the seconds given when cutoff was started)\ncutoff.seconds_remaining # Seconds left\ncutoff.elapsed_seconds # Seconds since the cutoff was started\ncutoff.ms_remaining # Milliseconds left\n\ncutoff.exceeded? # True if the cutoff is expired\n```\n\nPatches\n-------------\n\nCutoff is in early stages, but it aims to provide patches for common networked\ndependencies. Patches automatically insert useful checkpoints and timeouts. The\npatches so far are for `mysql2` and `Net::HTTP`. They are not loaded by default,\nso you need to require them manually.\n\nFor example, to load the Mysql2 patch:\n\n```ruby\n# In your Gemfile\ngem 'cutoff', require: %w[cutoff cutoff/patch/mysql2]\n```\n\n```ruby\n# Or manually\nrequire 'cutoff'\nrequire 'cutoff/patch/mysql2'\n```\n\n### Mysql2\n\nOnce it is enabled, any `Mysql2::Client` object will respect the current\nclass-level cutoff if one is set.\n\n```ruby\nrequire 'cutoff/patch/mysql2'\n\nclient = Mysql2::Client.new\nCutoff.wrap(3) do\n  sleep(4)\n\n  # This query will not be executed because the time is already expired\n  client.query('SELECT * FROM users')\nend\n\nCutoff.wrap(3) do\n  sleep(1)\n\n  # There are 2 seconds left, so a MAX_EXECUTION_TIME query hint is added\n  # to inform MySQL we only have 2 seconds to execute this query\n  # The executed query will be \"SELECT /*+ MAX_EXECUTION_TIME(2000) */ * FROM users\"\n  client.query('SELECT * FROM users')\n\n  # MySQL only supports MAX_EXECUTION_TIME for SELECTs so no query hint here\n  client.query(\"INSERT INTO users(first_name) VALUES('Joe')\")\n\n  sleep(3)\n\n  # We don't even execute this query because time is already expired\n  # This limit applies to all queries, including INSERTS, etc\n  client.query('SELECT * FROM users')\nend\n```\n\n### Net::HTTP\n\nOnce it is enabled, any `Net::HTTP` requests will respect the current\nclass-level cutoff if one is set.\n\n```ruby\nrequire 'cutoff/patch/net_http'\n\nCutoff.wrap(3) do\n  sleep(5)\n\n  # The cutoff is expired, so this hits a checkpoint and will not be executed\n  Net::HTTP.get(URI.parse('http://example.com'))\nend\n\nCutoff.wrap(3) do\n  sleep(1.5)\n\n  # The cutoff has 1.5 seconds left, so this request will be executed\n  # open_timeout, read_timeout, and write_timeout (Ruby \u003e= 2.6) will each\n  # be set to 1.5\n  # This means the overall time can be \u003e 1.5 since the combined phases can take\n  # up to 4.5 seconds\n  Net::HTTP.get(URI.parse('http://example.com'))\nend\n```\n\nSelecting Checkpoints\n---------------------------\n\nIn some cases, you may want to select some checkpoints to use, but not others.\nFor example, you may want to run some code that contains MySQL queries, but not\nuse the mysql2 patch. The `exclude` and `only` options support this.\n\n```ruby\nCutoff.wrap(10, exclude: :mysql2) do\n  # The mysql2 patch won't be used here\nend\n\nCutoff.wrap(10, only: %i[foo bar]) do\n  # These checkpoints will be used\n  Cutoff.checkpoint!(:foo)\n  Cutoff.checkpoint!(:bar)\n\n  # These checkpoints will be skipped\n  Cutoff.checkpoint!(:asdf)\n  Cutoff.checkpoint!\nend\n```\n\nTiming a Rails Controller\n---------------------------\n\nOne use of a cutoff is to add a deadline to a Rails controller action. This is\ntypically preferable to approaches like `Rack::Timeout` that use the dangerous\n`Timeout` class.\n\nCutoff includes a built-in integration for this purpose. If Rails is installed,\nthe `#cutoff` class method is available in your controllers.\n\n```ruby\nclass ApplicationController \u003c ActionController::Base\n  # You may want to set a long global cutoff, but it's not required\n  cutoff 30\nend\n\nclass UsersController \u003c ApplicationController\n  cutoff 5.0\n\n  def index\n    # Now in your action, you can call `checkpoint!`, or if you're using the\n    # patches, checkpoints will be added automatically\n    Cutoff.checkpoint!\n  end\nend\n```\n\nJust like with controller filters, you can use filters with the cutoff method.\n\n```ruby\nclass UsersController \u003c ApplicationController\n  # For example, use an :only filter\n  cutoff 5.0, only: :index\n\n  # Multiple calls work just fine. Last match wins\n  cutoff 2.5, only: :show\n\n  def index\n    # ...\n  end\n\n  def show\n    # ...\n  end\nend\n```\n\nConsider adding a global error handler for the `Cutoff::CutoffExceededError` in\ncase you want to display a nice error page for timeouts.\n\n```ruby\nclass ApplicationController \u003c ActionController::Base\n  rescue_from Cutoff::CutoffExceededError, with: :handle_cutoff_exceeded\n\n  def handle_cutoff_exceeded\n    # Render a nice error page\n  end\nend\n```\n\nTiming Sidekiq Workers\n------------\n\nIf Sidekiq is loaded, Cutoff includes middleware to support a `:cutoff` option.\n\n```ruby\nclass MyWorker\n  include Sidekiq::Worker\n\n  sidekiq_options cutoff: 6.0\n\n  def perform\n    # ...\n    Cutoff.checkpoint!\n    # ...\n  end\nend\n```\n\n\nDisabling Cutoff for Testing and Development\n------------\n\nWhen testing or debugging an application that uses Cutoff, you may want to\ndisable Cutoff entirely. These methods are not thread-safe and not intended for\nproduction.\n\n```ruby\n# This disables all cutoff timers, for both global and local instances\nCutoff.disable!\nCutoff.disabled? # =\u003e true\n\n# Re-enable cutoff\nCutoff.enable!\n```\n\nMulti-threading\n-----------------\n\nIn multi-threaded environments, cutoff class methods are independent in each\nthread. That means that if you start a cutoff in one thread then start a new\nthread, the second thread _will not_ inherit the cutoff from its parent thread.\n\n```ruby\nCutoff.wrap(6) do\n  Thread.new do\n    # This code can run as long as it wants because the class-level\n    # cutoff is independent\n\n    Cutoff.wrap(3) do\n      # However, you can start a new cutoff inside the new thread and it\n      # will not affect any other threads\n    end\n  end\nend\n```\n\nThe same rules apply to fibers. Each fiber has independent class-level cutoff\ninstances. This means you can use Cutoff in a multi-threaded web server or job\nrunner without worrying about thread conflicts.\n\nIf you want to use a single cutoff for multi-threading, you'll need to pass an\ninstance of a Cutoff.\n\n```ruby\ncutoff = Cutoff.new(6)\ncutoff.checkpoint! # parent thread can call checkpoint!\nThread.new do\n  # And the child thread can use the same cutoff\n  cutoff.checkpoint!\nend\nend\n```\n\nHowever, because patches use the class-level Cutoff methods, this only works\nwhen calling cutoff methods manually.\n\nNested Cutoffs\n-----------------\n\nWhen using the Cutoff class methods, it is possible to nest multiple Cutoff\ncontexts with `.wrap` or `.start`.\n\n```ruby\nCutoff.wrap(10) do\n  # This outer block has a timeout of 10 seconds\n  Cutoff.wrap(3) do\n    # But this inner block is only allowed to take 3 seconds\n  end\nend\n```\n\nA child cutoff can never be set for longer than the remaining time of its parent\ncutoff. So if a child is created for longer than the remaining allowed time, it\nwill be reduced to the remaining time of the outer cutoff.\n\n```ruby\nCutoff.wrap(5) do\n  sleep(4)\n  # There is only 1 second remaining in the parent\n  Cutoff.wrap(3) do\n    # So this inner block will only have 1 second to execute\n  end\nend\n```\n\nAbout the Timer\n-------------------\n\nCutoff tries to use the best timer available on whatever platform it's running\non. If a monotonic clock is available, that will be used, or failing that, if\nconcurrent-ruby is loaded, that will be used. If neither is available,\n`Time.now` is used.\n\nThis mean that Cutoff tries its best to prevent time from travelling backwards.\nHowever, the clock uniformity, resolution, and stability is determined by the\nsystem Cutoff is running on.\n\nManual start and stop\n----------------------\n\nIf you find that `Cutoff.wrap` is too limiting for some integrations, Cutoff\nalso provides the `start` and `stop` methods. Extra care is required to use\nthese to prevent a cutoff from being leaked. Every `start` call must be\naccompanied by a `stop` call, otherwise the cutoff will continue to run and\ncould affect a context other than the intended one.\n\n```ruby\nCutoff.start(2.5)\nbegin\n  # Execute code here\n  Cutoff.checkpoint!\nensure\n  # Always stop in an ensure statement to make sure an exception cannot leave\n  # a cutoff running\n  Cutoff.stop\nend\n\n# Nested cutoffs are still supported\nouter = Cutoff.start(10)\nbegin\n  # Outer 10s cutoff is used here\n  Cutoff.checkpoint!\n\n  inner = Cutoff.start(5)\n  begin\n    # Inner 5s cutoff is used here\n    Cutoff.checkpoint!\n  ensure\n    # Stops the inner cutoff\n    # We don't need to pass the instance here, but it does prevent some types of mistakes\n    Cutoff.stop(inner)\n  end\nensure\n  # Stops the outer cutoff\n  Cutoff.stop(outer)\nend\n\nCutoff.start(10)\nCutoff.start(5)\nbegin\n  # Code here\nensure\n  # This stops all cutoffs\n  Cutoff.clear_all\nend\n```\n\nBe careful, you can easily make a mistake when using this API, so prefer `.wrap`\nwhen possible.\n\nDesign Philosophy\n-------------------\n\nCutoff is designed to only stop code execution at predictable points. It will\nnever interrupt a running program unless:\n\n- `checkpoint!` is called\n- a network timeout is exceeded\n\nPatches are designed to ease the burden on developers to manually call\n`checkpoint!` or configure network timeouts. The ruby `Timeout` class is not\nused. See Julia Evans' post on [Why Ruby's Timeout is dangerous][julia_evans].\n\nPatches are only applied by explicit opt-in, and Cutoff can always be used as a\nstandalone library.\n\n[julia_evans]: https://jvns.ca/blog/2015/11/27/why-rubys-timeout-is-dangerous-and-thread-dot-raise-is-terrifying/\n[kir shatrov]: https://kirshatrov.com/posts/scaling-mysql-stack-part-2-deadlines/\n[api docs]: https://www.rubydoc.info/github/justinhoward/cutoff/master\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjustinhoward%2Fcutoff","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjustinhoward%2Fcutoff","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjustinhoward%2Fcutoff/lists"}