{"id":15048112,"url":"https://github.com/github/freno-client","last_synced_at":"2025-04-07T10:28:46.141Z","repository":{"id":46943463,"uuid":"93081684","full_name":"github/freno-client","owner":"github","description":"Ruby client and throttling library for freno, the throttler service","archived":false,"fork":false,"pushed_at":"2025-02-12T20:09:11.000Z","size":221,"stargazers_count":21,"open_issues_count":1,"forks_count":12,"subscribers_count":309,"default_branch":"main","last_synced_at":"2025-03-31T09:06:03.819Z","etag":null,"topics":["freno","high-availability","mysql","ruby","ruby-gem","throttling"],"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/github.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE.txt","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null}},"created_at":"2017-06-01T17:18:07.000Z","updated_at":"2025-02-03T12:51:26.000Z","dependencies_parsed_at":"2024-04-18T14:59:47.065Z","dependency_job_id":null,"html_url":"https://github.com/github/freno-client","commit_stats":{"total_commits":154,"total_committers":5,"mean_commits":30.8,"dds":0.4545454545454546,"last_synced_commit":"4592fe6eee4f179714072d29eabe86a7cd2483da"},"previous_names":[],"tags_count":10,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/github%2Ffreno-client","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/github%2Ffreno-client/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/github%2Ffreno-client/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/github%2Ffreno-client/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/github","download_url":"https://codeload.github.com/github/freno-client/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247635040,"owners_count":20970653,"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":["freno","high-availability","mysql","ruby","ruby-gem","throttling"],"created_at":"2024-09-24T21:08:15.810Z","updated_at":"2025-04-07T10:28:46.105Z","avatar_url":"https://github.com/github.png","language":"Ruby","readme":"A Ruby client and throttling library for [Freno](https://github.com/github/freno): the cooperative, highly available throttler service.\n\n## Current status\n\n`Freno::Client`, as [Freno](https://github.com/github/freno) itself, is in active development and its API can still change.\n\n## Installation\n\nAdd this line to your application's Gemfile:\n\n```ruby\ngem \"freno-client\"\n```\n\nAnd then execute:\n\n    $ bundle\n\nOr install it yourself as:\n\n    $ gem install freno-client\n\n## Usage\n\n`Freno::Client` uses [faraday](https://github.com/lostisland/faraday) to abstract the http client of your choice:\n\nTo start using the client, give it a faraday instance pointing to Freno's base URL.\n\n```ruby\nrequire \"freno/client\"\n\nFRENO_URL = \"http://freno.domain.com:8111\"\nfaraday   = Faraday.new(FRENO_URL)\nfreno     = Freno::Client.new(faraday)\n\nfreno.check?(app: :my_app, store_name: :my_cluster)\n# =\u003e true\n\nfreno.replication_delay(app: :my_app, store_name: :my_cluster)\n# =\u003e 0.125\n```\n\n### Providing sensible defaults\n\nIf most of the times you are going to ask Freno about the same app and/or storage name, you can tell the client to use some defaults, and override them as necessary.\n\n```ruby\nfreno = Freno::Client.new(faraday) do |client|\n  client.default_store_name = :my_cluster\n  client.default_app        = :my_app\nend\n\nfreno.check?\n# =\u003e true (Freno thinks that `my_app` can write to `main` storage)\n\nfreno.check?(app: :another_app, store_name: :another_storage)\n# =\u003e false (Freno thinks that `another_app` should not write to `another_storage`)\n```\n\n### What can I do with the client?\n\n#### Asking whether an app can write to a certain storage. ([`check` requests](https://github.com/github/freno/blob/master/doc/http.md#check-requests))\n\nIf we want to get a deep sense on why freno allowed or not, writing to a certain storage.\n\n```ruby\nresult = freno.check(app: :my_app, store_name: :my_cluster)\n# =\u003e #\u003cFreno::Client::Requests::Result ...\u003e\n\nresult.ok?\n# =\u003e false\n\nresult.failed?\n# =\u003e true\n\nresult.code\n# =\u003e 429\n\nresult.meaning\n# =\u003e :too_many_requests\n```\n\nOr if we only want to know if we can write:\n\n```ruby\nresult = freno.check?(app: :my_app, store_name: :my_cluster)\n# =\u003e true or false (a shortcut for `check.ok?`)\n```\n\n#### Asking whether replication delay is below a certain threshold. ([`check-read` requests](https://github.com/github/freno/blob/master/doc/http.md#specialized-requests))\n\n```ruby\nresult = freno.check_read(threshold: 0.5, app: :my_app, store_name: :my_cluster)\n# =\u003e #\u003cFreno::Client::Requests::Result ...\u003e\n\nresult.ok?\n# =\u003e true\n\nresult.failed?\n# =\u003e false\n\nresult.code\n# =\u003e 200\n\nresult.meaning\n# =\u003e :ok\n```\n\nOr if we only want to know if we can read:\n\n```ruby\nfreno.check?(threshold: 0.5, app: :my_app, store_name: :my_cluster)\n# =\u003e true or false (a shortcut for `check_read.ok?`)\n```\n\n#### Asking what's the replication delay\n\nFreno's response to [`GET /check`](https://github.com/github/freno/blob/master/doc/http.md#get-method) includes the replication delay value in seconds. The `replication_delay` method in the client returns this information.\n\n```ruby\nfreno.replication_delay(app: :my_app, store_name: :my_cluster)\n# =\u003e 0.125\n```\n\n#### Cross-cutting concerns with decorators\n\nDecorators can be used augment the client with custom features.\n\nA decorator is anything that has a `:request` accessor and can forward the execution of `perform` to it.\n\nThe following is an example of a decorator implementing a read-trough cache.\n\n```ruby\nclass Cache\n  attr_accessor :request\n\n  def initialize(cache, ttl)\n    @cache = cache\n    @ttl = ttl\n  end\n\n  def perform(**kwargs)\n    @cache.fetch(\"freno:client:v1:#{args.hash}\", ttl: @ttl) do\n      request.perform(kwargs)\n    end\n  end\nend\n```\n\nYou can use it to decorate a single kind of request to freno:\n\n```ruby\nfreno = Freno::Client.new(faraday) do |client|\n  client.decorate :replication_delay, with: Cache.new(App.cache, App.config.ttl)\nend\n```\n\nOr every kind of request:\n\n```ruby\nfreno = Freno::Client.new(faraday) do |client|\n  client.decorate :all, with: Cache.new(App.cache, App.config.ttl)\nend\n```\n\nAdditionally, decorators can be composed in multiple ways. The following client\napplies logging and instrumentation to all the requests, and it also applies caching, **before** the previous concerns, to `replication_delay` requests.\n\n```ruby\nfreno = Freno::Client.new(faraday) do |client|\n  client.decorate :replication_delay, with: caching\n  client.decorate :all, with: [logging, instrumentation]  \nend\n```\n\n### Throttler objects\n\nApart from the operations above, freno-client comes with `Freno::Throttler`, a Ruby library for throttling. You can use it in the following way:\n\n```ruby\nrequire \"freno/throttler\"\n\nclient    = Freno::Client.new(faraday)\nthrottler = Freno::Throttler.new(client: client, app: :my_app)\ncontext   = :my_cluster\n\nbid_data_set.each_slice(SLICE_SIZE) do |slice|\n  throttler.throttle(context) do\n    update(slice)\n  end\nend\n```\n\nIn the above example, `Freno::Throttler#throttle(context, \u0026block)` will check freno to determine whether is OK to proceed with the given block. If so, the block will be executed immediately, otherwise the throttler will sleep and try\nagain.\n\n#### Throttler configuration\n\n```ruby\nmodule Freno\n  class Throttler\n\n    DEFAULT_WAIT_SECONDS = 0.5\n    DEFAULT_MAX_WAIT_SECONDS = 10\n\n    def initialize(client: nil,\n                    app: nil,\n                    mapper: Mapper::Identity,\n                    instrumenter: Instrumenter::Noop,\n                    circuit_breaker: CircuitBreaker::Noop,\n                    wait_seconds: DEFAULT_WAIT_SECONDS,\n                    max_wait_seconds: DEFAULT_MAX_WAIT_SECONDS)\n\n\n      @client           = client\n      @app              = app\n      @mapper           = mapper\n      @instrumenter     = instrumenter\n      @circuit_breaker  = circuit_breaker\n      @wait_seconds     = wait_seconds\n      @max_wait_seconds = max_wait_seconds\n\n      yield self if block_given?\n\n      validate_args\n    end\n\n    ...\n  end\nend\n```\n\nA Throttler instance will make calls to freno on behalf of the given `app`,\nusing the given `client` (an instance of `Freno::Client`).\n\nYou optionally provide the time you want the throttler to sleep in case the check to freno fails, this is `wait_seconds`.\n\nIf replication lags badly, you can control until when you want to keep sleeping\nand retrying the check by setting `max_wait_seconds`. When that times out, the throttle will raise a `Freno::Throttler::WaitedTooLong` error.\n\n#### Instrumenting the throttler\n\nYou can also configure the throttler with an `instrumenter` collaborator to subscribe to events happening during the `throttle` call.\n\nAn instrumenter is an object that responds to `instrument(event_name, payload = {})` to receive events from the throttler. One could use `ActiveSupport::Notifications` as an instrumenter and subscribe to \"freno.*\" events somewhere else in the application, or implement one like the following to push some metrics to a stats system.\n\n```ruby\n  class StatsInstrumenter\n\n    attr_reader :stats\n\n    def initialize(stats:)\n      @stats = stats\n    end\n\n    def instrument(event_name, payload)\n      method = event_name.sub(\"throttler.\", \"\")\n      send(method, payload) if respond_to?(method)\n    end\n\n    def called(payload)\n      increment(\"throttler.called\", tags: extract_tags(payload))\n    end\n\n    def waited(payload)\n      stats.histogram(\"throttler.waited\", payload[:waited], tags: extract_tags(payload))\n    end\n\n    ...\n\n    def circuit_open(payload)\n      stats.increment(\"throttler.circuit_open\", tags: extract_tags(payload))\n    end\n\n    private\n\n    def extract_tags(payload)\n      cluster_names = payload[:store_names] || []\n      cluster_tags = cluster_names.map{ |cluster_name| \"cluster:#{cluster_name}\" }\n    end\n  end\n```\n\n#### Adding resiliency\n\nThe throttler can also receive a `circuit_breaker` object to implement resiliency.\n\nWith that information it receives, the circuit breaker determines whether or not to allow the next request. A circuit is said to be open when the next request is not allowed; and it's said to be closed when the next request is allowed\n\nIf the throttler waited too long, or an unexpected error happened; the circuit breaker will receive a `failure`. If in contrast it succeeded, the circuit breaker will receive a `success` message.\n\nOnce the circuit is open, the throttler will not try to throttle calls, an instead throw a `Freno::Throttler::CircuitOpen`\n\nThe following is a simple per-process circuit breaker implementation:\n\n```ruby\nclass MemoryCircuitBreaker\n\n  DEFAULT_CIRCUIT_RETRY_INTERVAL = 10\n\n  def initialize(circuit_retry_interval: DEFAULT_CIRCUIT_RETRY_INTERVAL)\n    @circuit_closed = true\n    @last_failure = nil\n    @circuit_retry_interval = circuit_retry_interval\n  end\n\n  def allow_request?\n    @circuit_closed || (Time.now - @last_failure) \u003e @circuit_retry_interval\n  end\n\n  def success\n    @circuit_closed = true\n  end\n\n  def failure\n    @last_failure = Time.now\n    @circuit_closed = false\n  end\nend\n```\n\n#### Flexible throttling strategies\n\nThe throttler uses a `mapper` to determine, based on the context provided to `#throttle`, the clusters which replication delay needs to be checked.\n\nBy default the throttler uses `Mapper::Identity`, which expect the context to be the store name(s) to check:\n\n```ruby\n# will check my_cluster's health\nthrottler.throttle(:my_cluster) { ... }\n# will check the health of cluster_a and cluster_b and throttle if any of them is not OK.\nthrottler.throttle([:cluster_a, :cluster_b]) { ... }\n```\n\nYou can create your own mapper, which is just an callable object (like a Proc, or any other object that responds to `call(context)`). The following is a mapper that knows how to throttle access to certain tables and shards.\n\n\n```ruby\nclass ShardMapper\n  def call(context = {})\n    context.map do |table, shards|\n      DatabaseStructure.cluster_for(table, shards)\n    end\n  end\nend\n\nthrottler = Freno::Throttler.new(client: freno, app: :my_app, mapper: ShardMapper.new)\n\nthrottler.throttle(:users =\u003e [1,2,3], :repositories =\u003e 5) do\n  perform_writes\nend\n```\n\n## Development\n\nAfter checking out the repo, run `bin/setup` to install dependencies. Then, run `bin/test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.\n\n## Contributing\n\nThis repository is open to [contributions](CONTRIBUTING.md). Contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.\n\n## Releasing\n\nIf you are the current maintainer of this gem:\n\n1. Create a branch for the release: `git checkout -b cut-release-vx.y.z`\n1. Make sure your local dependencies are up to date: `bin/setup`\n1. Ensure that tests are green: `bin/test`\n1. Bump gem version in `lib/freno/client/version.rb`\n1. Merge a PR to github/freno-client containing the changes in the version file\n1. Run `bin/release`\n\n## License\n\nThe gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).\n","funding_links":[],"categories":["Ruby"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgithub%2Ffreno-client","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fgithub%2Ffreno-client","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgithub%2Ffreno-client/lists"}