rx-healthcheck 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +8 -0
- data/Gemfile +10 -0
- data/Gemfile.lock +29 -0
- data/LICENSE.txt +21 -0
- data/README.md +73 -0
- data/Rakefile +12 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/lib/rx.rb +19 -0
- data/lib/rx/cache/in_memory_cache.rb +51 -0
- data/lib/rx/cache/no_op_cache.rb +17 -0
- data/lib/rx/check/active_record_check.rb +27 -0
- data/lib/rx/check/file_system_check.rb +22 -0
- data/lib/rx/check/generic_check.rb +22 -0
- data/lib/rx/check/http_check.rb +37 -0
- data/lib/rx/check/result.rb +34 -0
- data/lib/rx/concurrent/future.rb +71 -0
- data/lib/rx/concurrent/thread_pool.rb +53 -0
- data/lib/rx/middleware.rb +95 -0
- data/lib/rx/util/heap.rb +104 -0
- data/lib/rx/version.rb +5 -0
- data/rx.gemspec +32 -0
- metadata +81 -0
    
        checksums.yaml
    ADDED
    
    | @@ -0,0 +1,7 @@ | |
| 1 | 
            +
            ---
         | 
| 2 | 
            +
            SHA256:
         | 
| 3 | 
            +
              metadata.gz: 51191dafecd2e8ad24a88a3c60a44f49618f7983ded633bf58df1f482583c6a7
         | 
| 4 | 
            +
              data.tar.gz: c283c8507f67d1e1d6012e8a94b33fb4ba17fd0724bde9e523388b28efa5b0ba
         | 
| 5 | 
            +
            SHA512:
         | 
| 6 | 
            +
              metadata.gz: 7f79992064d97c64192b55378a2d5477fe11cfbfbf3aad28b92236bcaa5bcd7bb247b0df885d00c6eec17146f23620822312c39068e4dbb391d4d287a36da30f
         | 
| 7 | 
            +
              data.tar.gz: 9e4cc0de75b8b3bf88e9c4588706ecd1315051f558779e53150c615e581f8b04363731b3527134a8a35044564fd08226d14c94e3c7ba3e17491dfd7acd604291
         | 
    
        data/.gitignore
    ADDED
    
    
    
        data/Gemfile
    ADDED
    
    
    
        data/Gemfile.lock
    ADDED
    
    | @@ -0,0 +1,29 @@ | |
| 1 | 
            +
            PATH
         | 
| 2 | 
            +
              remote: .
         | 
| 3 | 
            +
              specs:
         | 
| 4 | 
            +
                rx (0.1.0)
         | 
| 5 | 
            +
                  simplecov (= 0.21.2)
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            GEM
         | 
| 8 | 
            +
              remote: https://rubygems.org/
         | 
| 9 | 
            +
              specs:
         | 
| 10 | 
            +
                docile (1.4.0)
         | 
| 11 | 
            +
                minitest (5.14.4)
         | 
| 12 | 
            +
                rake (13.0.3)
         | 
| 13 | 
            +
                simplecov (0.21.2)
         | 
| 14 | 
            +
                  docile (~> 1.1)
         | 
| 15 | 
            +
                  simplecov-html (~> 0.11)
         | 
| 16 | 
            +
                  simplecov_json_formatter (~> 0.1)
         | 
| 17 | 
            +
                simplecov-html (0.12.3)
         | 
| 18 | 
            +
                simplecov_json_formatter (0.1.3)
         | 
| 19 | 
            +
             | 
| 20 | 
            +
            PLATFORMS
         | 
| 21 | 
            +
              x86_64-linux
         | 
| 22 | 
            +
             | 
| 23 | 
            +
            DEPENDENCIES
         | 
| 24 | 
            +
              minitest (~> 5.0)
         | 
| 25 | 
            +
              rake (~> 13.0)
         | 
| 26 | 
            +
              rx!
         | 
| 27 | 
            +
             | 
| 28 | 
            +
            BUNDLED WITH
         | 
| 29 | 
            +
               2.2.11
         | 
    
        data/LICENSE.txt
    ADDED
    
    | @@ -0,0 +1,21 @@ | |
| 1 | 
            +
            The MIT License (MIT)
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            Copyright (c) 2021 Zach Pendleton
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            Permission is hereby granted, free of charge, to any person obtaining a copy
         | 
| 6 | 
            +
            of this software and associated documentation files (the "Software"), to deal
         | 
| 7 | 
            +
            in the Software without restriction, including without limitation the rights
         | 
| 8 | 
            +
            to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
         | 
| 9 | 
            +
            copies of the Software, and to permit persons to whom the Software is
         | 
| 10 | 
            +
            furnished to do so, subject to the following conditions:
         | 
| 11 | 
            +
             | 
| 12 | 
            +
            The above copyright notice and this permission notice shall be included in
         | 
| 13 | 
            +
            all copies or substantial portions of the Software.
         | 
| 14 | 
            +
             | 
| 15 | 
            +
            THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
         | 
| 16 | 
            +
            IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
         | 
| 17 | 
            +
            FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
         | 
| 18 | 
            +
            AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
         | 
| 19 | 
            +
            LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
         | 
| 20 | 
            +
            OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
         | 
| 21 | 
            +
            THE SOFTWARE.
         | 
    
        data/README.md
    ADDED
    
    | @@ -0,0 +1,73 @@ | |
| 1 | 
            +
            # Rx
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            Rx is a Rack middleware that provides tiered health checks to any Rack application, including Rails-based apps.
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            ## Tiered Health Checks
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            What is a tiered health check? I'm glad you asked! Health checks serve different purposes:
         | 
| 8 | 
            +
             | 
| 9 | 
            +
            - Some are used by load balancers to ensure that a server is capable of serving traffic
         | 
| 10 | 
            +
            - Some are used by other applications to verify the health of your service as a dependency
         | 
| 11 | 
            +
            - Some are used by customers or status pages to determine uptime
         | 
| 12 | 
            +
             | 
| 13 | 
            +
            These use cases are all similar, but may require different levels of verification. One may require your service to just return 200, while another may need to check connectivity to the database, cache, or external services.
         | 
| 14 | 
            +
             | 
| 15 | 
            +
            Rx provides three levels of health checks:
         | 
| 16 | 
            +
             | 
| 17 | 
            +
            1. `/liveness`: A health check that determines if the server is running.
         | 
| 18 | 
            +
            2. `/readiness`: Readiness checks determine if critical, dependent services are running (think a database or cache)
         | 
| 19 | 
            +
            3. `/deep`: A health check that walks your entire dependency tree, checking other critical and secondary services.
         | 
| 20 | 
            +
             | 
| 21 | 
            +
            ### Rails Applications
         | 
| 22 | 
            +
             | 
| 23 | 
            +
            Add `rx` to your Gemfile, and then create a new initializer with something like this:
         | 
| 24 | 
            +
             | 
| 25 | 
            +
            ```ruby
         | 
| 26 | 
            +
            Rails.application.config.middleware.insert(
         | 
| 27 | 
            +
              Rx::Middleware,
         | 
| 28 | 
            +
                liveness: [Rx::Check::FileSystemCheck.new],
         | 
| 29 | 
            +
                readiness: [
         | 
| 30 | 
            +
                  Rx::Check::FileSystemCheck.new,
         | 
| 31 | 
            +
                  Rx::Check::ActiveRecordCheck.new,
         | 
| 32 | 
            +
                  Rx::Check::HttpCheck.new("http://example.com"),
         | 
| 33 | 
            +
                  Rx::Check::GenericCheck.new(-> { $redis.ping == "PONG" }, "redis")],
         | 
| 34 | 
            +
                deep_critical: [Rx::Check::HttpCheck.new("http://criticalservice.com/health")],
         | 
| 35 | 
            +
                deep_secondary: [Rx::Check::HttpCheck.new("http://otherservice.com/health-check")]
         | 
| 36 | 
            +
              )
         | 
| 37 | 
            +
            ```
         | 
| 38 | 
            +
             | 
| 39 | 
            +
            ### Configuring Dependencies
         | 
| 40 | 
            +
             | 
| 41 | 
            +
            Now that you're running `rx`, you will need to configure which dependencies it tests in each health check. You can do this by passing `Rx::Check` objects to the middleware. `rx` ships with a number of standard checks:
         | 
| 42 | 
            +
             | 
| 43 | 
            +
            - Filesystem health
         | 
| 44 | 
            +
            - ActiveRecord
         | 
| 45 | 
            +
            - HTTP
         | 
| 46 | 
            +
            - Generic Check
         | 
| 47 | 
            +
             | 
| 48 | 
            +
            In addition to the stock checks, you may create your own by copying an existing check
         | 
| 49 | 
            +
            and modifying it (though it's probably simpler to just use GenericCheck).
         | 
| 50 | 
            +
             | 
| 51 | 
            +
            `RX::Middleware` accepts the following named parameters as configuration:
         | 
| 52 | 
            +
             | 
| 53 | 
            +
            ```ruby
         | 
| 54 | 
            +
            liveness: [],
         | 
| 55 | 
            +
            readiness: [],
         | 
| 56 | 
            +
            deep_critical: [],
         | 
| 57 | 
            +
            deep_secondary: []
         | 
| 58 | 
            +
            ```
         | 
| 59 | 
            +
             | 
| 60 | 
            +
            Each collection must contain 0 or more `Rx::Check` objects. Those checks will be performed when the health check is queried. Deep checks will always also run the readiness checks.
         | 
| 61 | 
            +
             | 
| 62 | 
            +
            ## Contributing
         | 
| 63 | 
            +
             | 
| 64 | 
            +
            Bug reports and pull requests are welcome on GitHub at https://github.com/zachpendleton/rx.
         | 
| 65 | 
            +
             | 
| 66 | 
            +
            Some tips for developing the gem locally:
         | 
| 67 | 
            +
             | 
| 68 | 
            +
            * Tests can be run by calling `rake`
         | 
| 69 | 
            +
            * You can point your Rails app to a local gem by adding a `path` option to your Gemfile, a la `gem "rx", path: "path/to/rx" (though you _will_ need to restart Rails whenever you change the gem).
         | 
| 70 | 
            +
             | 
| 71 | 
            +
            ## License
         | 
| 72 | 
            +
             | 
| 73 | 
            +
            The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
         | 
    
        data/Rakefile
    ADDED
    
    
    
        data/bin/console
    ADDED
    
    | @@ -0,0 +1,15 @@ | |
| 1 | 
            +
            #!/usr/bin/env ruby
         | 
| 2 | 
            +
            # frozen_string_literal: true
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            require "bundler/setup"
         | 
| 5 | 
            +
            require "rx"
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            # You can add fixtures and/or initialization code here to make experimenting
         | 
| 8 | 
            +
            # with your gem easier. You can also use a different console, if you like.
         | 
| 9 | 
            +
             | 
| 10 | 
            +
            # (If you use this, don't forget to add pry to your Gemfile!)
         | 
| 11 | 
            +
            # require "pry"
         | 
| 12 | 
            +
            # Pry.start
         | 
| 13 | 
            +
             | 
| 14 | 
            +
            require "irb"
         | 
| 15 | 
            +
            IRB.start(__FILE__)
         | 
    
        data/bin/setup
    ADDED
    
    
    
        data/lib/rx.rb
    ADDED
    
    | @@ -0,0 +1,19 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require_relative "rx/version"
         | 
| 4 | 
            +
            require_relative "rx/middleware"
         | 
| 5 | 
            +
            require_relative "rx/cache/in_memory_cache"
         | 
| 6 | 
            +
            require_relative "rx/cache/no_op_cache"
         | 
| 7 | 
            +
            require_relative "rx/check/active_record_check"
         | 
| 8 | 
            +
            require_relative "rx/check/file_system_check"
         | 
| 9 | 
            +
            require_relative "rx/check/generic_check"
         | 
| 10 | 
            +
            require_relative "rx/check/http_check"
         | 
| 11 | 
            +
            require_relative "rx/check/result"
         | 
| 12 | 
            +
            require_relative "rx/concurrent/future"
         | 
| 13 | 
            +
            require_relative "rx/concurrent/thread_pool"
         | 
| 14 | 
            +
            require_relative "rx/util/heap"
         | 
| 15 | 
            +
             | 
| 16 | 
            +
            module Rx
         | 
| 17 | 
            +
              class Error < StandardError; end
         | 
| 18 | 
            +
              # Your code goes here...
         | 
| 19 | 
            +
            end
         | 
| @@ -0,0 +1,51 @@ | |
| 1 | 
            +
            require_relative "../util/heap"
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Rx
         | 
| 4 | 
            +
              module Cache
         | 
| 5 | 
            +
                class InMemoryCache
         | 
| 6 | 
            +
                  def initialize
         | 
| 7 | 
            +
                    @heap = Rx::Util::Heap.new do |a, b|
         | 
| 8 | 
            +
                      a[1] < b[1]
         | 
| 9 | 
            +
                    end
         | 
| 10 | 
            +
                    @lock = Mutex.new
         | 
| 11 | 
            +
                    @map  = Hash.new
         | 
| 12 | 
            +
                  end
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                  def cache(k, expires_in = 60)
         | 
| 15 | 
            +
                    if value = get(k)
         | 
| 16 | 
            +
                      return value
         | 
| 17 | 
            +
                    end
         | 
| 18 | 
            +
                    
         | 
| 19 | 
            +
                    value = yield
         | 
| 20 | 
            +
                    put(k, value, expires_in)
         | 
| 21 | 
            +
                    value
         | 
| 22 | 
            +
                  end
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                  def get(k)
         | 
| 25 | 
            +
                    clean!
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                    lock.synchronize do
         | 
| 28 | 
            +
                      map[k]
         | 
| 29 | 
            +
                    end
         | 
| 30 | 
            +
                  end
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                  def put(k, v, expires_in = 60)
         | 
| 33 | 
            +
                    lock.synchronize do
         | 
| 34 | 
            +
                      map[k] = v
         | 
| 35 | 
            +
                      heap << [k, Time.now + expires_in]
         | 
| 36 | 
            +
                    end
         | 
| 37 | 
            +
                  end
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                  private
         | 
| 40 | 
            +
                  attr_reader :heap, :lock, :map
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                  def clean!
         | 
| 43 | 
            +
                    lock.synchronize do
         | 
| 44 | 
            +
                      while !heap.peek.nil? && heap.peek[1] < Time.now
         | 
| 45 | 
            +
                        map.delete(heap.pop[0])
         | 
| 46 | 
            +
                      end
         | 
| 47 | 
            +
                    end
         | 
| 48 | 
            +
                  end
         | 
| 49 | 
            +
                end
         | 
| 50 | 
            +
              end
         | 
| 51 | 
            +
            end
         | 
| @@ -0,0 +1,27 @@ | |
| 1 | 
            +
            module Rx
         | 
| 2 | 
            +
              module Check
         | 
| 3 | 
            +
                class ActiveRecordCheck
         | 
| 4 | 
            +
                  attr_reader :name
         | 
| 5 | 
            +
             | 
| 6 | 
            +
                  def initialize(name = "activerecord")
         | 
| 7 | 
            +
                    @name = name
         | 
| 8 | 
            +
                  end
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                  def check
         | 
| 11 | 
            +
                    Result.from(name) do
         | 
| 12 | 
            +
                      unless activerecord_defined?
         | 
| 13 | 
            +
                        raise StandardError.new("Undefined class ActiveRecord::Base")
         | 
| 14 | 
            +
                      end
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                      ActiveRecord::Base.connection.active?
         | 
| 17 | 
            +
                    end
         | 
| 18 | 
            +
                  end
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                  private
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                  def activerecord_defined?
         | 
| 23 | 
            +
                    defined?(ActiveRecord::Base)
         | 
| 24 | 
            +
                  end
         | 
| 25 | 
            +
                end
         | 
| 26 | 
            +
              end
         | 
| 27 | 
            +
            end
         | 
| @@ -0,0 +1,22 @@ | |
| 1 | 
            +
            module Rx
         | 
| 2 | 
            +
              module Check
         | 
| 3 | 
            +
                class FileSystemCheck
         | 
| 4 | 
            +
                  FILENAME = "rx".freeze
         | 
| 5 | 
            +
             | 
| 6 | 
            +
                  attr_reader :name
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                  def initialize(name = "fs")
         | 
| 9 | 
            +
                    @name = name
         | 
| 10 | 
            +
                  end
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                  def check
         | 
| 13 | 
            +
                    Result.from(name) do
         | 
| 14 | 
            +
                      !!Tempfile.open(FILENAME) do |f|
         | 
| 15 | 
            +
                        f.write("ok")
         | 
| 16 | 
            +
                        f.flush
         | 
| 17 | 
            +
                      end
         | 
| 18 | 
            +
                    end
         | 
| 19 | 
            +
                  end
         | 
| 20 | 
            +
                end
         | 
| 21 | 
            +
              end
         | 
| 22 | 
            +
            end
         | 
| @@ -0,0 +1,22 @@ | |
| 1 | 
            +
            module Rx
         | 
| 2 | 
            +
              module Check
         | 
| 3 | 
            +
                class GenericCheck
         | 
| 4 | 
            +
                  attr_reader :name
         | 
| 5 | 
            +
             | 
| 6 | 
            +
                  def initialize(callable, name = "generic")
         | 
| 7 | 
            +
                    @callable = callable
         | 
| 8 | 
            +
                    @name = name
         | 
| 9 | 
            +
                  end
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                  def check
         | 
| 12 | 
            +
                    Result.from(name) do
         | 
| 13 | 
            +
                      callable.call
         | 
| 14 | 
            +
                    end
         | 
| 15 | 
            +
                  end
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                  private
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                  attr_reader :callable
         | 
| 20 | 
            +
                end
         | 
| 21 | 
            +
              end
         | 
| 22 | 
            +
            end
         | 
| @@ -0,0 +1,37 @@ | |
| 1 | 
            +
            require "uri"
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Rx
         | 
| 4 | 
            +
              module Check
         | 
| 5 | 
            +
                class HttpCheck
         | 
| 6 | 
            +
                  attr_reader :name
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                  def initialize(url, name = "http")
         | 
| 9 | 
            +
                    @url = URI(url)
         | 
| 10 | 
            +
                    @name = name
         | 
| 11 | 
            +
                  end
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                  def check
         | 
| 14 | 
            +
                    Result.from(name) do
         | 
| 15 | 
            +
                      http = Net::HTTP.new(url.host, url.port)
         | 
| 16 | 
            +
                      http.read_timeout = 1
         | 
| 17 | 
            +
                      http.use_ssl = url.scheme == "https"
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                      response = http.request(Net::HTTP::Get.new(path))
         | 
| 20 | 
            +
                      response.code == "200"
         | 
| 21 | 
            +
                    end
         | 
| 22 | 
            +
                  end
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                  private
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                  attr_reader :url
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                  def path
         | 
| 29 | 
            +
                    path = url.path == "" ? "/" : url.path
         | 
| 30 | 
            +
                    path = "#{path}?#{url.query}" if url.query
         | 
| 31 | 
            +
                    path = "#{path}##{url.fragment}" if url.fragment
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                    path
         | 
| 34 | 
            +
                  end
         | 
| 35 | 
            +
                end
         | 
| 36 | 
            +
              end
         | 
| 37 | 
            +
            end
         | 
| @@ -0,0 +1,34 @@ | |
| 1 | 
            +
            module Rx
         | 
| 2 | 
            +
              module Check
         | 
| 3 | 
            +
                class Result
         | 
| 4 | 
            +
                  def self.from(check_name)
         | 
| 5 | 
            +
                    start_at = Time.now
         | 
| 6 | 
            +
                    err = nil
         | 
| 7 | 
            +
                    result = false
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                    begin
         | 
| 10 | 
            +
                      result = yield
         | 
| 11 | 
            +
                    rescue StandardError => ex
         | 
| 12 | 
            +
                      err = ex
         | 
| 13 | 
            +
                    end
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                    end_at = Time.now
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                    Result.new(check_name, result, ((end_at - start_at) * 1000).round(2), err)
         | 
| 18 | 
            +
                  end
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                  attr_reader :name, :timing, :error
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                  def initialize(name, ok, timing, error)
         | 
| 23 | 
            +
                    @name = name
         | 
| 24 | 
            +
                    @ok = ok
         | 
| 25 | 
            +
                    @timing = timing
         | 
| 26 | 
            +
                    @error = error
         | 
| 27 | 
            +
                  end
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                  def ok?
         | 
| 30 | 
            +
                    @ok
         | 
| 31 | 
            +
                  end
         | 
| 32 | 
            +
                end
         | 
| 33 | 
            +
              end
         | 
| 34 | 
            +
            end
         | 
| @@ -0,0 +1,71 @@ | |
| 1 | 
            +
            require "thread"
         | 
| 2 | 
            +
            require_relative "thread_pool"
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            module Rx
         | 
| 5 | 
            +
              module Concurrent
         | 
| 6 | 
            +
                class Future
         | 
| 7 | 
            +
                  @@pool = ThreadPool.new.start
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                  ALLOWED_STATES = %i[pending in_progress completed failed]
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                  attr_reader :error
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                  def self.execute(&block)
         | 
| 14 | 
            +
                    Future.new(&block).execute
         | 
| 15 | 
            +
                  end
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                  def initialize(&block)
         | 
| 18 | 
            +
                    @channel = Queue.new
         | 
| 19 | 
            +
                    @state = :pending
         | 
| 20 | 
            +
                    @work = block
         | 
| 21 | 
            +
                  end
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                  def completed?
         | 
| 24 | 
            +
                    state == :completed
         | 
| 25 | 
            +
                  end
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                  def execute
         | 
| 28 | 
            +
                    @state = :in_progress
         | 
| 29 | 
            +
                    pool.submit do
         | 
| 30 | 
            +
                      begin
         | 
| 31 | 
            +
                        channel << work.call
         | 
| 32 | 
            +
                        @state = :completed
         | 
| 33 | 
            +
                      rescue StandardError => ex
         | 
| 34 | 
            +
                        @error = ex
         | 
| 35 | 
            +
                        @state = :failed
         | 
| 36 | 
            +
                        channel.close
         | 
| 37 | 
            +
                      end
         | 
| 38 | 
            +
                    end
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                    self
         | 
| 41 | 
            +
                  end
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                  def failed?
         | 
| 44 | 
            +
                    state == :failed
         | 
| 45 | 
            +
                  end
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                  def in_progress?
         | 
| 48 | 
            +
                    state == :in_progress
         | 
| 49 | 
            +
                  end
         | 
| 50 | 
            +
             | 
| 51 | 
            +
                  def pending?
         | 
| 52 | 
            +
                    state == :pending
         | 
| 53 | 
            +
                  end
         | 
| 54 | 
            +
             | 
| 55 | 
            +
                  def value
         | 
| 56 | 
            +
                    if (completed? || failed?) && channel.empty?
         | 
| 57 | 
            +
                      return @value
         | 
| 58 | 
            +
                    end
         | 
| 59 | 
            +
             | 
| 60 | 
            +
                    @value = channel.pop
         | 
| 61 | 
            +
                  end
         | 
| 62 | 
            +
             | 
| 63 | 
            +
                  private
         | 
| 64 | 
            +
                  attr_reader :channel, :state, :work
         | 
| 65 | 
            +
             | 
| 66 | 
            +
                  def pool
         | 
| 67 | 
            +
                    @@pool
         | 
| 68 | 
            +
                  end
         | 
| 69 | 
            +
                end
         | 
| 70 | 
            +
              end
         | 
| 71 | 
            +
            end
         | 
| @@ -0,0 +1,53 @@ | |
| 1 | 
            +
            require "thread"
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Rx
         | 
| 4 | 
            +
              module Concurrent
         | 
| 5 | 
            +
                class ThreadPool
         | 
| 6 | 
            +
                  def initialize(size = Etc.nprocessors)
         | 
| 7 | 
            +
                    @pool = []
         | 
| 8 | 
            +
                    @size = size
         | 
| 9 | 
            +
                  end
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                  def shutdown
         | 
| 12 | 
            +
                    return unless started?
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                    queue.close
         | 
| 15 | 
            +
                    pool.map(&:join)
         | 
| 16 | 
            +
                    pool.clear
         | 
| 17 | 
            +
                  end
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                  def start
         | 
| 20 | 
            +
                    return if started?
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                    @queue = Queue.new
         | 
| 23 | 
            +
                    size.times { pool << Thread.new(&worker) }
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                    self
         | 
| 26 | 
            +
                  end
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                  def started?
         | 
| 29 | 
            +
                    pool.map(&:alive?).any?
         | 
| 30 | 
            +
                  end
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                  def submit(&block)
         | 
| 33 | 
            +
                    return unless started?
         | 
| 34 | 
            +
                    queue << block
         | 
| 35 | 
            +
                  end
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                  private
         | 
| 38 | 
            +
                  attr_reader :pool, :queue, :size
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                  def worker
         | 
| 41 | 
            +
                    -> {
         | 
| 42 | 
            +
                      while job = queue.pop
         | 
| 43 | 
            +
                        begin
         | 
| 44 | 
            +
                          job.call
         | 
| 45 | 
            +
                        rescue StandardError => _
         | 
| 46 | 
            +
                          # do nothing
         | 
| 47 | 
            +
                        end
         | 
| 48 | 
            +
                      end
         | 
| 49 | 
            +
                    }
         | 
| 50 | 
            +
                  end
         | 
| 51 | 
            +
                end
         | 
| 52 | 
            +
              end
         | 
| 53 | 
            +
            end
         | 
| @@ -0,0 +1,95 @@ | |
| 1 | 
            +
            require "json"
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Rx
         | 
| 4 | 
            +
              class Middleware
         | 
| 5 | 
            +
                DEFAULT_OPTIONS = {
         | 
| 6 | 
            +
                  cache: true
         | 
| 7 | 
            +
                }.freeze
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                def initialize(app,
         | 
| 10 | 
            +
                               liveness:       [Rx::Check::FileSystemCheck.new],
         | 
| 11 | 
            +
                               readiness:      [Rx::Check::FileSystemCheck.new],
         | 
| 12 | 
            +
                               deep_critical:  [],
         | 
| 13 | 
            +
                               deep_secondary: [],
         | 
| 14 | 
            +
                               options:        {})
         | 
| 15 | 
            +
                  @app = app
         | 
| 16 | 
            +
                  @options = DEFAULT_OPTIONS.merge(options)
         | 
| 17 | 
            +
                  @cache = cache_factory(self.options)
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                  @liveness_checks = liveness
         | 
| 20 | 
            +
                  @readiness_checks = readiness
         | 
| 21 | 
            +
                  @deep_critical_checks = deep_critical
         | 
| 22 | 
            +
                  @deep_secondary_checks = deep_secondary
         | 
| 23 | 
            +
                end
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                def call(env)
         | 
| 26 | 
            +
                  unless health_check_request?(env)
         | 
| 27 | 
            +
                    return app.call(env)
         | 
| 28 | 
            +
                  end
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                  case env["REQUEST_PATH"]
         | 
| 31 | 
            +
                  when "/liveness"
         | 
| 32 | 
            +
                    ok = check_to_component(liveness_checks).map { |x| x[:status] == 200 }.all?
         | 
| 33 | 
            +
                    liveness_response(ok)
         | 
| 34 | 
            +
                  when "/readiness"
         | 
| 35 | 
            +
                    readiness_response(check_to_component(readiness_checks))
         | 
| 36 | 
            +
                  when "/deep"
         | 
| 37 | 
            +
                    @cache.cache("deep") do
         | 
| 38 | 
            +
                      readiness = check_to_component(readiness_checks)
         | 
| 39 | 
            +
                      critical  = check_to_component(deep_critical_checks)
         | 
| 40 | 
            +
                      secondary = check_to_component(deep_secondary_checks)
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                      deep_response(readiness, critical, secondary)
         | 
| 43 | 
            +
                    end
         | 
| 44 | 
            +
                  end
         | 
| 45 | 
            +
                end
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                private
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                attr_reader :app, :liveness_checks, :readiness_checks, :deep_critical_checks,
         | 
| 50 | 
            +
                            :deep_secondary_checks, :options
         | 
| 51 | 
            +
             | 
| 52 | 
            +
                def cache_factory(options)
         | 
| 53 | 
            +
                  unless options[:cache]
         | 
| 54 | 
            +
                    return Rx::Cache::NoOpCache.new
         | 
| 55 | 
            +
                  end
         | 
| 56 | 
            +
             | 
| 57 | 
            +
                  Rx::Cache::InMemoryCache.new
         | 
| 58 | 
            +
                end
         | 
| 59 | 
            +
             | 
| 60 | 
            +
                def health_check_request?(env)
         | 
| 61 | 
            +
                  %w[/liveness /readiness /deep].include?(env["REQUEST_PATH"])
         | 
| 62 | 
            +
                end
         | 
| 63 | 
            +
             | 
| 64 | 
            +
                def liveness_response(is_ok)
         | 
| 65 | 
            +
                  [is_ok ? 200 : 503, {}, []]
         | 
| 66 | 
            +
                end
         | 
| 67 | 
            +
             | 
| 68 | 
            +
                def readiness_response(components)
         | 
| 69 | 
            +
                  status = components.map { |x| x[:status] == 200 }.all? ? 200 : 503
         | 
| 70 | 
            +
             | 
| 71 | 
            +
                  [
         | 
| 72 | 
            +
                    status,
         | 
| 73 | 
            +
                    {"content-type" => "application/json"},
         | 
| 74 | 
            +
                    [JSON.dump({status: status, components: components})]
         | 
| 75 | 
            +
                  ]
         | 
| 76 | 
            +
                end
         | 
| 77 | 
            +
             | 
| 78 | 
            +
                def deep_response(readiness, critical, secondary)
         | 
| 79 | 
            +
                  status = (readiness.map { |x| x[:status] == 200 } + critical.map { |x| x[:status] == 200 }).all? ? 200 : 503
         | 
| 80 | 
            +
             | 
| 81 | 
            +
                  [
         | 
| 82 | 
            +
                    status,
         | 
| 83 | 
            +
                    {"content-type" => "application/json"},
         | 
| 84 | 
            +
                    [JSON.dump(status: status, readiness: readiness, critical: critical, secondary: secondary)]
         | 
| 85 | 
            +
                  ]
         | 
| 86 | 
            +
                end
         | 
| 87 | 
            +
             | 
| 88 | 
            +
                def check_to_component(check)
         | 
| 89 | 
            +
                  Array(check)
         | 
| 90 | 
            +
                    .map { |check| Rx::Concurrent::Future.execute { check.check } }
         | 
| 91 | 
            +
                    .map(&:value)
         | 
| 92 | 
            +
                    .map { |r| { name: r.name, status: r.ok? ? 200 : 503, message: r.ok? ? "ok" : r.error, response_time_ms: r.timing } }
         | 
| 93 | 
            +
                end
         | 
| 94 | 
            +
              end
         | 
| 95 | 
            +
            end
         | 
    
        data/lib/rx/util/heap.rb
    ADDED
    
    | @@ -0,0 +1,104 @@ | |
| 1 | 
            +
            # heap = []
         | 
| 2 | 
            +
            # def add(value, heap)
         | 
| 3 | 
            +
            #   heap << value
         | 
| 4 | 
            +
            #   sort(heap)
         | 
| 5 | 
            +
            # end
         | 
| 6 | 
            +
            # def sort(heap)
         | 
| 7 | 
            +
            #   return if heap.length == 1
         | 
| 8 | 
            +
            #   last_parent = (heap.length - 2) / 2
         | 
| 9 | 
            +
            #   while last_parent >= 0
         | 
| 10 | 
            +
            #     l = last_parent * 2 + 1
         | 
| 11 | 
            +
            #     r = last_parent * 2 + 2
         | 
| 12 | 
            +
            #     s = last_parent
         | 
| 13 | 
            +
             | 
| 14 | 
            +
            #     if heap[l] < heap[last_parent]
         | 
| 15 | 
            +
            #       s = l
         | 
| 16 | 
            +
            #     end
         | 
| 17 | 
            +
             | 
| 18 | 
            +
            #     if r < heap.length && heap[r] < heap[last_parent]
         | 
| 19 | 
            +
            #       s = r
         | 
| 20 | 
            +
            #     end
         | 
| 21 | 
            +
             | 
| 22 | 
            +
            #     if s != last_parent
         | 
| 23 | 
            +
            #       heap[s], heap[last_parent] = heap[last_parent], heap[s]
         | 
| 24 | 
            +
            #     end
         | 
| 25 | 
            +
             | 
| 26 | 
            +
            #     last_parent -= 1
         | 
| 27 | 
            +
            #   end
         | 
| 28 | 
            +
            # end
         | 
| 29 | 
            +
             | 
| 30 | 
            +
            module Rx
         | 
| 31 | 
            +
              module Util
         | 
| 32 | 
            +
                class Heap
         | 
| 33 | 
            +
                  %i[empty? length size].each do |m|
         | 
| 34 | 
            +
                    define_method(m) { heap.send(m) }
         | 
| 35 | 
            +
                  end
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                  def initialize(items = [], &comparator)
         | 
| 38 | 
            +
                    @heap = items.dup
         | 
| 39 | 
            +
                    @comparator = block_given? ? comparator : -> (a, b) { a < b }
         | 
| 40 | 
            +
                    sort!
         | 
| 41 | 
            +
                  end
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                  def <<(item)
         | 
| 44 | 
            +
                    push(item)
         | 
| 45 | 
            +
                  end
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                  def peek
         | 
| 48 | 
            +
                    heap.first
         | 
| 49 | 
            +
                  end
         | 
| 50 | 
            +
             | 
| 51 | 
            +
                  def pop
         | 
| 52 | 
            +
                    item = heap.shift
         | 
| 53 | 
            +
                    sort!
         | 
| 54 | 
            +
                    item
         | 
| 55 | 
            +
                  end
         | 
| 56 | 
            +
             | 
| 57 | 
            +
                  def push(item)
         | 
| 58 | 
            +
                    heap << item
         | 
| 59 | 
            +
                    sort!
         | 
| 60 | 
            +
                    self
         | 
| 61 | 
            +
                  end
         | 
| 62 | 
            +
             | 
| 63 | 
            +
                  private
         | 
| 64 | 
            +
             | 
| 65 | 
            +
                  attr_reader :comparator, :heap
         | 
| 66 | 
            +
             | 
| 67 | 
            +
                  def left(n)
         | 
| 68 | 
            +
                    2 * n + 1
         | 
| 69 | 
            +
                  end
         | 
| 70 | 
            +
             | 
| 71 | 
            +
                  def parent(n)
         | 
| 72 | 
            +
                    (n - 1) / 2
         | 
| 73 | 
            +
                  end
         | 
| 74 | 
            +
             | 
| 75 | 
            +
                  def right(n)
         | 
| 76 | 
            +
                    2 * n + 2
         | 
| 77 | 
            +
                  end
         | 
| 78 | 
            +
             | 
| 79 | 
            +
                  def sort!
         | 
| 80 | 
            +
                    return if heap.length <= 1
         | 
| 81 | 
            +
                    n = parent(heap.length - 1)
         | 
| 82 | 
            +
                    while n >= 0
         | 
| 83 | 
            +
                      l = left(n)
         | 
| 84 | 
            +
                      r = right(n)
         | 
| 85 | 
            +
                      s = n
         | 
| 86 | 
            +
             | 
| 87 | 
            +
                      if comparator.call(heap[l], heap[s])
         | 
| 88 | 
            +
                        s = l
         | 
| 89 | 
            +
                      end
         | 
| 90 | 
            +
             | 
| 91 | 
            +
                      if r < heap.length && comparator.call(heap[r], heap[s])
         | 
| 92 | 
            +
                        s = r
         | 
| 93 | 
            +
                      end
         | 
| 94 | 
            +
             | 
| 95 | 
            +
                      if s != n
         | 
| 96 | 
            +
                        heap[s], heap[n] = heap[n], heap[s]
         | 
| 97 | 
            +
                      end
         | 
| 98 | 
            +
             | 
| 99 | 
            +
                      n -= 1
         | 
| 100 | 
            +
                    end
         | 
| 101 | 
            +
                  end
         | 
| 102 | 
            +
                end
         | 
| 103 | 
            +
              end
         | 
| 104 | 
            +
            end
         | 
    
        data/lib/rx/version.rb
    ADDED
    
    
    
        data/rx.gemspec
    ADDED
    
    | @@ -0,0 +1,32 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require_relative "lib/rx/version"
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            Gem::Specification.new do |spec|
         | 
| 6 | 
            +
              spec.name          = "rx-healthcheck"
         | 
| 7 | 
            +
              spec.version       = Rx::VERSION
         | 
| 8 | 
            +
              spec.authors       = ["Zach Pendleton"]
         | 
| 9 | 
            +
              spec.email         = ["zachpendleton@gmail.com"]
         | 
| 10 | 
            +
             | 
| 11 | 
            +
              spec.summary       = "Standard health checks for Rails and Rack applications"
         | 
| 12 | 
            +
              spec.homepage      = "https://github.com/zachpendleton/rx"
         | 
| 13 | 
            +
              spec.license       = "MIT"
         | 
| 14 | 
            +
              spec.required_ruby_version = Gem::Requirement.new(">= 2.4.0")
         | 
| 15 | 
            +
             | 
| 16 | 
            +
              spec.metadata["homepage_uri"] = spec.homepage
         | 
| 17 | 
            +
              spec.metadata["source_code_uri"] = "https://github.com/zachpendleton/rx"
         | 
| 18 | 
            +
             | 
| 19 | 
            +
              # Specify which files should be added to the gem when it is released.
         | 
| 20 | 
            +
              # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
         | 
| 21 | 
            +
              spec.files = Dir.chdir(File.expand_path(__dir__)) do
         | 
| 22 | 
            +
                `git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
         | 
| 23 | 
            +
              end
         | 
| 24 | 
            +
              spec.bindir        = "exe"
         | 
| 25 | 
            +
              spec.executables   = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
         | 
| 26 | 
            +
              spec.require_paths = ["lib"]
         | 
| 27 | 
            +
             | 
| 28 | 
            +
              spec.add_dependency "simplecov", "0.21.2"
         | 
| 29 | 
            +
             | 
| 30 | 
            +
              # For more information and examples about making a new gem, checkout our
         | 
| 31 | 
            +
              # guide at: https://bundler.io/guides/creating_gem.html
         | 
| 32 | 
            +
            end
         | 
    
        metadata
    ADDED
    
    | @@ -0,0 +1,81 @@ | |
| 1 | 
            +
            --- !ruby/object:Gem::Specification
         | 
| 2 | 
            +
            name: rx-healthcheck
         | 
| 3 | 
            +
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            +
              version: 0.1.0
         | 
| 5 | 
            +
            platform: ruby
         | 
| 6 | 
            +
            authors:
         | 
| 7 | 
            +
            - Zach Pendleton
         | 
| 8 | 
            +
            autorequire: 
         | 
| 9 | 
            +
            bindir: exe
         | 
| 10 | 
            +
            cert_chain: []
         | 
| 11 | 
            +
            date: 2021-05-25 00:00:00.000000000 Z
         | 
| 12 | 
            +
            dependencies:
         | 
| 13 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 14 | 
            +
              name: simplecov
         | 
| 15 | 
            +
              requirement: !ruby/object:Gem::Requirement
         | 
| 16 | 
            +
                requirements:
         | 
| 17 | 
            +
                - - '='
         | 
| 18 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 19 | 
            +
                    version: 0.21.2
         | 
| 20 | 
            +
              type: :runtime
         | 
| 21 | 
            +
              prerelease: false
         | 
| 22 | 
            +
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 23 | 
            +
                requirements:
         | 
| 24 | 
            +
                - - '='
         | 
| 25 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 26 | 
            +
                    version: 0.21.2
         | 
| 27 | 
            +
            description: 
         | 
| 28 | 
            +
            email:
         | 
| 29 | 
            +
            - zachpendleton@gmail.com
         | 
| 30 | 
            +
            executables: []
         | 
| 31 | 
            +
            extensions: []
         | 
| 32 | 
            +
            extra_rdoc_files: []
         | 
| 33 | 
            +
            files:
         | 
| 34 | 
            +
            - ".gitignore"
         | 
| 35 | 
            +
            - Gemfile
         | 
| 36 | 
            +
            - Gemfile.lock
         | 
| 37 | 
            +
            - LICENSE.txt
         | 
| 38 | 
            +
            - README.md
         | 
| 39 | 
            +
            - Rakefile
         | 
| 40 | 
            +
            - bin/console
         | 
| 41 | 
            +
            - bin/setup
         | 
| 42 | 
            +
            - lib/rx.rb
         | 
| 43 | 
            +
            - lib/rx/cache/in_memory_cache.rb
         | 
| 44 | 
            +
            - lib/rx/cache/no_op_cache.rb
         | 
| 45 | 
            +
            - lib/rx/check/active_record_check.rb
         | 
| 46 | 
            +
            - lib/rx/check/file_system_check.rb
         | 
| 47 | 
            +
            - lib/rx/check/generic_check.rb
         | 
| 48 | 
            +
            - lib/rx/check/http_check.rb
         | 
| 49 | 
            +
            - lib/rx/check/result.rb
         | 
| 50 | 
            +
            - lib/rx/concurrent/future.rb
         | 
| 51 | 
            +
            - lib/rx/concurrent/thread_pool.rb
         | 
| 52 | 
            +
            - lib/rx/middleware.rb
         | 
| 53 | 
            +
            - lib/rx/util/heap.rb
         | 
| 54 | 
            +
            - lib/rx/version.rb
         | 
| 55 | 
            +
            - rx.gemspec
         | 
| 56 | 
            +
            homepage: https://github.com/zachpendleton/rx
         | 
| 57 | 
            +
            licenses:
         | 
| 58 | 
            +
            - MIT
         | 
| 59 | 
            +
            metadata:
         | 
| 60 | 
            +
              homepage_uri: https://github.com/zachpendleton/rx
         | 
| 61 | 
            +
              source_code_uri: https://github.com/zachpendleton/rx
         | 
| 62 | 
            +
            post_install_message: 
         | 
| 63 | 
            +
            rdoc_options: []
         | 
| 64 | 
            +
            require_paths:
         | 
| 65 | 
            +
            - lib
         | 
| 66 | 
            +
            required_ruby_version: !ruby/object:Gem::Requirement
         | 
| 67 | 
            +
              requirements:
         | 
| 68 | 
            +
              - - ">="
         | 
| 69 | 
            +
                - !ruby/object:Gem::Version
         | 
| 70 | 
            +
                  version: 2.4.0
         | 
| 71 | 
            +
            required_rubygems_version: !ruby/object:Gem::Requirement
         | 
| 72 | 
            +
              requirements:
         | 
| 73 | 
            +
              - - ">="
         | 
| 74 | 
            +
                - !ruby/object:Gem::Version
         | 
| 75 | 
            +
                  version: '0'
         | 
| 76 | 
            +
            requirements: []
         | 
| 77 | 
            +
            rubygems_version: 3.1.2
         | 
| 78 | 
            +
            signing_key: 
         | 
| 79 | 
            +
            specification_version: 4
         | 
| 80 | 
            +
            summary: Standard health checks for Rails and Rack applications
         | 
| 81 | 
            +
            test_files: []
         |