Repository: rails/solid_cable Branch: main Commit: 92818a0fb781 Files: 168 Total size: 154.5 KB Directory structure: gitextract_sjfrw0w9/ ├── .github/ │ └── workflows/ │ ├── lint.yml │ └── test.yml ├── .gitignore ├── .rubocop.yml ├── Gemfile ├── MIT-LICENSE ├── README.md ├── Rakefile ├── app/ │ ├── jobs/ │ │ └── solid_cable/ │ │ └── trim_job.rb │ └── models/ │ └── solid_cable/ │ ├── message.rb │ └── record.rb ├── bench/ │ ├── .dockerignore │ ├── .ruby-version │ ├── Dockerfile │ ├── Gemfile │ ├── Rakefile │ ├── app/ │ │ ├── assets/ │ │ │ ├── builds/ │ │ │ │ └── .keep │ │ │ ├── images/ │ │ │ │ └── .keep │ │ │ └── stylesheets/ │ │ │ └── application.tailwind.css │ │ ├── channels/ │ │ │ ├── application_cable/ │ │ │ │ ├── channel.rb │ │ │ │ └── connection.rb │ │ │ └── broadcast_channel.rb │ │ ├── controllers/ │ │ │ ├── application_controller.rb │ │ │ ├── concerns/ │ │ │ │ └── .keep │ │ │ ├── rooms_controller.rb │ │ │ └── ws_debugger_controller.rb │ │ ├── javascript/ │ │ │ ├── application.js │ │ │ ├── channels/ │ │ │ │ ├── broadcast_channel.js │ │ │ │ ├── consumer.js │ │ │ │ └── index.js │ │ │ └── controllers/ │ │ │ ├── application.js │ │ │ └── index.js │ │ ├── jobs/ │ │ │ └── application_job.rb │ │ ├── mailers/ │ │ │ └── application_mailer.rb │ │ ├── models/ │ │ │ ├── application_record.rb │ │ │ ├── concerns/ │ │ │ │ └── .keep │ │ │ └── room.rb │ │ └── views/ │ │ ├── layouts/ │ │ │ ├── application.html.erb │ │ │ ├── mailer.html.erb │ │ │ └── mailer.text.erb │ │ ├── rooms/ │ │ │ ├── _form.html.erb │ │ │ ├── _room.html.erb │ │ │ ├── edit.html.erb │ │ │ ├── index.html.erb │ │ │ ├── new.html.erb │ │ │ └── show.html.erb │ │ └── ws_debugger/ │ │ └── show.html.erb │ ├── bin/ │ │ ├── bundle │ │ ├── docker-entrypoint │ │ ├── importmap │ │ ├── kamal │ │ ├── rails │ │ ├── rake │ │ ├── rubocop │ │ └── setup │ ├── config/ │ │ ├── application.rb │ │ ├── boot.rb │ │ ├── cable.yml │ │ ├── credentials.yml.enc │ │ ├── database.yml │ │ ├── deploy.yml │ │ ├── environment.rb │ │ ├── environments/ │ │ │ ├── development.rb │ │ │ ├── production.rb │ │ │ └── test.rb │ │ ├── importmap.rb │ │ ├── init.sql │ │ ├── initializers/ │ │ │ ├── assets.rb │ │ │ ├── content_security_policy.rb │ │ │ ├── filter_parameter_logging.rb │ │ │ ├── inflections.rb │ │ │ └── permissions_policy.rb │ │ ├── locales/ │ │ │ └── en.yml │ │ ├── puma.rb │ │ ├── routes.rb │ │ ├── storage.yml │ │ └── tailwind.config.js │ ├── config.ru │ ├── db/ │ │ ├── migrate/ │ │ │ ├── 20240529231225_create_rooms.rb │ │ │ ├── 20240530031126_create_solid_cable_message.solid_cable.rb │ │ │ ├── 20240607184931_index_channels.solid_cable.rb │ │ │ ├── 20240609023040_create_active_error_faults.active_error.rb │ │ │ ├── 20240609023041_create_active_error_instances.active_error.rb │ │ │ └── 20240912235943_create_compact_channel.rb │ │ ├── schema.rb │ │ └── seeds.rb │ ├── loadtest.js │ ├── log/ │ │ └── .keep │ ├── public/ │ │ ├── 404.html │ │ ├── 406-unsupported-browser.html │ │ ├── 422.html │ │ ├── 500.html │ │ └── robots.txt │ ├── test/ │ │ ├── channels/ │ │ │ ├── application_cable/ │ │ │ │ └── connection_test.rb │ │ │ └── broadcast_channel_test.rb │ │ ├── controllers/ │ │ │ └── .keep │ │ ├── fixtures/ │ │ │ ├── files/ │ │ │ │ └── .keep │ │ │ └── rooms.yml │ │ ├── models/ │ │ │ ├── .keep │ │ │ └── room_test.rb │ │ └── test_helper.rb │ └── vendor/ │ ├── .keep │ └── javascript/ │ └── .keep ├── bin/ │ ├── rails │ ├── release │ └── test ├── lib/ │ ├── action_cable/ │ │ └── subscription_adapter/ │ │ └── solid_cable.rb │ ├── generators/ │ │ └── solid_cable/ │ │ ├── install/ │ │ │ ├── USAGE │ │ │ ├── install_generator.rb │ │ │ └── templates/ │ │ │ ├── config/ │ │ │ │ └── cable.yml │ │ │ └── db/ │ │ │ └── cable_schema.rb │ │ └── update/ │ │ ├── USAGE │ │ ├── templates/ │ │ │ └── db/ │ │ │ └── migrate/ │ │ │ └── create_compact_channel.rb │ │ └── update_generator.rb │ ├── solid_cable/ │ │ ├── engine.rb │ │ └── version.rb │ ├── solid_cable.rb │ └── tasks/ │ └── solid_cable_tasks.rake ├── solid_cable.gemspec └── test/ ├── config_stubs.rb ├── dummy/ │ ├── Rakefile │ ├── app/ │ │ ├── controllers/ │ │ │ ├── application_controller.rb │ │ │ └── concerns/ │ │ │ └── .keep │ │ ├── helpers/ │ │ │ └── application_helper.rb │ │ ├── jobs/ │ │ │ └── application_job.rb │ │ ├── models/ │ │ │ ├── application_record.rb │ │ │ └── concerns/ │ │ │ └── .keep │ │ └── views/ │ │ ├── layouts/ │ │ │ ├── application.html.erb │ │ │ ├── mailer.html.erb │ │ │ └── mailer.text.erb │ │ └── pwa/ │ │ ├── manifest.json.erb │ │ └── service-worker.js │ ├── bin/ │ │ ├── ci │ │ ├── dev │ │ ├── rails │ │ ├── rake │ │ └── setup │ ├── config/ │ │ ├── application.rb │ │ ├── boot.rb │ │ ├── cable.yml │ │ ├── ci.rb │ │ ├── database.yml │ │ ├── environment.rb │ │ ├── environments/ │ │ │ ├── development.rb │ │ │ ├── production.rb │ │ │ └── test.rb │ │ ├── initializers/ │ │ │ ├── assets.rb │ │ │ ├── content_security_policy.rb │ │ │ ├── filter_parameter_logging.rb │ │ │ └── inflections.rb │ │ ├── locales/ │ │ │ └── en.yml │ │ ├── puma.rb │ │ ├── routes.rb │ │ └── storage.yml │ ├── config.ru │ ├── db/ │ │ └── schema.rb │ ├── log/ │ │ └── .keep │ └── public/ │ ├── 400.html │ ├── 404.html │ ├── 406-unsupported-browser.html │ ├── 422.html │ └── 500.html ├── jobs/ │ └── trim_job_test.rb ├── lib/ │ ├── action_cable/ │ │ └── subscription_adapter/ │ │ └── solid_cable_test.rb │ └── generators/ │ └── solid_cable/ │ ├── install/ │ │ └── install_generator_test.rb │ └── update/ │ └── update_generator_test.rb ├── solid_cable_test.rb └── test_helper.rb ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/lint.yml ================================================ name: lint on: push: branches: - main pull_request: jobs: build: runs-on: ubuntu-latest name: Linting steps: - uses: actions/checkout@v6 - name: Set up Ruby uses: ruby/setup-ruby@v1 with: ruby-version: '4.0' - name: Install dependencies run: bundle install - name: Run tests run: bundle exec rubocop --config=./.rubocop.yml --parallel ================================================ FILE: .github/workflows/test.yml ================================================ name: tests on: push: branches: - main pull_request: jobs: build: name: Tests runs-on: ubuntu-latest timeout-minutes: 10 strategy: fail-fast: false matrix: database: [mysql, postgres, sqlite] ruby-version: - 3.2 - 3.3 - 3.4 - 4.0 services: mysql: image: mysql:8.0.31 env: MYSQL_ALLOW_EMPTY_PASSWORD: "yes" ports: - 33060:3306 options: --health-cmd "mysql -h localhost -e \"select now()\"" --health-interval 1s --health-timeout 5s --health-retries 30 postgres: image: postgres:15.1 env: POSTGRES_HOST_AUTH_METHOD: "trust" ports: - 55432:5432 env: TARGET_DB: ${{ matrix.database }} steps: - uses: actions/checkout@v6 - name: Set up Ruby uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby-version }} - name: Install dependencies run: bundle install - name: Setup db run: | bin/rails db:setup - name: Run tests run: bin/test ================================================ FILE: .gitignore ================================================ /.bundle/ /doc/ /log/*.log /pkg/ /tmp/ /test/dummy/db/*.sqlite3 /test/dummy/db/*.sqlite3-* /test/dummy/log/*.log /test/dummy/storage/ /test/dummy/tmp/ Gemfile.lock /bench/db/*.sqlite3 /bench/db/*.sqlite3-* /bench/log/*.log /bench/storage/ /bench/tmp/ /bench/app/assets/builds/* !/bench/app/assets/builds/.keep /bench/config/master.key /bench/storage/* !/bench/storage/.keep /bench/tmp/storage/* !/bench/tmp/storage/ !/bench/tmp/storage/.keep /bench/public/assets /bench/.env* !/bench/.env*.erb /bench/k6 test/dummy/solid_cable_test ================================================ FILE: .rubocop.yml ================================================ inherit_gem: rubocop-rails-omakase: rubocop.yml AllCops: NewCops: enable SuggestExtensions: false Exclude: - vendor/**/* - Gemfile.lock - db/*_schema.rb - db/schema.rb - tmp/**/* - bin/**/* - test/dummy/**/* - bench/**/* - test/lib/action_cable/subscription_adapter/solid_cable_test.rb - lib/generators/**/*_schema.rb TargetRubyVersion: 3.3 ================================================ FILE: Gemfile ================================================ # frozen_string_literal: true source "https://rubygems.org" git_source(:github) { |repo| "https://github.com/#{repo}.git" } # Specify your gem's dependencies in solid_cable.gemspec. gemspec gem "puma" gem "pg" gem "sqlite3" gem "trilogy" gem "rubocop-rails-omakase" ================================================ FILE: MIT-LICENSE ================================================ Copyright Nick Pezza Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # Solid Cable Solid Cable is a database-backed Action Cable adapter that keeps messages in a table and continuously polls for updates. This makes it possible to drop the common dependency on Redis, if it isn't needed for any other purpose. Despite polling, the performance of Solid Cable is comparable to Redis in most situations. And in all circumstances, it makes it easier to deploy Rails when Redis is no longer a required dependency for Action Cable functionality. > [!NOTE] > Solid Cable is tested to work with MySQL, SQLite, and PostgreSQL. > > Action Cable already has a [dedicated PostgreSQL adapter](https://guides.rubyonrails.org/action_cable_overview.html#postgresql-adapter), > which utilizes the builtin `NOTIFY` command for better performance. However, that > adapter has an 8kb limit on its payload. Solid Cable is a great alternative if you find yourself > broadcasting large payloads, or prefer not to use the `NOTIFY` command. ## Installation Solid Cable is configured by default in new Rails 8 applications. But if you're running an earlier version, you can add it manually following these steps: 1. `bundle add solid_cable` 2. `bin/rails solid_cable:install` This will configure Solid Cable as the production cable adapter by overwritting `config/cable.yml` and create `db/cable_schema.rb`. You will then have to add the configuration for the cable database in `config/database.yml`. If you're using SQLite, it'll look like this: ```yaml production: primary: <<: *default database: storage/production.sqlite3 cable: <<: *default database: storage/production_cable.sqlite3 migrations_paths: db/cable_migrate ``` ...or if you're using MySQL/PostgreSQL/Trilogy: ```yaml production: primary: &primary_production <<: *default database: app_production username: app password: <%= ENV["APP_DATABASE_PASSWORD"] %> cable: <<: *primary_production database: app_production_cable migrations_paths: db/cable_migrate ``` > [!NOTE] > Calling `bin/rails solid_cable:install` will automatically setup `config/cable.yml`, so no additional configuration is needed there (although you must make sure that you use the `cable` name in `database.yml` for this to match!). But if you want to use Solid Cable in a different environment (like staging or even development), you'll have to manually add that `connects_to` block to the respective environment in the `config/cable.yml` file. And, as always, make sure that the name you're using for the database in `config/cable.yml` matches the name you define in `config/database.yml`. Then run `db:prepare` in production to ensure the database is created and the schema is loaded. ### Single database configuration Running Solid Cable in a separate database is recommended, but it's also possible to use a single database for both the app and Action Cable. 1. Copy the contents of `db/cable_schema.rb` into a normal migration and delete `db/cable_schema.rb` 2. Remove `connects_to` from `config/cable.yml` 3. `bin/rails db:migrate` You won't have multiple databases, so `database.yml` doesn't need to have primary and cable database. ## Configuration All configuration is managed via the `config/cable.yml` file. By default, it'll be configured like this: ```yaml production: adapter: solid_cable connects_to: database: writing: cable polling_interval: 0.1.seconds message_retention: 1.day ``` The options are: - `connects_to` - set the Active Record database configuration for the Solid Cable models. All options available in Active Record can be used here. - `polling_interval` - sets the frequency of the polling interval. (Defaults to 0.1.seconds) - `message_retention` - sets the retention time for messages kept in the database. Used as the cut-off when trimming is performed. (Defaults to 1.day) - `autotrim` - sets wether you want Solid Cable to handle autotrimming messages. (Defaults to true) - `silence_polling` - whether to silence Active Record logs emitted when polling (Defaults to true) - `use_skip_locked` - whether to use `FOR UPDATE SKIP LOCKED` when performing trimming. This will be automatically detected in the future, and for now, you'd only need to set this to `false` if your database doesn't support it. For MySQL, that'd be versions < 8, and for PostgreSQL, versions < 9.5. If you use SQLite, this has no effect, as writes are sequential. (Defaults to true) - `trim_batch_size` - the batch size to use when deleting old records (default: `100`) - `reconnect_attempts` - Supports a number of connection attempts or an array of durations to wait between attempts. (Defaults to 1 retry attempt) ## Trimming Messages are autotrimmed based upon the `message_retention` setting to determine how long messages are to be kept around. If no `message_retention` is given or parsing fails, it defaults to `1.day`. Messages are trimmed when a messsage is broadcast. Autotrimming can negatively impact performance slightly depending on your workload because it is potentially doing a delete on broadcast. If you would prefer, you can disable autotrimming by setting `autotrim: false` and you can manually enqueue the job later, `SolidCable::TrimJob.perform_later`, or run it on a recurring interval out of band. ## Upgrading If you have already installed Solid Cable < 3 and are upgrading to version 3, run `solid_cable:update` to install a new migration. ## Benchmarks Inside the `bench` directory there is a minimal Rails app that is used to benchmark. You are welcome to update the config/deploy.yml file to point to your own server if you want to deploy the app to your own server and run benchmarks. To benchmark we use [k6](https://k6.io). Most of the setup was gotten from this [article](https://evilmartians.com/chronicles/real-time-stress-anycable-k6-websockets-and-yabeda). 1. Install k6 1. Install xk6-cable by running `xk6 build --with github.com/anycable/xk6-cable`. This will output a custom k6 binary. 1. Run the load test with `./k6 run loadtest.js` - This script takes a variety of ENV variables: - WS_URL: The url to send websocket connections - MAX: The number of virtual users to hit the server with - TIME: The duration of the load test - MESSAGES_NUM: The number of messages each VU will send to the server #### Results Our loadtest is run on a Hetzner CCX13, with a MESSAGES_NUM of 5, and a TIME of 90. ##### SQLite With a polling interval of 0.1 seconds and autotrimming enabled. 100 VUs ``` rtt..................: avg=135.82ms min=50ms med=138ms max=357ms p(90)=174ms p(95)=195ms ws_connecting........: avg=205.81ms min=149.35ms med=199.01ms max=509.48ms p(90)=254.04ms p(95)=261.77ms ``` 250 VUs ``` rtt..................: avg=146.24ms min=50ms med=144ms max=435ms p(90)=209ms p(95)=234.04ms ws_connecting........: avg=222.15ms min=146.47ms med=208.57ms max=1.3s p(90)=263.6ms p(95)=284.18ms ``` 500 VUs ``` rtt..................: avg=271.79ms min=48ms med=205ms max=1.15s p(90)=558ms p(95)=660ms ws_connecting........: avg=248.81ms min=145.89ms med=221.89ms max=1.38s p(90)=290.41ms p(95)=322.2ms ``` 750 VUs ``` rtt..................: avg=548.27ms min=51ms med=438ms max=5.19s p(90)=1.18s p(95)=1.29s ws_connecting........: avg=266.37ms min=144.06ms med=224.93ms max=2.33s p(90)=298ms p(95)=342.87ms ``` With trimming disabled 250 VUs ``` rtt..................: avg=139.47ms min=48ms med=142ms max=807ms p(90)=189ms p(95)=214ms ws_connecting........: avg=212.58ms min=146.19ms med=196.25ms max=1.25s p(90)=255.74ms p(95)=272.44ms ``` With a polling interval of 0.01 seconds it becomes comparable to Redis 250 VUs ``` rtt..................: avg=84.22ms min=43ms med=69ms max=416ms p(90)=137ms p(95)=150ms ws_connecting........: avg=219.37ms min=144.71ms med=200.77ms max=2.17s p(90)=265.23ms p(95)=290.83ms ``` ##### Redis This instance was hosted on the same machine. 100 VUs ``` rtt..................: avg=68.95ms min=41ms med=56ms max=6.23s p(90)=114ms p(95)=129ms ws_connecting........: avg=211.09ms min=153.23ms med=195.69ms max=1.44s p(90)=258.1ms p(95)=272.23ms ``` 250 VUs ``` rtt..................: avg=69.32ms min=40ms med=56ms max=645ms p(90)=119ms p(95)=135ms ws_connecting........: avg=212.95ms min=142.92ms med=196.31ms max=1.25s p(90)=260.25ms p(95)=273.49ms ``` 500 VUs ``` rtt..................: avg=87.5ms min=40ms med=67ms max=839ms p(90)=149ms p(95)=176ms ws_connecting........: avg=242.62ms min=142.03ms med=213.76ms max=2.34s p(90)=291.25ms p(95)=324.04ms ``` 750 VUs ``` rtt..................: avg=162.54ms min=39ms med=123ms max=2.26s p(90)=343.1ms p(95)=438ms ws_connecting........: avg=353.08ms min=143ms med=264.15ms max=2.73s p(90)=541.36ms p(95)=1.15s ``` ##### MySQL With a polling interval of 0.1 seconds and autotrimming enabled. This instance was also hosted on the same machine. 100 VUs ``` rtt..................: avg=136.02ms min=51ms med=137ms max=877ms p(90)=168.1ms p(95)=198ms ws_connecting........: avg=207.76ms min=151.93ms med=196.74ms max=1.21s p(90)=249.91ms p(95)=260.37ms ``` 250 VUs ``` rtt..................: avg=159.33ms min=51ms med=149ms max=559ms p(90)=236ms p(95)=263ms ws_connecting........: avg=232.38ms min=151.6ms med=218.09ms max=1.38s p(90)=287.99ms p(95)=324.6ms ``` 500 VUs ``` rtt..................: avg=441.07ms min=51ms med=312ms max=2.29s p(90)=931ms p(95)=1.07s ws_connecting........: avg=256.73ms min=152.23ms med=231.02ms max=2.31s p(90)=305.69ms p(95)=340.83ms ``` 750 VUs ``` rtt..................: avg=822.08ms min=51ms med=732ms max=5.05s p(90)=1.76s p(95)=1.97s ws_connecting........: avg=278.08ms min=146.66ms med=236.35ms max=2.37s p(90)=318.17ms p(95)=374.98ms ``` ##### PostgreSQL with Solid Cable With a polling interval of 0.1 seconds and autotrimming enabled. This instance was also hosted on the same machine. 100 VUs ``` rtt..................: avg=137.45ms min=48ms med=139ms max=439ms p(90)=179.1ms p(95)=204ms ws_connecting........: avg=207.13ms min=150.29ms med=197.76ms max=443.67ms p(90)=254.44ms p(95)=263.29ms ``` 250 VUs ``` rtt..................: avg=151.63ms min=49ms med=146ms max=538ms p(90)=222ms p(95)=248.04ms ws_connecting........: avg=245.89ms min=147.18ms med=205.57ms max=30s p(90)=265.08ms p(95)=281.15ms ``` 500 VUs ``` rtt..................: avg=362.79ms min=50ms med=249ms max=1.21s p(90)=757ms p(95)=844ms ws_connecting........: avg=257.02ms min=146.13ms med=227.65ms max=2.39s p(90)=303.22ms p(95)=344.39ms ``` ##### PostgreSQL with dedicated adapter 100 VUs ``` rtt..................: avg=69.76ms min=41ms med=57ms max=622ms p(90)=116ms p(95)=133ms ws_connecting........: avg=210.97ms min=149.68ms med=196.06ms max=1.27s p(90)=259.67ms p(95)=273.17ms ``` 250 VUs ``` rtt..................: avg=73.43ms min=40ms med=58ms max=698ms p(90)=126ms p(95)=141ms ws_connecting........: avg=210.83ms min=143.01ms med=195.22ms max=1.27s p(90)=259.27ms p(95)=272.6ms ``` ## License The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). ================================================ FILE: Rakefile ================================================ require "bundler/setup" APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__) load "rails/tasks/engine.rake" require "bundler/gem_tasks" ================================================ FILE: app/jobs/solid_cable/trim_job.rb ================================================ # frozen_string_literal: true module SolidCable class TrimJob < ActiveJob::Base def perform return unless trim? ::SolidCable::Message.transaction do ids = ::SolidCable::Message.trimmable.non_blocking_lock. limit(trim_batch_size).pluck(:id) ::SolidCable::Message.where(id: ids).delete_all end end private def trim_batch_size ::SolidCable.trim_batch_size end def trim? expires_per_write = (1 / trim_batch_size.to_f) * ::SolidCable.trim_chance !::SolidCable.autotrim? || rand < (expires_per_write - expires_per_write.floor) end end end ================================================ FILE: app/models/solid_cable/message.rb ================================================ # frozen_string_literal: true module SolidCable class Message < SolidCable::Record scope :trimmable, lambda { where(created_at: ...::SolidCable.message_retention.ago) } scope :broadcastable, lambda { |channels, last_id| where(channel_hash: channel_hashes_for(channels)). where(id: (last_id.to_i + 1)..).order(:id) } class << self def broadcast(channel, payload) insert({ created_at: Time.current, channel:, payload:, channel_hash: channel_hash_for(channel) }) end def channel_hashes_for(channels) channels.map { |channel| channel_hash_for(channel) } end # Need to unpack this as a signed integer since Postgresql and SQLite # don't support unsigned integers def channel_hash_for(channel) Digest::SHA256.digest(channel.to_s).unpack1("q>") end end end end ================================================ FILE: app/models/solid_cable/record.rb ================================================ # frozen_string_literal: true module SolidCable class Record < ActiveRecord::Base self.abstract_class = true connects_to(**SolidCable.connects_to) if SolidCable.connects_to.present? def self.non_blocking_lock if SolidCable.use_skip_locked lock(Arel.sql("FOR UPDATE SKIP LOCKED")) else lock end end end end ================================================ FILE: bench/.dockerignore ================================================ # See https://docs.docker.com/engine/reference/builder/#dockerignore-file for more about ignoring files. # Ignore git directory. /.git/ # Ignore bundler config. /.bundle # Ignore all default key files. /config/master.key /config/credentials/*.key # Ignore all environment files. /.env* !/.env.example # Ignore all logfiles and tempfiles. /log/* /tmp/* !/log/.keep !/tmp/.keep tmp/* log/* tmp/cache/assets/* # Ignore pidfiles, but keep the directory. /tmp/pids/* !/tmp/pids/ !/tmp/pids/.keep # Ignore storage (uploaded files in development and any SQLite databases). /storage/* !/storage/.keep /tmp/storage/* /tmp/cache/* tmp/storage/* tmp/cache/* /coverage/* coverage/* !/tmp/storage/.keep # Ignore assets. /node_modules/ /app/assets/builds/* !/app/assets/builds/.keep /public/assets /vendor/bundle test/* /test/* tags /tags k6 ================================================ FILE: bench/.ruby-version ================================================ 3.3.5 ================================================ FILE: bench/Dockerfile ================================================ # syntax = docker/dockerfile:1 # This Dockerfile is designed for production, not development. Use with Kamal or build'n'run by hand: # docker build -t my-app . # docker run -d -p 80:80 -p 443:443 --name my-app -e RAILS_MASTER_KEY= my-app # For a containerized dev environment, see Dev Containers: https://guides.rubyonrails.org/getting_started_with_devcontainer.html # Make sure RUBY_VERSION matches the Ruby version in .ruby-version ARG RUBY_VERSION=3.3.5 FROM docker.io/library/ruby:$RUBY_VERSION-slim AS base # Rails app lives here WORKDIR /rails # Install base packages RUN apt-get update -qq && \ apt-get install --no-install-recommends -y curl libjemalloc2 postgresql-client && \ apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* # Set production environment ENV RAILS_ENV="production" \ BUNDLE_WITHOUT="development:test:linters:deploy" \ BUNDLE_DEPLOYMENT="1" \ BUNDLE_PATH="/usr/local/bundle" \ BUNDLE_WITHOUT="development" # Throw-away build stage to reduce size of final image FROM base AS build # Install packages needed to build gems RUN apt-get update -qq && \ apt-get install --no-install-recommends -y build-essential git pkg-config libpq-dev && \ apt-get clean && rm -rf /var/cache/apt/archives /var/lib/apt/lists/* /tmp/* /var/tmp/* # Install application gems COPY Gemfile Gemfile.lock ./ RUN bundle install && \ rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git && \ bundle exec bootsnap precompile --gemfile # Copy application code COPY . . RUN bundle exec bootsnap precompile app/ lib/ && \ SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile && \ rm -rf vendor/ruby/3.3.0/cache # Final stage for app image FROM base # Copy built artifacts: gems, application COPY --from=build "${BUNDLE_PATH}" "${BUNDLE_PATH}" COPY --from=build /rails /rails # Run and own only the runtime files as a non-root user for security RUN groupadd --system --gid 1000 rails && \ useradd rails --uid 1000 --gid 1000 --create-home --shell /bin/bash && \ mkdir /data && \ chown -R rails:rails db log storage tmp /data USER 1000:1000 # Entrypoint prepares the database. ENTRYPOINT ["/rails/bin/docker-entrypoint"] # Start the server by default, this can be overwritten at runtime EXPOSE 3000 CMD ["./bin/rails", "server"] ================================================ FILE: bench/Gemfile ================================================ source "https://rubygems.org" gem "rails", github: "rails/rails", branch: "main" gem "activeerror" gem "propshaft" gem "solid_cable", github: "rails/solid_cable", branch: "main" gem "sqlite3" gem "trilogy" gem "pg" gem "puma" gem "importmap-rails" gem "redis" gem "bootsnap", require: false gem "kamal", require: false gem "tailwindcss-rails" ================================================ FILE: bench/Rakefile ================================================ # Add your own tasks in files placed in lib/tasks ending in .rake, # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. require_relative "config/application" Rails.application.load_tasks ================================================ FILE: bench/app/assets/builds/.keep ================================================ ================================================ FILE: bench/app/assets/images/.keep ================================================ ================================================ FILE: bench/app/assets/stylesheets/application.tailwind.css ================================================ @tailwind base; @tailwind components; @tailwind utilities; /* @layer components { .btn-primary { @apply py-2 px-4 bg-blue-200; } } */ ================================================ FILE: bench/app/channels/application_cable/channel.rb ================================================ # :markup: markdown module ApplicationCable class Channel < ActionCable::Channel::Base end end ================================================ FILE: bench/app/channels/application_cable/connection.rb ================================================ module ApplicationCable class Connection < ActionCable::Connection::Base rescue_from Exception do |error| Rails.error.report(e, handled: false, source: "application.action_cable") end identified_by :id def connect self.id = SecureRandom.uuid end end end ================================================ FILE: bench/app/channels/broadcast_channel.rb ================================================ class BroadcastChannel < ApplicationCable::Channel def subscribed Rails.logger.info "a client subscribed: #{id}" stream_from "broadcast:#{id}" end def unsubscribed Rails.logger.info "unsubscribed: #{id}" stop_all_streams end def ping(data) broadcast_to id, { message: "pong #{data.with_indifferent_access[:message]}" } end end ================================================ FILE: bench/app/controllers/application_controller.rb ================================================ class ApplicationController < ActionController::Base end ================================================ FILE: bench/app/controllers/concerns/.keep ================================================ ================================================ FILE: bench/app/controllers/rooms_controller.rb ================================================ class RoomsController < ApplicationController before_action :set_room, only: %i[ show edit update destroy ] # GET /rooms def index @rooms = Room.all end # GET /rooms/1 def show end # GET /rooms/new def new @room = Room.new end # GET /rooms/1/edit def edit end # POST /rooms def create @room = Room.new(room_params) if @room.save redirect_to @room, notice: "Room was successfully created." else render :new, status: :unprocessable_entity end end # PATCH/PUT /rooms/1 def update if @room.update(room_params) redirect_to @room, notice: "Room was successfully updated.", status: :see_other else render :edit, status: :unprocessable_entity end end # DELETE /rooms/1 def destroy @room.destroy! redirect_to rooms_url, notice: "Room was successfully destroyed.", status: :see_other end private # Use callbacks to share common setup or constraints between actions. def set_room @room = Room.find(params[:id]) end # Only allow a list of trusted parameters through. def room_params params.require(:room).permit(:name) end end ================================================ FILE: bench/app/controllers/ws_debugger_controller.rb ================================================ class WsDebuggerController < ApplicationController end ================================================ FILE: bench/app/javascript/application.js ================================================ // Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails import "channels" ================================================ FILE: bench/app/javascript/channels/broadcast_channel.js ================================================ import consumer from "channels/consumer" consumer.subscriptions.create("BroadcastChannel", { connected() { // Called when the subscription is ready for use on the server }, disconnected() { // Called when the subscription has been terminated by the server }, received(data) { // Called when there's incoming data on the websocket for this channel } }); ================================================ FILE: bench/app/javascript/channels/consumer.js ================================================ // Action Cable provides the framework to deal with WebSockets in Rails. // You can generate new channels where WebSocket features live using the `bin/rails generate channel` command. import { createConsumer } from "@rails/actioncable" export default createConsumer() ================================================ FILE: bench/app/javascript/channels/index.js ================================================ // Import all the channels to be used by Action Cable import "channels/broadcast_channel" ================================================ FILE: bench/app/javascript/controllers/application.js ================================================ import { Application } from "@hotwired/stimulus" const application = Application.start() // Configure Stimulus development experience application.debug = false window.Stimulus = application export { application } ================================================ FILE: bench/app/javascript/controllers/index.js ================================================ // Import and register all your controllers from the importmap under controllers/* import { application } from "controllers/application" // Eager load all controllers defined in the import map under controllers/**/*_controller import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading" eagerLoadControllersFrom("controllers", application) // Lazy load controllers as they appear in the DOM (remember not to preload controllers in import map!) // import { lazyLoadControllersFrom } from "@hotwired/stimulus-loading" // lazyLoadControllersFrom("controllers", application) ================================================ FILE: bench/app/jobs/application_job.rb ================================================ class ApplicationJob < ActiveJob::Base # Automatically retry jobs that encountered a deadlock # retry_on ActiveRecord::Deadlocked # Most jobs are safe to ignore if the underlying records are no longer available # discard_on ActiveJob::DeserializationError end ================================================ FILE: bench/app/mailers/application_mailer.rb ================================================ class ApplicationMailer < ActionMailer::Base default from: "from@example.com" layout "mailer" end ================================================ FILE: bench/app/models/application_record.rb ================================================ class ApplicationRecord < ActiveRecord::Base primary_abstract_class end ================================================ FILE: bench/app/models/concerns/.keep ================================================ ================================================ FILE: bench/app/models/room.rb ================================================ class Room < ApplicationRecord end ================================================ FILE: bench/app/views/layouts/application.html.erb ================================================ <%= content_for(:title) || "Cablebench" %> <%= csrf_meta_tags %> <%= csp_meta_tag %> <%= yield :head %> <%= stylesheet_link_tag "tailwind", "inter-font", "data-turbo-track": "reload" %> <%= stylesheet_link_tag :all, "data-turbo-track": "reload" %> <%= javascript_importmap_tags %>
<%= yield %>
================================================ FILE: bench/app/views/layouts/mailer.html.erb ================================================ <%= yield %> ================================================ FILE: bench/app/views/layouts/mailer.text.erb ================================================ <%= yield %> ================================================ FILE: bench/app/views/rooms/_form.html.erb ================================================ <%= form_with(model: room, class: "contents") do |form| %> <% if room.errors.any? %>

<%= pluralize(room.errors.count, "error") %> prohibited this room from being saved:

<% end %>
<%= form.label :name %> <%= form.text_field :name, class: "block shadow rounded-md border border-gray-400 outline-none px-3 py-2 mt-2 w-full" %>
<%= form.submit class: "rounded-lg py-3 px-5 bg-blue-600 text-white inline-block font-medium cursor-pointer" %>
<% end %> ================================================ FILE: bench/app/views/rooms/_room.html.erb ================================================

Name: <%= room.name %>

================================================ FILE: bench/app/views/rooms/edit.html.erb ================================================

Editing room

<%= render "form", room: @room %> <%= link_to "Show this room", @room, class: "ml-2 rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium" %> <%= link_to "Back to rooms", rooms_path, class: "ml-2 rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium" %>
================================================ FILE: bench/app/views/rooms/index.html.erb ================================================
<% if notice.present? %>

<%= notice %>

<% end %> <% content_for :title, "Rooms" %>

Rooms

<%= link_to "New room", new_room_path, class: "rounded-lg py-3 px-5 bg-blue-600 text-white block font-medium" %>
<% @rooms.each do |room| %> <%= render room %>

<%= link_to "Show this room", room, class: "ml-2 rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium" %>

<% end %>
================================================ FILE: bench/app/views/rooms/new.html.erb ================================================

New room

<%= render "form", room: @room %> <%= link_to "Back to rooms", rooms_path, class: "ml-2 rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium" %>
================================================ FILE: bench/app/views/rooms/show.html.erb ================================================
<% if notice.present? %>

<%= notice %>

<% end %> <%= render @room %> <%= link_to "Edit this room", edit_room_path(@room), class: "mt-2 rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium" %> <%= link_to "Back to rooms", rooms_path, class: "ml-2 rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium" %>
<%= button_to "Destroy this room", @room, method: :delete, class: "mt-2 rounded-lg py-3 px-5 bg-gray-100 font-medium" %>
================================================ FILE: bench/app/views/ws_debugger/show.html.erb ================================================

hello world

================================================ FILE: bench/bin/bundle ================================================ #!/usr/bin/env ruby # frozen_string_literal: true # # This file was generated by Bundler. # # The application 'bundle' is installed as part of a gem, and # this file is here to facilitate running it. # require "rubygems" m = Module.new do module_function def invoked_as_script? File.expand_path($0) == File.expand_path(__FILE__) end def env_var_version ENV["BUNDLER_VERSION"] end def cli_arg_version return unless invoked_as_script? # don't want to hijack other binstubs return unless "update".start_with?(ARGV.first || " ") # must be running `bundle update` bundler_version = nil update_index = nil ARGV.each_with_index do |a, i| if update_index && update_index.succ == i && a.match?(Gem::Version::ANCHORED_VERSION_PATTERN) bundler_version = a end next unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/ bundler_version = $1 update_index = i end bundler_version end def gemfile gemfile = ENV["BUNDLE_GEMFILE"] return gemfile if gemfile && !gemfile.empty? File.expand_path("../Gemfile", __dir__) end def lockfile lockfile = case File.basename(gemfile) when "gems.rb" then gemfile.sub(/\.rb$/, ".locked") else "#{gemfile}.lock" end File.expand_path(lockfile) end def lockfile_version return unless File.file?(lockfile) lockfile_contents = File.read(lockfile) return unless lockfile_contents =~ /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/ Regexp.last_match(1) end def bundler_requirement @bundler_requirement ||= env_var_version || cli_arg_version || bundler_requirement_for(lockfile_version) end def bundler_requirement_for(version) return "#{Gem::Requirement.default}.a" unless version bundler_gem_version = Gem::Version.new(version) bundler_gem_version.approximate_recommendation end def load_bundler! ENV["BUNDLE_GEMFILE"] ||= gemfile activate_bundler end def activate_bundler gem_error = activation_error_handling do gem "bundler", bundler_requirement end return if gem_error.nil? require_error = activation_error_handling do require "bundler/version" end return if require_error.nil? && Gem::Requirement.new(bundler_requirement).satisfied_by?(Gem::Version.new(Bundler::VERSION)) warn "Activating bundler (#{bundler_requirement}) failed:\n#{gem_error.message}\n\nTo install the version of bundler this project requires, run `gem install bundler -v '#{bundler_requirement}'`" exit 42 end def activation_error_handling yield nil rescue StandardError, LoadError => e e end end m.load_bundler! if m.invoked_as_script? load Gem.bin_path("bundler", "bundle") end ================================================ FILE: bench/bin/docker-entrypoint ================================================ #!/bin/bash -e # Enable jemalloc for reduced memory usage and latency. if [ -z "${LD_PRELOAD+x}" ] && [ -f /usr/lib/*/libjemalloc.so.2 ]; then export LD_PRELOAD="$(echo /usr/lib/*/libjemalloc.so.2)" fi # If running the rails server then create or migrate existing database if [ "${1}" == "./bin/rails" ] && [ "${2}" == "server" ]; then ./bin/rails db:prepare fi exec "${@}" ================================================ FILE: bench/bin/importmap ================================================ #!/usr/bin/env ruby require_relative "../config/application" require "importmap/commands" ================================================ FILE: bench/bin/kamal ================================================ #!/usr/bin/env ruby # frozen_string_literal: true # # This file was generated by Bundler. # # The application 'kamal' is installed as part of a gem, and # this file is here to facilitate running it. # ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) bundle_binstub = File.expand_path("bundle", __dir__) if File.file?(bundle_binstub) if File.read(bundle_binstub, 300).include?("This file was generated by Bundler") load(bundle_binstub) else abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") end end require "rubygems" require "bundler/setup" load Gem.bin_path("kamal", "kamal") ================================================ FILE: bench/bin/rails ================================================ #!/usr/bin/env ruby APP_PATH = File.expand_path("../config/application", __dir__) require_relative "../config/boot" require "rails/commands" ================================================ FILE: bench/bin/rake ================================================ #!/usr/bin/env ruby require_relative "../config/boot" require "rake" Rake.application.run ================================================ FILE: bench/bin/rubocop ================================================ #!/usr/bin/env ruby require "rubygems" require "bundler/setup" # explicit rubocop config increases performance slightly while avoiding config confusion. ARGV.unshift("--config", File.expand_path("../.rubocop.yml", __dir__)) load Gem.bin_path("rubocop", "rubocop") ================================================ FILE: bench/bin/setup ================================================ #!/usr/bin/env ruby require "fileutils" APP_ROOT = File.expand_path("..", __dir__) APP_NAME = "cablebench" def system!(*args) system(*args, exception: true) end FileUtils.chdir APP_ROOT do # This script is a way to set up or update your development environment automatically. # This script is idempotent, so that you can run it at any time and get an expectable outcome. # Add necessary setup steps to this file. puts "== Installing dependencies ==" system! "gem install bundler --conservative" system("bundle check") || system!("bundle install") # puts "\n== Copying sample files ==" # unless File.exist?("config/database.yml") # FileUtils.cp "config/database.yml.sample", "config/database.yml" # end puts "\n== Preparing database ==" system! "bin/rails db:prepare" puts "\n== Removing old logs and tempfiles ==" system! "bin/rails log:clear tmp:clear" puts "\n== Restarting application server ==" system! "bin/rails restart" # puts "\n== Configuring puma-dev ==" # system "ln -nfs #{APP_ROOT} ~/.puma-dev/#{APP_NAME}" # system "curl -Is https://#{APP_NAME}.test/up | head -n 1" end ================================================ FILE: bench/config/application.rb ================================================ require_relative "boot" require "rails/all" # Require the gems listed in Gemfile, including any gems # you've limited to :test, :development, or :production. Bundler.require(*Rails.groups) module Cablebench class Application < Rails::Application config.action_cable.mount_path = '/cable' config.action_cable.disable_request_forgery_protection = true # Initialize configuration defaults for originally generated Rails version. config.load_defaults 8.0 # Please, add to the `ignore` list any other `lib` subdirectories that do # not contain `.rb` files, or that should not be reloaded or eager loaded. # Common ones are `templates`, `generators`, or `middleware`, for example. config.autoload_lib(ignore: %w[assets tasks]) # Configuration for the application, engines, and railties goes here. # # These settings can be overridden in specific environments using the files # in config/environments, which are processed later. # # config.time_zone = "Central Time (US & Canada)" # config.eager_load_paths << Rails.root.join("extras") end end ================================================ FILE: bench/config/boot.rb ================================================ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) require "bundler/setup" # Set up gems listed in the Gemfile. require "bootsnap/setup" # Speed up boot time by caching expensive operations. ================================================ FILE: bench/config/cable.yml ================================================ development: # adapter: redis # url: redis://localhost:6379/1 adapter: solid_cable message_retention: 15.minutes polling_interval: 0.1.seconds test: adapter: test production: # adapter: redis # url: <%= ENV.fetch("REDIS_URL") { "redis://#{ENV["HOST"]}:6379/1" } %> # channel_prefix: cablebench_production # adapter: postgresql # database: cablebench # user: cablebench # password: <%= ENV["POSTGRES_PASSWORD"] %> # host: <%= ENV["HOST"] %> adapter: solid_cable message_retention: 1.day polling_interval: 0.1.seconds ================================================ FILE: bench/config/credentials.yml.enc ================================================ 7lxKveoOh/YSo6BSUdrNHtg29iVtfI9AinJgpSiiQkSWpoVPHluT1PNVQpleQhs3LqM0WP4aMBX12xzUP843RI2LYQmtUmbjkJtfkMs/dCijfslkwfrwSI8ApQINOTwVG3fbYdFKe5N8C2Qhqbch+vMDktSL/eTXw7x1dSaUUjRF1zfEzi+xtn8D4nbDSv3m5tbUAzG2D3NU2bYIBaMtSNlnu0orY6RFPi8HRnGArsoMd41cBxUUJP6a23jatCGTl7BRrwRncBpoMKPbimiqE2YpE1XD+A25qcjA/1kxneSVvRl+0S0lUqVbUO9L84tsj9prt8YWKTeDWP9MWu9wGvyVoUb9eDMf15k3soBrKvG2++oZ6t/2S7pIg9gGEMBcRZwq9nHRnaS5SEhUFBmb4yr40/qWdpKGFKGAA/9al3sD3ChAlYXc5zQ81V4+o3hCxapvWbUr--2KQ9UYDapBokyl5i--Me6kHSK8rQVQyCeZM2w1lw== ================================================ FILE: bench/config/database.yml ================================================ default: &default adapter: sqlite3 pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> timeout: 5000 development: <<: *default database: storage/development.sqlite3 # Warning: The database defined as "test" will be erased and # re-generated from your development database when you run "rake". # Do not set this db to the same as development or production. test: <<: *default database: storage/test.sqlite3 # Store production database in the storage/ directory, which by default # is mounted as a persistent Docker volume in config/deploy.yml. production: # <<: *default # adapter: sqlite3 # pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> # timeout: 5000 # database: /data/production.sqlite3 # adapter: trilogy # pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 50 } %> # timeout: 5000 # database: solid_cable # user: root # password: <%= ENV["MYSQL_ROOT_PASSWORD"] %> # host: <%= ENV["HOST"] %> # ssl: true adapter: postgresql pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 50 } %> timeout: 5000 database: cablebench user: cablebench password: <%= ENV["POSTGRES_PASSWORD"] %> host: <%= ENV["HOST"] %> ================================================ FILE: bench/config/deploy.yml ================================================ service: solidcable image: npezza/solid_cable asset_path: /rails/public/assets servers: web: - <%= ENV["HOST"] %> registry: username: npezza password: - KAMAL_REGISTRY_PASSWORD env: secret: - RAILS_MASTER_KEY - MYSQL_ROOT_PASSWORD - HOST - POSTGRES_PASSWORD volumes: - "solidcable:/data" builder: context: "." local: arch: amd64 accessories: mysql: image: mysql:8.3 host: "<%= ENV['HOST'] %>" port: 3306 env: clear: MYSQL_ROOT_HOST: '%' secret: - MYSQL_ROOT_PASSWORD files: - config/init.sql:/docker-entrypoint-initdb.d/setup.sql directories: - data:/var/lib/mysql redis: image: redis:latest host: "<%= ENV['HOST'] %>" port: 6379 cmd: "redis-server" volumes: - /var/lib/redis:/data postgres: image: postgres:16 host: "<%= ENV['HOST'] %>" port: 5432 env: clear: POSTGRES_USER: "cablebench" POSTGRES_DB: "cablebench" secret: - POSTGRES_PASSWORD files: - config/init.sql:/docker-entrypoint-initdb.d/setup.sql directories: - data:/var/lib/postgresql/data ================================================ FILE: bench/config/environment.rb ================================================ # Load the Rails application. require_relative "application" # Initialize the Rails application. Rails.application.initialize! ================================================ FILE: bench/config/environments/development.rb ================================================ require "active_support/core_ext/integer/time" Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. # In the development environment your application's code is reloaded any time # it changes. This slows down response time but is perfect for development # since you don't have to restart the web server when you make code changes. config.enable_reloading = true # Do not eager load code on boot. config.eager_load = false # Show full error reports. config.consider_all_requests_local = true # Enable server timing. config.server_timing = true # Enable/disable caching. By default caching is disabled. # Run rails dev:cache to toggle caching. if Rails.root.join("tmp/caching-dev.txt").exist? config.action_controller.perform_caching = true config.action_controller.enable_fragment_cache_logging = true config.cache_store = :memory_store config.public_file_server.headers = { "Cache-Control" => "public, max-age=#{2.days.to_i}" } else config.action_controller.perform_caching = false config.cache_store = :null_store end # Store uploaded files on the local file system (see config/storage.yml for options). config.active_storage.service = :local # Don't care if the mailer can't send. config.action_mailer.raise_delivery_errors = false config.action_mailer.perform_caching = false config.action_mailer.default_url_options = { host: "localhost", port: 3000 } # Print deprecation notices to the Rails logger. config.active_support.deprecation = :log # Raise an error on page load if there are pending migrations. config.active_record.migration_error = :page_load # Highlight code that triggered database queries in logs. config.active_record.verbose_query_logs = true # Highlight code that enqueued background job in logs. config.active_job.verbose_enqueue_logs = true # Raises error for missing translations. # config.i18n.raise_on_missing_translations = true # Annotate rendered view with file names. config.action_view.annotate_rendered_view_with_filenames = true # Uncomment if you wish to allow Action Cable access from any origin. # config.action_cable.disable_request_forgery_protection = true # Raise error when a before_action's only/except options reference missing actions. config.action_controller.raise_on_missing_callback_actions = true # Apply autocorrection by RuboCop to files generated by `bin/rails generate`. # config.generators.apply_rubocop_autocorrect_after_generate! end ================================================ FILE: bench/config/environments/production.rb ================================================ require "active_support/core_ext/integer/time" Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. # Code is not reloaded between requests. config.enable_reloading = false # Eager load code on boot. This eager loads most of Rails and # your application in memory, allowing both threaded web servers # and those relying on copy on write to perform better. # Rake tasks automatically ignore this option for performance. config.eager_load = true # Full error reports are disabled and caching is turned on. config.consider_all_requests_local = false config.action_controller.perform_caching = true # Ensures that a master key has been made available in ENV["RAILS_MASTER_KEY"], config/master.key, or an environment # key such as config/credentials/production.key. This key is used to decrypt credentials (and other encrypted files). # config.require_master_key = true # Disable serving static files from `public/`, relying on NGINX/Apache to do so instead. # config.public_file_server.enabled = false # Enable serving of images, stylesheets, and JavaScripts from an asset server. # config.asset_host = "http://assets.example.com" # Specifies the header that your server uses for sending files. # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for Apache # config.action_dispatch.x_sendfile_header = "X-Accel-Redirect" # for NGINX # Store uploaded files on the local file system (see config/storage.yml for options). config.active_storage.service = :local # Mount Action Cable outside main process or domain. # config.action_cable.mount_path = nil # config.action_cable.url = "wss://example.com/cable" # config.action_cable.allowed_request_origins = [ "http://example.com", /http:\/\/example.*/ ] config.action_cable.worker_pool_size = 10 # Assume all access to the app is happening through a SSL-terminating reverse proxy. # Can be used together with config.force_ssl for Strict-Transport-Security and secure cookies. config.assume_ssl = true # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. config.force_ssl = true # Skip http-to-https redirect for the default health check endpoint. # config.ssl_options = { redirect: { exclude: ->(request) { request.path == "/up" } } } # Log to STDOUT by default config.logger = ActiveSupport::Logger.new(STDOUT) .tap { |logger| logger.formatter = ::Logger::Formatter.new } .then { |logger| ActiveSupport::TaggedLogging.new(logger) } # Prepend all log lines with the following tags. config.log_tags = [ :request_id ] # "info" includes generic and useful information about system operation, but avoids logging too much # information to avoid inadvertent exposure of personally identifiable information (PII). If you # want to log everything, set the level to "debug". config.log_level = ENV.fetch("RAILS_LOG_LEVEL", "debug") # Use a different cache store in production. # config.cache_store = :mem_cache_store # Use a real queuing backend for Active Job (and separate queues per environment). # config.active_job.queue_adapter = :resque # config.active_job.queue_name_prefix = "cablebench_production" config.action_mailer.perform_caching = false # Ignore bad email addresses and do not raise email delivery errors. # Set this to true and configure the email server for immediate delivery to raise delivery errors. # config.action_mailer.raise_delivery_errors = false # Enable locale fallbacks for I18n (makes lookups for any locale fall back to # the I18n.default_locale when a translation cannot be found). config.i18n.fallbacks = true # Don't log any deprecations. config.active_support.report_deprecations = false # Do not dump schema after migrations. config.active_record.dump_schema_after_migration = false # Enable DNS rebinding protection and other `Host` header attacks. # config.hosts = [ # "example.com", # Allow requests from example.com # /.*\.example\.com/ # Allow requests from subdomains like `www.example.com` # ] # Skip DNS rebinding protection for the default health check endpoint. # config.host_authorization = { exclude: ->(request) { request.path == "/up" } } end ================================================ FILE: bench/config/environments/test.rb ================================================ require "active_support/core_ext/integer/time" # The test environment is used exclusively to run your application's # test suite. You never need to work with it otherwise. Remember that # your test database is "scratch space" for the test suite and is wiped # and recreated between test runs. Don't rely on the data there! Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. # While tests run files are not watched, reloading is not necessary. config.enable_reloading = false # Eager loading loads your entire application. When running a single test locally, # this is usually not necessary, and can slow down your test suite. However, it's # recommended that you enable it in continuous integration systems to ensure eager # loading is working properly before deploying your code. config.eager_load = ENV["CI"].present? # Configure public file server for tests with Cache-Control for performance. config.public_file_server.headers = { "Cache-Control" => "public, max-age=#{1.hour.to_i}" } # Show full error reports and disable caching. config.consider_all_requests_local = true config.action_controller.perform_caching = false config.cache_store = :null_store # Render exception templates for rescuable exceptions and raise for other exceptions. config.action_dispatch.show_exceptions = :rescuable # Disable request forgery protection in test environment. config.action_controller.allow_forgery_protection = false # Store uploaded files on the local file system in a temporary directory. config.active_storage.service = :test config.action_mailer.perform_caching = false # Tell Action Mailer not to deliver emails to the real world. # The :test delivery method accumulates sent emails in the # ActionMailer::Base.deliveries array. config.action_mailer.delivery_method = :test # Unlike controllers, the mailer instance doesn't have any context about the # incoming request so you'll need to provide the :host parameter yourself. config.action_mailer.default_url_options = { host: "www.example.com" } # Print deprecation notices to the stderr. config.active_support.deprecation = :stderr # Raises error for missing translations. # config.i18n.raise_on_missing_translations = true # Annotate rendered view with file names. # config.action_view.annotate_rendered_view_with_filenames = true # Raise error when a before_action's only/except options reference missing actions. config.action_controller.raise_on_missing_callback_actions = true end ================================================ FILE: bench/config/importmap.rb ================================================ # Pin npm packages by running ./bin/importmap pin "application" pin "@rails/actioncable", to: "actioncable.esm.js" pin_all_from "app/javascript/channels", under: "channels" ================================================ FILE: bench/config/init.sql ================================================ CREATE DATABASE cablebench; ================================================ FILE: bench/config/initializers/assets.rb ================================================ # Be sure to restart your server when you modify this file. # Version of your assets, change this if you want to expire all your assets. Rails.application.config.assets.version = "1.0" # Add additional assets to the asset load path. # Rails.application.config.assets.paths << Emoji.images_path ================================================ FILE: bench/config/initializers/content_security_policy.rb ================================================ # Be sure to restart your server when you modify this file. # Define an application-wide content security policy. # See the Securing Rails Applications Guide for more information: # https://guides.rubyonrails.org/security.html#content-security-policy-header # Rails.application.configure do # config.content_security_policy do |policy| # policy.default_src :self, :https # policy.font_src :self, :https, :data # policy.img_src :self, :https, :data # policy.object_src :none # policy.script_src :self, :https # policy.style_src :self, :https # # Specify URI for violation reports # # policy.report_uri "/csp-violation-report-endpoint" # end # # # Generate session nonces for permitted importmap, inline scripts, and inline styles. # config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s } # config.content_security_policy_nonce_directives = %w(script-src style-src) # # # Report violations without enforcing the policy. # # config.content_security_policy_report_only = true # end ================================================ FILE: bench/config/initializers/filter_parameter_logging.rb ================================================ # Be sure to restart your server when you modify this file. # Configure parameters to be partially matched (e.g. passw matches password) and filtered from the log file. # Use this to limit dissemination of sensitive information. # See the ActiveSupport::ParameterFilter documentation for supported notations and behaviors. Rails.application.config.filter_parameters += [ :passw, :email, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn ] ================================================ FILE: bench/config/initializers/inflections.rb ================================================ # Be sure to restart your server when you modify this file. # Add new inflection rules using the following format. Inflections # are locale specific, and you may define rules for as many different # locales as you wish. All of these examples are active by default: # ActiveSupport::Inflector.inflections(:en) do |inflect| # inflect.plural /^(ox)$/i, "\\1en" # inflect.singular /^(ox)en/i, "\\1" # inflect.irregular "person", "people" # inflect.uncountable %w( fish sheep ) # end # These inflection rules are supported but not enabled by default: # ActiveSupport::Inflector.inflections(:en) do |inflect| # inflect.acronym "RESTful" # end ================================================ FILE: bench/config/initializers/permissions_policy.rb ================================================ # Be sure to restart your server when you modify this file. # Define an application-wide HTTP permissions policy. For further # information see: https://developers.google.com/web/updates/2018/06/feature-policy # Rails.application.config.permissions_policy do |policy| # policy.camera :none # policy.gyroscope :none # policy.microphone :none # policy.usb :none # policy.fullscreen :self # policy.payment :self, "https://secure.example.com" # end ================================================ FILE: bench/config/locales/en.yml ================================================ # Files in the config/locales directory are used for internationalization and # are automatically loaded by Rails. If you want to use locales other than # English, add the necessary files in this directory. # # To use the locales, use `I18n.t`: # # I18n.t "hello" # # In views, this is aliased to just `t`: # # <%= t("hello") %> # # To use a different locale, set it with `I18n.locale`: # # I18n.locale = :es # # This would use the information in config/locales/es.yml. # # To learn more about the API, please read the Rails Internationalization guide # at https://guides.rubyonrails.org/i18n.html. # # Be aware that YAML interprets the following case-insensitive strings as # booleans: `true`, `false`, `on`, `off`, `yes`, `no`. Therefore, these strings # must be quoted to be interpreted as strings. For example: # # en: # "yes": yup # enabled: "ON" en: hello: "Hello world" ================================================ FILE: bench/config/puma.rb ================================================ # This configuration file will be evaluated by Puma. The top-level methods that # are invoked here are part of Puma's configuration DSL. For more information # about methods provided by the DSL, see https://puma.io/puma/Puma/DSL.html. # Puma starts a configurable number of processes (workers) and each process # serves each request in a thread from an internal thread pool. # # The ideal number of threads per worker depends both on how much time the # application spends waiting for IO operations and on how much you wish to # prioritize throughput over latency. # # As a rule of thumb, increasing the number of threads will increase how much # traffic a given process can handle (throughput), but due to CRuby's # Global VM Lock (GVL) it has diminishing returns and will degrade the # response time (latency) of the application. # # The default is set to 3 threads as it's deemed a decent compromise between # throughput and latency for the average Rails application. # # Any libraries that use a connection pool or another resource pool should # be configured to provide at least as many connections as the number of # threads. This includes Active Record's `pool` parameter in `database.yml`. threads_count = ENV.fetch("RAILS_MAX_THREADS", 3) threads threads_count, threads_count # Specifies the `environment` that Puma will run in. rails_env = ENV.fetch("RAILS_ENV", "development") environment rails_env case rails_env when "production" # If you are running more than 1 thread per process, the workers count # should be equal to the number of processors (CPU cores) in production. # # Automatically detect the number of available processors in production. require "concurrent-ruby" workers_count = Integer(ENV.fetch("WEB_CONCURRENCY") { Concurrent.available_processor_count }) workers workers_count if workers_count > 1 worker_timeout 240 preload_app! when "development" # Specifies a very generous `worker_timeout` so that the worker # isn't killed by Puma when suspended by a debugger. worker_timeout 3600 end # Specifies the `port` that Puma will listen on to receive requests; default is 3000. port ENV.fetch("PORT", 3000) # Allow puma to be restarted by `bin/rails restart` command. plugin :tmp_restart # Only use a pidfile when requested pidfile ENV["PIDFILE"] if ENV["PIDFILE"] ================================================ FILE: bench/config/routes.rb ================================================ Rails.application.routes.draw do mount ActiveError::Engine => "/errors" mount ActionCable.server => "/cable" get "/ws" => "ws_debugger#show" # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html # Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500. # Can be used by load balancers and uptime monitors to verify that the app is live. get "up" => "rails/health#show", as: :rails_health_check resources :rooms root "rooms#index" end ================================================ FILE: bench/config/storage.yml ================================================ test: service: Disk root: <%= Rails.root.join("tmp/storage") %> local: service: Disk root: <%= Rails.root.join("storage") %> # Use bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key) # amazon: # service: S3 # access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %> # secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %> # region: us-east-1 # bucket: your_own_bucket-<%= Rails.env %> # Remember not to checkin your GCS keyfile to a repository # google: # service: GCS # project: your_project # credentials: <%= Rails.root.join("path/to/gcs.keyfile") %> # bucket: your_own_bucket-<%= Rails.env %> # Use bin/rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key) # microsoft: # service: AzureStorage # storage_account_name: your_account_name # storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %> # container: your_container_name-<%= Rails.env %> # mirror: # service: Mirror # primary: local # mirrors: [ amazon, google, microsoft ] ================================================ FILE: bench/config/tailwind.config.js ================================================ const defaultTheme = require('tailwindcss/defaultTheme') module.exports = { content: [ './public/*.html', './app/helpers/**/*.rb', './app/javascript/**/*.js', './app/views/**/*.{erb,haml,html,slim}' ], theme: { extend: { fontFamily: { sans: ['Inter var', ...defaultTheme.fontFamily.sans], }, }, }, plugins: [ require('@tailwindcss/forms'), require('@tailwindcss/typography'), require('@tailwindcss/container-queries'), ] } ================================================ FILE: bench/config.ru ================================================ # This file is used by Rack-based servers to start the application. require_relative "config/environment" run Rails.application Rails.application.load_server ================================================ FILE: bench/db/migrate/20240529231225_create_rooms.rb ================================================ class CreateRooms < ActiveRecord::Migration[8.0] def change create_table :rooms do |t| t.string :name t.timestamps end end end ================================================ FILE: bench/db/migrate/20240530031126_create_solid_cable_message.solid_cable.rb ================================================ # frozen_string_literal: true # This migration comes from solid_cable (originally 20240103034713) class CreateSolidCableMessage < ActiveRecord::Migration[7.1] def change create_table :solid_cable_messages, if_not_exists: true do |t| t.text :channel t.text :payload t.timestamps t.index :created_at end end end ================================================ FILE: bench/db/migrate/20240607184931_index_channels.solid_cable.rb ================================================ # This migration comes from solid_cable (originally 20240607184711) class IndexChannels < ActiveRecord::Migration[7.1] def change add_index :solid_cable_messages, :channel, length: 500 end end ================================================ FILE: bench/db/migrate/20240609023040_create_active_error_faults.active_error.rb ================================================ # frozen_string_literal: true # This migration comes from active_error (originally 20200727220359) class CreateActiveErrorFaults < ActiveRecord::Migration[7.1] def change # rubocop:disable Metrics/AbcSize create_table :active_error_faults do |t| t.belongs_to :cause t.binary :backtrace, limit: 512.megabytes t.binary :backtrace_locations, limit: 512.megabytes t.string :klass t.text :message t.string :controller t.string :action t.integer :instances_count t.text :blamed_files, limit: 512.megabytes t.text :options t.timestamps end end end ================================================ FILE: bench/db/migrate/20240609023041_create_active_error_instances.active_error.rb ================================================ # frozen_string_literal: true # This migration comes from active_error (originally 20200727225318) class CreateActiveErrorInstances < ActiveRecord::Migration[7.1] def change create_table :active_error_instances do |t| t.belongs_to :fault t.text :url t.binary :headers, limit: 512.megabytes t.binary :parameters, limit: 512.megabytes t.timestamps end end end ================================================ FILE: bench/db/migrate/20240912235943_create_compact_channel.rb ================================================ # frozen_string_literal: true class CreateCompactChannel < ActiveRecord::Migration[7.2] def up change_column :solid_cable_messages, :channel, :binary, limit: 1024, null: false add_column :solid_cable_messages, :channel_hash, :integer, limit: 8, if_not_exists: true add_index :solid_cable_messages, :channel_hash, if_not_exists: true change_column :solid_cable_messages, :payload, :binary, limit: 536_870_912, null: false SolidCable::Message.reset_column_information SolidCable::Message.find_each do |msg| msg.update(channel_hash: SolidCable::Message.channel_hash_for(msg.channel)) end end def down change_column :solid_cable_messages, :channel, :text remove_column :solid_cable_messages, :channel_hash, if_exists: true change_column :solid_cable_messages, :payload, :text end end ================================================ FILE: bench/db/schema.rb ================================================ # This file is auto-generated from the current state of the database. Instead # of editing this file, please use the migrations feature of Active Record to # incrementally modify your database, and then regenerate this schema definition. # # This file is the source Rails uses to define your schema when running `bin/rails # db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to # be faster and is potentially less error prone than running all of your # migrations from scratch. Old migrations may fail to apply correctly if those # migrations use external dependencies or application code. # # It's strongly recommended that you check this file into your version control system. ActiveRecord::Schema[8.0].define(version: 2024_09_12_235943) do create_table "active_error_faults", force: :cascade do |t| t.integer "cause_id" t.binary "backtrace", limit: 536870912 t.binary "backtrace_locations", limit: 536870912 t.string "klass" t.text "message" t.string "controller" t.string "action" t.integer "instances_count" t.text "blamed_files", limit: 536870912 t.text "options" t.datetime "created_at", null: false t.datetime "updated_at", null: false t.index ["cause_id"], name: "index_active_error_faults_on_cause_id" end create_table "active_error_instances", force: :cascade do |t| t.integer "fault_id" t.text "url" t.binary "headers", limit: 536870912 t.binary "parameters", limit: 536870912 t.datetime "created_at", null: false t.datetime "updated_at", null: false t.index ["fault_id"], name: "index_active_error_instances_on_fault_id" end create_table "rooms", force: :cascade do |t| t.string "name" t.datetime "created_at", null: false t.datetime "updated_at", null: false end create_table "solid_cable_messages", force: :cascade do |t| t.binary "channel", limit: 1024, null: false t.binary "payload", limit: 536870912, null: false t.datetime "created_at", null: false t.integer "channel_hash", limit: 8, null: false t.index ["channel"], name: "index_solid_cable_messages_on_channel" t.index ["channel_hash"], name: "index_solid_cable_messages_on_channel_hash" t.index ["created_at"], name: "index_solid_cable_messages_on_created_at" end end ================================================ FILE: bench/db/seeds.rb ================================================ # This file should ensure the existence of records required to run the application in every environment (production, # development, test). The code here should be idempotent so that it can be executed at any point in every environment. # The data can then be loaded with the bin/rails db:seed command (or created alongside the database with db:setup). # # Example: # # ["Action", "Comedy", "Drama", "Horror"].each do |genre_name| # MovieGenre.find_or_create_by!(name: genre_name) # end ================================================ FILE: bench/loadtest.js ================================================ import { check, sleep, fail } from "k6"; import cable from "k6/x/cable"; import { randomIntBetween } from "https://jslib.k6.io/k6-utils/1.1.0/index.js"; import { Trend } from "k6/metrics"; let rttTrend = new Trend("rtt", true); const WS_URL = __ENV.WS_URL || "wss://solid-cable.dev/cable"; const WS_COOKIE = __ENV.WS_COOKIE; // we need a valid cookie to authorize request const MAX = parseInt(__ENV.MAX || "20"); // Total test duration const TIME = parseInt(__ENV.TIME || "90"); const MESSAGES_NUM = parseInt(__ENV.NUM || "5"); export let options = { thresholds: { checks: ["rate>0.9"], }, scenarios: { ping: { // We use ramping executor to slowly increase the number of users during a test executor: "ramping-vus", startVUs: (MAX / 10 || 1) | 0, stages: [ { duration: `${TIME / 3}s`, target: (MAX / 4) | 0 }, { duration: `${(7 * TIME) / 12}s`, target: MAX }, { duration: `${TIME / 12}s`, target: 0 }, ], }, }, }; export default function () { const client = cable.connect(WS_URL, { cookies: WS_COOKIE, receiveTimeoutMS: 60000 }); if (!check(client, { "successful connection": (obj) => obj })) { fail("connection failed"); } const channel = client.subscribe("BroadcastChannel", {}); if (!check(channel, { "successful subscription": (obj) => obj })) { fail("failed to subscribe"); } for (let i = 0; i < MESSAGES_NUM; i++) { let startMessage = Date.now(); channel.perform("ping", { message: `Hello ${i}` }); let message = channel.receive(); if (!check(message, { "received res": (obj) => obj.message === `pong Hello ${i}` })) { fail("expected message hasn't been received"); } let endMessage = Date.now(); rttTrend.add(endMessage - startMessage); sleep(randomIntBetween(5, 10) / 10); } // Terminate the WS connection client.disconnect(); } ================================================ FILE: bench/log/.keep ================================================ ================================================ FILE: bench/public/404.html ================================================ The page you were looking for doesn't exist (404)

The page you were looking for doesn't exist.

You may have mistyped the address or the page may have moved.

If you are the application owner check the logs for more information.

================================================ FILE: bench/public/406-unsupported-browser.html ================================================ Your browser is not supported (406)

Your browser is not supported.

Please upgrade your browser to continue.

================================================ FILE: bench/public/422.html ================================================ The change you wanted was rejected (422)

The change you wanted was rejected.

Maybe you tried to change something you didn't have access to.

If you are the application owner check the logs for more information.

================================================ FILE: bench/public/500.html ================================================ We're sorry, but something went wrong (500)

We're sorry, but something went wrong.

If you are the application owner check the logs for more information.

================================================ FILE: bench/public/robots.txt ================================================ # See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file ================================================ FILE: bench/test/channels/application_cable/connection_test.rb ================================================ require "test_helper" module ApplicationCable class ConnectionTest < ActionCable::Connection::TestCase # test "connects with cookies" do # cookies.signed[:user_id] = 42 # # connect # # assert_equal connection.user_id, "42" # end end end ================================================ FILE: bench/test/channels/broadcast_channel_test.rb ================================================ require "test_helper" class BroadcastChannelTest < ActionCable::Channel::TestCase # test "subscribes" do # subscribe # assert subscription.confirmed? # end end ================================================ FILE: bench/test/controllers/.keep ================================================ ================================================ FILE: bench/test/fixtures/files/.keep ================================================ ================================================ FILE: bench/test/fixtures/rooms.yml ================================================ # Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html one: name: MyString two: name: MyString ================================================ FILE: bench/test/models/.keep ================================================ ================================================ FILE: bench/test/models/room_test.rb ================================================ require "test_helper" class RoomTest < ActiveSupport::TestCase # test "the truth" do # assert true # end end ================================================ FILE: bench/test/test_helper.rb ================================================ ENV["RAILS_ENV"] ||= "test" require_relative "../config/environment" require "rails/test_help" module ActiveSupport class TestCase # Run tests in parallel with specified workers parallelize(workers: :number_of_processors) # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. fixtures :all # Add more helper methods to be used by all tests here... end end ================================================ FILE: bench/vendor/.keep ================================================ ================================================ FILE: bench/vendor/javascript/.keep ================================================ ================================================ FILE: bin/rails ================================================ #!/usr/bin/env ruby # This command will automatically be run when you run "rails" with Rails gems # installed from the root of your application. ENGINE_ROOT = File.expand_path("..", __dir__) ENGINE_PATH = File.expand_path("../lib/solid_cable/engine", __dir__) APP_PATH = File.expand_path("../test/dummy/config/application", __dir__) # Set up gems listed in the Gemfile. ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) require "bundler/setup" if File.exist?(ENV["BUNDLE_GEMFILE"]) require "rails/all" require "rails/engine/commands" ================================================ FILE: bin/release ================================================ #!/usr/bin/env bash VERSION=$1 if [ -z "$VERSION" ] then echo "Usage: bin/release " exit 1 fi printf "# frozen_string_literal: true\n\nmodule SolidCable\n VERSION = \"$VERSION\"\nend\n" > ./lib/solid_cable/version.rb bundle git add lib/solid_cable/version.rb git commit -m "Bump version for $VERSION" rake release ================================================ FILE: bin/test ================================================ #!/usr/bin/env ruby $: << File.expand_path("../test", __dir__) require "bundler/setup" require "rails/plugin/test" ================================================ FILE: lib/action_cable/subscription_adapter/solid_cable.rb ================================================ # frozen_string_literal: true require "action_cable/subscription_adapter/base" require "action_cable/subscription_adapter/channel_prefix" require "action_cable/subscription_adapter/subscriber_map" require "concurrent/atomic/semaphore" module ActionCable module SubscriptionAdapter class SolidCable < ::ActionCable::SubscriptionAdapter::Base prepend ::ActionCable::SubscriptionAdapter::ChannelPrefix def initialize(*) super @listener = nil end def broadcast(channel, payload) ::SolidCable::Message.broadcast(channel, payload) ::SolidCable::TrimJob.perform_now if ::SolidCable.autotrim? end def subscribe(channel, callback, success_callback = nil) listener.add_subscriber(channel, callback, success_callback) end def unsubscribe(channel, callback) listener.remove_subscriber(channel, callback) end delegate :shutdown, to: :listener private def listener @listener || @server.mutex.synchronize do @listener ||= Listener.new(@server.event_loop) end end class Listener < ::ActionCable::SubscriptionAdapter::SubscriberMap CONNECTION_ERRORS = [ ActiveRecord::ConnectionFailed ] Stop = Class.new(Exception) def initialize(event_loop) super() @event_loop = event_loop # Critical section begins with 0 permits. It can be understood as # being "normally held" by the listener thread. It is released # for specific sections of code, rather than acquired. @critical = Concurrent::Semaphore.new(0) @reconnect_attempt = 0 @last_id = last_message_id @thread = Thread.new do Thread.current.name = "solid_cable_listener" Thread.current.report_on_exception = true begin listen rescue *CONNECTION_ERRORS retry if retry_connecting? end end end def listen loop do begin instance = interruptible { Rails.application.executor.run! } with_polling_volume { broadcast_messages } ensure instance.complete! if instance end interruptible { sleep ::SolidCable.polling_interval } end rescue Stop ensure @critical.release end def interruptible @critical.release yield ensure @critical.acquire end def shutdown @critical.acquire # We have the critical permit, and so the listen thread must be # safe to interrupt. thread.raise(Stop) @critical.release thread.join end def add_channel(channel, on_success) channels[channel] = last_message_id event_loop.post(&on_success) if on_success end def remove_channel(channel) channels.delete(channel) end def invoke_callback(*) event_loop.post { super } end private attr_reader :event_loop, :thread attr_accessor :last_id, :reconnect_attempt def last_message_id ::SolidCable::Message.maximum(:id) || 0 end def channels @channels ||= Concurrent::Map.new end def broadcast_messages current_channels = channels.dup ::SolidCable::Message. broadcastable(current_channels.keys, last_id). each do |message| should_broadcast_message = false channels.compute_if_present(message.channel) do |channel_last_id| break if channel_last_id >= message.id should_broadcast_message = true message.id end broadcast(message.channel, message.payload) if should_broadcast_message self.last_id = message.id end end def with_polling_volume if ::SolidCable.silence_polling? && ActiveRecord::Base.logger ActiveRecord::Base.logger.silence { yield } else yield end end def reconnect_attempts @reconnect_attempts ||= ::SolidCable.reconnect_attempts end def retry_connecting? self.reconnect_attempt += 1 return false if reconnect_attempt > reconnect_attempts.size sleep_t = reconnect_attempts[reconnect_attempt - 1] sleep(sleep_t) if sleep_t > 0 true end end end end end ================================================ FILE: lib/generators/solid_cable/install/USAGE ================================================ Description: Installs solid_cable as the Action Cable adapter Example: bin/rails generate solid_cable:install This will perform the following: Installs solid_cable migrations ================================================ FILE: lib/generators/solid_cable/install/install_generator.rb ================================================ # frozen_string_literal: true class SolidCable::InstallGenerator < Rails::Generators::Base source_root File.expand_path("templates", __dir__) def copy_files template "db/cable_schema.rb" template "config/cable.yml", force: true end end ================================================ FILE: lib/generators/solid_cable/install/templates/config/cable.yml ================================================ # Async adapter only works within the same process, so for manually triggering cable updates from a console, # and seeing results in the browser, you must do so from the web console (running inside the dev process), # not a terminal started via bin/rails console! Add "console" to any action or any ERB template view # to make the web console appear. development: adapter: async test: adapter: test production: adapter: solid_cable connects_to: database: writing: cable polling_interval: 0.1.seconds message_retention: 1.day ================================================ FILE: lib/generators/solid_cable/install/templates/db/cable_schema.rb ================================================ ActiveRecord::Schema[7.1].define(version: 1) do create_table "solid_cable_messages", force: :cascade do |t| t.binary "channel", limit: 1024, null: false t.binary "payload", limit: 536870912, null: false t.datetime "created_at", null: false t.integer "channel_hash", limit: 8, null: false t.index ["channel"], name: "index_solid_cable_messages_on_channel" t.index ["channel_hash"], name: "index_solid_cable_messages_on_channel_hash" t.index ["created_at"], name: "index_solid_cable_messages_on_created_at" end end ================================================ FILE: lib/generators/solid_cable/update/USAGE ================================================ Description: Updates Solid Cable migrations Example: bin/rails generate solid_cable:update This will perform the following: Installs new Solid Cable migrations ================================================ FILE: lib/generators/solid_cable/update/templates/db/migrate/create_compact_channel.rb ================================================ # frozen_string_literal: true class CreateCompactChannel < ActiveRecord::Migration[7.2] def up change_column :solid_cable_messages, :channel, :binary, limit: 1024, null: false add_column :solid_cable_messages, :channel_hash, :integer, limit: 8, if_not_exists: true add_index :solid_cable_messages, :channel_hash, if_not_exists: true change_column :solid_cable_messages, :payload, :binary, limit: 536_870_912, null: false SolidCable::Message.find_each do |msg| msg.update(channel_hash: SolidCable::Message.channel_hash_for(msg.channel)) end end def down change_column :solid_cable_messages, :channel, :text remove_column :solid_cable_messages, :channel_hash, if_exists: true change_column :solid_cable_messages, :payload, :text end end ================================================ FILE: lib/generators/solid_cable/update/update_generator.rb ================================================ # frozen_string_literal: true require "rails/generators" require "rails/generators/active_record" class SolidCable::UpdateGenerator < Rails::Generators::Base include ActiveRecord::Generators::Migration source_root File.expand_path("templates", __dir__) def copy_files migration_template "db/migrate/create_compact_channel.rb", "db/cable_migrate/create_compact_channel.rb" end end ================================================ FILE: lib/solid_cable/engine.rb ================================================ # frozen_string_literal: true module SolidCable class Engine < ::Rails::Engine isolate_namespace SolidCable end end ================================================ FILE: lib/solid_cable/version.rb ================================================ # frozen_string_literal: true module SolidCable VERSION = "3.0.12" end ================================================ FILE: lib/solid_cable.rb ================================================ # frozen_string_literal: true require "solid_cable/version" require "solid_cable/engine" require "action_cable/subscription_adapter/solid_cable" module SolidCable class << self def connects_to cable_config.connects_to.to_h.deep_transform_values(&:to_sym) end def silence_polling? cable_config.silence_polling != false end def polling_interval parse_duration(cable_config.polling_interval, default: 0.1.seconds) end def message_retention parse_duration(cable_config.message_retention, default: 1.day) end def autotrim? cable_config.autotrim != false end def trim_batch_size if (size = cable_config.trim_batch_size.to_i) < 2 100 else size end end def use_skip_locked cable_config.use_skip_locked != false end # For every write that we do, we attempt to delete trim_chance times as # many records. This ensures there is downward pressure on the cache size # while there is valid data to delete. Read this as 'every time the trim job # runs theres a trim_multiplier chance this trims'. Adjust number to make it # more or less likely to trim. Only works like this if trim_batch_size is # 100 def trim_chance 2 end def reconnect_attempts attempts = cable_config.fetch(:reconnect_attempts, 1) attempts = Array.new(attempts, 0) if attempts.is_a?(Integer) attempts end private def cable_config Rails.application.config_for("cable") end def parse_duration(duration, default:) if duration.present? *amount, units = duration.to_s.split(".") amount.join(".").to_f.public_send(units) else default end end end end ================================================ FILE: lib/tasks/solid_cable_tasks.rake ================================================ # frozen_string_literal: true desc "Copy over the schema and set cable adapter for Solid Cable" namespace :solid_cable do task :install do Rails::Command.invoke :generate, [ "solid_cable:install" ] end task :update do Rails::Command.invoke :generate, [ "solid_cable:update" ] end end ================================================ FILE: solid_cable.gemspec ================================================ # frozen_string_literal: true require_relative "lib/solid_cable/version" Gem::Specification.new do |spec| spec.name = "solid_cable" spec.version = SolidCable::VERSION spec.authors = [ "Nick Pezza" ] spec.email = [ "pezza@hey.com" ] spec.homepage = "https://github.com/rails/solid_cable" spec.summary = "Database-backed Action Cable backend." spec.description = "Database-backed Action Cable backend." spec.license = "MIT" spec.metadata["homepage_uri"] = spec.homepage spec.metadata["source_code_uri"] = spec.homepage spec.metadata["rubygems_mfa_required"] = "true" spec.files = Dir.chdir(File.expand_path(__dir__)) do Dir["{app,config,db,lib}/**/*", "MIT-LICENSE", "Rakefile", "README.md"] end rails_version = ">= 7.2" spec.required_ruby_version = ">= 3.2.0" spec.add_dependency "activerecord", rails_version spec.add_dependency "activejob", rails_version spec.add_dependency "actioncable", rails_version spec.add_dependency "railties", rails_version spec.add_development_dependency "minitest", "~> 5.0" end ================================================ FILE: test/config_stubs.rb ================================================ # frozen_string_literal: true module ConfigStubs extend ActiveSupport::Concern class ConfigStub def initialize(**) @config = ActiveSupport::OrderedOptions.new. update({ adapter: :test }.merge(**)) end def config_for(_file) @config end def executor @executor ||= ExectorStub.new end class ExectorStub def run! end end end def with_cable_config(**) Rails.stub(:application, ConfigStub.new(**)) { yield } end end ================================================ FILE: test/dummy/Rakefile ================================================ # Add your own tasks in files placed in lib/tasks ending in .rake, # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. require_relative "config/application" Rails.application.load_tasks ================================================ FILE: test/dummy/app/controllers/application_controller.rb ================================================ class ApplicationController < ActionController::Base # Only allow modern browsers supporting webp images, web push, badges, import maps, CSS nesting, and CSS :has. allow_browser versions: :modern end ================================================ FILE: test/dummy/app/controllers/concerns/.keep ================================================ ================================================ FILE: test/dummy/app/helpers/application_helper.rb ================================================ module ApplicationHelper end ================================================ FILE: test/dummy/app/jobs/application_job.rb ================================================ class ApplicationJob < ActiveJob::Base # Automatically retry jobs that encountered a deadlock # retry_on ActiveRecord::Deadlocked # Most jobs are safe to ignore if the underlying records are no longer available # discard_on ActiveJob::DeserializationError end ================================================ FILE: test/dummy/app/models/application_record.rb ================================================ class ApplicationRecord < ActiveRecord::Base primary_abstract_class end ================================================ FILE: test/dummy/app/models/concerns/.keep ================================================ ================================================ FILE: test/dummy/app/views/layouts/application.html.erb ================================================ <%= content_for(:title) || "Dummy" %> <%= csrf_meta_tags %> <%= csp_meta_tag %> <%= yield :head %> <%# Enable PWA manifest for installable apps (make sure to enable in config/routes.rb too!) %> <%#= tag.link rel: "manifest", href: pwa_manifest_path(format: :json) %> <%# Includes all stylesheet files in app/assets/stylesheets %> <%= stylesheet_link_tag :app %> <%= yield %> ================================================ FILE: test/dummy/app/views/layouts/mailer.html.erb ================================================ <%= yield %> ================================================ FILE: test/dummy/app/views/layouts/mailer.text.erb ================================================ <%= yield %> ================================================ FILE: test/dummy/app/views/pwa/manifest.json.erb ================================================ { "name": "Dummy", "icons": [ { "src": "/icon.png", "type": "image/png", "sizes": "512x512" }, { "src": "/icon.png", "type": "image/png", "sizes": "512x512", "purpose": "maskable" } ], "start_url": "/", "display": "standalone", "scope": "/", "description": "Dummy.", "theme_color": "red", "background_color": "red" } ================================================ FILE: test/dummy/app/views/pwa/service-worker.js ================================================ // Add a service worker for processing Web Push notifications: // // self.addEventListener("push", async (event) => { // const { title, options } = await event.data.json() // event.waitUntil(self.registration.showNotification(title, options)) // }) // // self.addEventListener("notificationclick", function(event) { // event.notification.close() // event.waitUntil( // clients.matchAll({ type: "window" }).then((clientList) => { // for (let i = 0; i < clientList.length; i++) { // let client = clientList[i] // let clientPath = (new URL(client.url)).pathname // // if (clientPath == event.notification.data.path && "focus" in client) { // return client.focus() // } // } // // if (clients.openWindow) { // return clients.openWindow(event.notification.data.path) // } // }) // ) // }) ================================================ FILE: test/dummy/bin/ci ================================================ #!/usr/bin/env ruby require_relative "../config/boot" require "active_support/continuous_integration" CI = ActiveSupport::ContinuousIntegration require_relative "../config/ci.rb" ================================================ FILE: test/dummy/bin/dev ================================================ #!/usr/bin/env ruby exec "./bin/rails", "server", *ARGV ================================================ FILE: test/dummy/bin/rails ================================================ #!/usr/bin/env ruby APP_PATH = File.expand_path("../config/application", __dir__) require_relative "../config/boot" require "rails/commands" ================================================ FILE: test/dummy/bin/rake ================================================ #!/usr/bin/env ruby require_relative "../config/boot" require "rake" Rake.application.run ================================================ FILE: test/dummy/bin/setup ================================================ #!/usr/bin/env ruby require "fileutils" APP_ROOT = File.expand_path("..", __dir__) def system!(*args) system(*args, exception: true) end FileUtils.chdir APP_ROOT do # This script is a way to set up or update your development environment automatically. # This script is idempotent, so that you can run it at any time and get an expectable outcome. # Add necessary setup steps to this file. puts "== Installing dependencies ==" system("bundle check") || system!("bundle install") # puts "\n== Copying sample files ==" # unless File.exist?("config/database.yml") # FileUtils.cp "config/database.yml.sample", "config/database.yml" # end puts "\n== Preparing database ==" system! "bin/rails db:prepare" system! "bin/rails db:reset" if ARGV.include?("--reset") puts "\n== Removing old logs and tempfiles ==" system! "bin/rails log:clear tmp:clear" unless ARGV.include?("--skip-server") puts "\n== Starting development server ==" STDOUT.flush # flush the output before exec(2) so that it displays exec "bin/dev" end end ================================================ FILE: test/dummy/config/application.rb ================================================ require_relative "boot" require "rails/all" # Require the gems listed in Gemfile, including any gems # you've limited to :test, :development, or :production. Bundler.require(*Rails.groups) module Dummy class Application < Rails::Application config.load_defaults Rails::VERSION::STRING.to_f # For compatibility with applications that use this config config.action_controller.include_all_helpers = false # Please, add to the `ignore` list any other `lib` subdirectories that do # not contain `.rb` files, or that should not be reloaded or eager loaded. # Common ones are `templates`, `generators`, or `middleware`, for example. config.autoload_lib(ignore: %w[assets tasks]) # Configuration for the application, engines, and railties goes here. # # These settings can be overridden in specific environments using the files # in config/environments, which are processed later. # # config.time_zone = "Central Time (US & Canada)" # config.eager_load_paths << Rails.root.join("extras") end end ================================================ FILE: test/dummy/config/boot.rb ================================================ # Set up gems listed in the Gemfile. ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../../Gemfile", __dir__) require "bundler/setup" if File.exist?(ENV["BUNDLE_GEMFILE"]) $LOAD_PATH.unshift File.expand_path("../../../lib", __dir__) ================================================ FILE: test/dummy/config/cable.yml ================================================ development: adapter: async test: adapter: test production: adapter: redis url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %> channel_prefix: dummy_production ================================================ FILE: test/dummy/config/ci.rb ================================================ # Run using bin/ci CI.run do step "Setup", "bin/setup --skip-server" step "Tests: Rails", "bin/rails test" step "Tests: System", "bin/rails test:system" step "Tests: Seeds", "env RAILS_ENV=test bin/rails db:seed:replant" # Optional: set a green GitHub commit status to unblock PR merge. # Requires the `gh` CLI and `gh extension install basecamp/gh-signoff`. # if success? # step "Signoff: All systems go. Ready for merge and deploy.", "gh signoff" # else # failure "Signoff: CI failed. Do not merge or deploy.", "Fix the issues and try again." # end end ================================================ FILE: test/dummy/config/database.yml ================================================ <% def database_name_from(name); ["mysql", "postgres"].exclude?(ENV["TARGET_DB"]) ? "db/#{name}.sqlite3" : name; end %> <% if ENV["TARGET_DB"] == "mysql" %> default: &default adapter: trilogy username: root pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> host: "127.0.0.1" port: 33060 <% elsif ENV["TARGET_DB"] == "postgres" %> default: &default adapter: postgresql encoding: unicode username: postgres pool: 5 host: "127.0.0.1" port: 55432 gssencmode: disable # https://github.com/ged/ruby-pg/issues/311 <% else %> default: &default adapter: sqlite3 pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 50 } %> timeout: 100 <% end %> development: <<: *default database: <%= database_name_from("solid_cable_development") %> test: <<: *default pool: 20 database: <%= database_name_from("solid_cable_test") %> ================================================ FILE: test/dummy/config/environment.rb ================================================ # Load the Rails application. require_relative "application" # Initialize the Rails application. Rails.application.initialize! ================================================ FILE: test/dummy/config/environments/development.rb ================================================ require "active_support/core_ext/integer/time" Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. # Make code changes take effect immediately without server restart. config.enable_reloading = true # Do not eager load code on boot. config.eager_load = false # Show full error reports. config.consider_all_requests_local = true # Enable server timing. config.server_timing = true # Enable/disable Action Controller caching. By default Action Controller caching is disabled. # Run rails dev:cache to toggle Action Controller caching. if Rails.root.join("tmp/caching-dev.txt").exist? config.action_controller.perform_caching = true config.action_controller.enable_fragment_cache_logging = true config.public_file_server.headers = { "cache-control" => "public, max-age=#{2.days.to_i}" } else config.action_controller.perform_caching = false end # Change to :null_store to avoid any caching. config.cache_store = :memory_store # Store uploaded files on the local file system (see config/storage.yml for options). # config.active_storage.service = :local # Don't care if the mailer can't send. # config.action_mailer.raise_delivery_errors = false # Make template changes take effect immediately. # config.action_mailer.perform_caching = false # Set localhost to be used by links generated in mailer templates. # config.action_mailer.default_url_options = { host: "localhost", port: 3000 } # Print deprecation notices to the Rails logger. config.active_support.deprecation = :log # Raise an error on page load if there are pending migrations. config.active_record.migration_error = :page_load # Highlight code that triggered database queries in logs. config.active_record.verbose_query_logs = true # Append comments with runtime information tags to SQL queries in logs. config.active_record.query_log_tags_enabled = true # Highlight code that enqueued background job in logs. config.active_job.verbose_enqueue_logs = true # Highlight code that triggered redirect in logs. config.action_dispatch.verbose_redirect_logs = true # Suppress logger output for asset requests. # config.assets.quiet = true # Raises error for missing translations. # config.i18n.raise_on_missing_translations = true # Annotate rendered view with file names. config.action_view.annotate_rendered_view_with_filenames = true # Uncomment if you wish to allow Action Cable access from any origin. # config.action_cable.disable_request_forgery_protection = true # Raise error when a before_action's only/except options reference missing actions. config.action_controller.raise_on_missing_callback_actions = true end ================================================ FILE: test/dummy/config/environments/production.rb ================================================ require "active_support/core_ext/integer/time" Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. # Code is not reloaded between requests. config.enable_reloading = false # Eager load code on boot for better performance and memory savings (ignored by Rake tasks). config.eager_load = true # Full error reports are disabled. config.consider_all_requests_local = false # Turn on fragment caching in view templates. config.action_controller.perform_caching = true # Cache assets for far-future expiry since they are all digest stamped. config.public_file_server.headers = { "cache-control" => "public, max-age=#{1.year.to_i}" } # Enable serving of images, stylesheets, and JavaScripts from an asset server. # config.asset_host = "http://assets.example.com" # Store uploaded files on the local file system (see config/storage.yml for options). # config.active_storage.service = :local # Assume all access to the app is happening through a SSL-terminating reverse proxy. config.assume_ssl = true # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. config.force_ssl = true # Skip http-to-https redirect for the default health check endpoint. # config.ssl_options = { redirect: { exclude: ->(request) { request.path == "/up" } } } # Log to STDOUT with the current request id as a default log tag. config.log_tags = [ :request_id ] config.logger = ActiveSupport::TaggedLogging.logger(STDOUT) # Change to "debug" to log everything (including potentially personally-identifiable information!). config.log_level = ENV.fetch("RAILS_LOG_LEVEL", "info") # Prevent health checks from clogging up the logs. config.silence_healthcheck_path = "/up" # Don't log any deprecations. config.active_support.report_deprecations = false # Replace the default in-process memory cache store with a durable alternative. # config.cache_store = :mem_cache_store # Replace the default in-process and non-durable queuing backend for Active Job. # config.active_job.queue_adapter = :resque # Ignore bad email addresses and do not raise email delivery errors. # Set this to true and configure the email server for immediate delivery to raise delivery errors. # config.action_mailer.raise_delivery_errors = false # Set host to be used by links generated in mailer templates. config.action_mailer.default_url_options = { host: "example.com" } # Specify outgoing SMTP server. Remember to add smtp/* credentials via bin/rails credentials:edit. # config.action_mailer.smtp_settings = { # user_name: Rails.application.credentials.dig(:smtp, :user_name), # password: Rails.application.credentials.dig(:smtp, :password), # address: "smtp.example.com", # port: 587, # authentication: :plain # } # Enable locale fallbacks for I18n (makes lookups for any locale fall back to # the I18n.default_locale when a translation cannot be found). config.i18n.fallbacks = true # Do not dump schema after migrations. config.active_record.dump_schema_after_migration = false # Only use :id for inspections in production. config.active_record.attributes_for_inspect = [ :id ] # Enable DNS rebinding protection and other `Host` header attacks. # config.hosts = [ # "example.com", # Allow requests from example.com # /.*\.example\.com/ # Allow requests from subdomains like `www.example.com` # ] # # Skip DNS rebinding protection for the default health check endpoint. # config.host_authorization = { exclude: ->(request) { request.path == "/up" } } end ================================================ FILE: test/dummy/config/environments/test.rb ================================================ # The test environment is used exclusively to run your application's # test suite. You never need to work with it otherwise. Remember that # your test database is "scratch space" for the test suite and is wiped # and recreated between test runs. Don't rely on the data there! Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. # While tests run files are not watched, reloading is not necessary. config.enable_reloading = false # Eager loading loads your entire application. When running a single test locally, # this is usually not necessary, and can slow down your test suite. However, it's # recommended that you enable it in continuous integration systems to ensure eager # loading is working properly before deploying your code. config.eager_load = ENV["CI"].present? # Configure public file server for tests with cache-control for performance. config.public_file_server.headers = { "cache-control" => "public, max-age=3600" } # Show full error reports. config.consider_all_requests_local = true config.cache_store = :null_store # Render exception templates for rescuable exceptions and raise for other exceptions. config.action_dispatch.show_exceptions = :rescuable # Disable request forgery protection in test environment. config.action_controller.allow_forgery_protection = false # Store uploaded files on the local file system in a temporary directory. # config.active_storage.service = :test # Tell Action Mailer not to deliver emails to the real world. # The :test delivery method accumulates sent emails in the # ActionMailer::Base.deliveries array. # config.action_mailer.delivery_method = :test # Set host to be used by links generated in mailer templates. # config.action_mailer.default_url_options = { host: "example.com" } # Print deprecation notices to the stderr. config.active_support.deprecation = :stderr # Raises error for missing translations. # config.i18n.raise_on_missing_translations = true # Annotate rendered view with file names. # config.action_view.annotate_rendered_view_with_filenames = true # Raise error when a before_action's only/except options reference missing actions. config.action_controller.raise_on_missing_callback_actions = true end ================================================ FILE: test/dummy/config/initializers/assets.rb ================================================ # Be sure to restart your server when you modify this file. # Version of your assets, change this if you want to expire all your assets. # Rails.application.config.assets.version = "1.0" # Add additional assets to the asset load path. # Rails.application.config.assets.paths << Emoji.images_path ================================================ FILE: test/dummy/config/initializers/content_security_policy.rb ================================================ # Be sure to restart your server when you modify this file. # Define an application-wide content security policy. # See the Securing Rails Applications Guide for more information: # https://guides.rubyonrails.org/security.html#content-security-policy-header # Rails.application.configure do # config.content_security_policy do |policy| # policy.default_src :self, :https # policy.font_src :self, :https, :data # policy.img_src :self, :https, :data # policy.object_src :none # policy.script_src :self, :https # policy.style_src :self, :https # # Specify URI for violation reports # # policy.report_uri "/csp-violation-report-endpoint" # end # # # Generate session nonces for permitted importmap, inline scripts, and inline styles. # config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s } # config.content_security_policy_nonce_directives = %w(script-src style-src) # # # Automatically add `nonce` to `javascript_tag`, `javascript_include_tag`, and `stylesheet_link_tag` # # if the corresponding directives are specified in `content_security_policy_nonce_directives`. # # config.content_security_policy_nonce_auto = true # # # Report violations without enforcing the policy. # # config.content_security_policy_report_only = true # end ================================================ FILE: test/dummy/config/initializers/filter_parameter_logging.rb ================================================ # Be sure to restart your server when you modify this file. # Configure parameters to be partially matched (e.g. passw matches password) and filtered from the log file. # Use this to limit dissemination of sensitive information. # See the ActiveSupport::ParameterFilter documentation for supported notations and behaviors. Rails.application.config.filter_parameters += [ :passw, :email, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn, :cvv, :cvc ] ================================================ FILE: test/dummy/config/initializers/inflections.rb ================================================ # Be sure to restart your server when you modify this file. # Add new inflection rules using the following format. Inflections # are locale specific, and you may define rules for as many different # locales as you wish. All of these examples are active by default: # ActiveSupport::Inflector.inflections(:en) do |inflect| # inflect.plural /^(ox)$/i, "\\1en" # inflect.singular /^(ox)en/i, "\\1" # inflect.irregular "person", "people" # inflect.uncountable %w( fish sheep ) # end # These inflection rules are supported but not enabled by default: # ActiveSupport::Inflector.inflections(:en) do |inflect| # inflect.acronym "RESTful" # end ================================================ FILE: test/dummy/config/locales/en.yml ================================================ # Files in the config/locales directory are used for internationalization and # are automatically loaded by Rails. If you want to use locales other than # English, add the necessary files in this directory. # # To use the locales, use `I18n.t`: # # I18n.t "hello" # # In views, this is aliased to just `t`: # # <%= t("hello") %> # # To use a different locale, set it with `I18n.locale`: # # I18n.locale = :es # # This would use the information in config/locales/es.yml. # # To learn more about the API, please read the Rails Internationalization guide # at https://guides.rubyonrails.org/i18n.html. # # Be aware that YAML interprets the following case-insensitive strings as # booleans: `true`, `false`, `on`, `off`, `yes`, `no`. Therefore, these strings # must be quoted to be interpreted as strings. For example: # # en: # "yes": yup # enabled: "ON" en: hello: "Hello world" ================================================ FILE: test/dummy/config/puma.rb ================================================ # This configuration file will be evaluated by Puma. The top-level methods that # are invoked here are part of Puma's configuration DSL. For more information # about methods provided by the DSL, see https://puma.io/puma/Puma/DSL.html. # # Puma starts a configurable number of processes (workers) and each process # serves each request in a thread from an internal thread pool. # # You can control the number of workers using ENV["WEB_CONCURRENCY"]. You # should only set this value when you want to run 2 or more workers. The # default is already 1. You can set it to `auto` to automatically start a worker # for each available processor. # # The ideal number of threads per worker depends both on how much time the # application spends waiting for IO operations and on how much you wish to # prioritize throughput over latency. # # As a rule of thumb, increasing the number of threads will increase how much # traffic a given process can handle (throughput), but due to CRuby's # Global VM Lock (GVL) it has diminishing returns and will degrade the # response time (latency) of the application. # # The default is set to 3 threads as it's deemed a decent compromise between # throughput and latency for the average Rails application. # # Any libraries that use a connection pool or another resource pool should # be configured to provide at least as many connections as the number of # threads. This includes Active Record's `pool` parameter in `database.yml`. threads_count = ENV.fetch("RAILS_MAX_THREADS", 3) threads threads_count, threads_count # Specifies the `port` that Puma will listen on to receive requests; default is 3000. port ENV.fetch("PORT", 3000) # Allow puma to be restarted by `bin/rails restart` command. plugin :tmp_restart # Specify the PID file. Defaults to tmp/pids/server.pid in development. # In other environments, only set the PID file if requested. pidfile ENV["PIDFILE"] if ENV["PIDFILE"] ================================================ FILE: test/dummy/config/routes.rb ================================================ # frozen_string_literal: true Rails.application.routes.draw do # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html # Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500. # Can be used by load balancers and uptime monitors to verify that the app is live. get "up" => "rails/health#show", as: :rails_health_check # Defines the root path route ("/") # root "posts#index" end ================================================ FILE: test/dummy/config/storage.yml ================================================ test: service: Disk root: <%= Rails.root.join("tmp/storage") %> local: service: Disk root: <%= Rails.root.join("storage") %> # Use bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key) # amazon: # service: S3 # access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %> # secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %> # region: us-east-1 # bucket: your_own_bucket-<%= Rails.env %> # Remember not to checkin your GCS keyfile to a repository # google: # service: GCS # project: your_project # credentials: <%= Rails.root.join("path/to/gcs.keyfile") %> # bucket: your_own_bucket-<%= Rails.env %> # mirror: # service: Mirror # primary: local # mirrors: [ amazon, google, microsoft ] ================================================ FILE: test/dummy/config.ru ================================================ # This file is used by Rack-based servers to start the application. require_relative "config/environment" run Rails.application Rails.application.load_server ================================================ FILE: test/dummy/db/schema.rb ================================================ # This file is auto-generated from the current state of the database. Instead # of editing this file, please use the migrations feature of Active Record to # incrementally modify your database, and then regenerate this schema definition. # # This file is the source Rails uses to define your schema when running `bin/rails # db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to # be faster and is potentially less error prone than running all of your # migrations from scratch. Old migrations may fail to apply correctly if those # migrations use external dependencies or application code. # # It's strongly recommended that you check this file into your version control system. ActiveRecord::Schema[7.2].define(version: 2024_09_12_130854) do create_table "solid_cable_messages", force: :cascade do |t| t.binary "channel", limit: 1024, null: false t.binary "payload", limit: 536870912, null: false t.datetime "created_at", null: false t.integer "channel_hash", limit: 8, null: false t.index ["channel"], name: "index_solid_cable_messages_on_channel" t.index ["channel_hash"], name: "index_solid_cable_messages_on_channel_hash" t.index ["created_at"], name: "index_solid_cable_messages_on_created_at" end end ================================================ FILE: test/dummy/log/.keep ================================================ ================================================ FILE: test/dummy/public/400.html ================================================ The server cannot process the request due to a client error (400 Bad Request)

The server cannot process the request due to a client error. Please check the request and try again. If you're the application owner check the logs for more information.

================================================ FILE: test/dummy/public/404.html ================================================ The page you were looking for doesn't exist (404 Not found)

The page you were looking for doesn't exist. You may have mistyped the address or the page may have moved. If you're the application owner check the logs for more information.

================================================ FILE: test/dummy/public/406-unsupported-browser.html ================================================ Your browser is not supported (406 Not Acceptable)

Your browser is not supported.
Please upgrade your browser to continue.

================================================ FILE: test/dummy/public/422.html ================================================ The change you wanted was rejected (422 Unprocessable Entity)

The change you wanted was rejected. Maybe you tried to change something you didn't have access to. If you're the application owner check the logs for more information.

================================================ FILE: test/dummy/public/500.html ================================================ We're sorry, but something went wrong (500 Internal Server Error)

We're sorry, but something went wrong.
If you're the application owner check the logs for more information.

================================================ FILE: test/jobs/trim_job_test.rb ================================================ # frozen_string_literal: true require "test_helper" require "config_stubs" class TrimJobTest < ActiveJob::TestCase include ConfigStubs test "trims a limited number of messages" do SolidCable.stub(:trim_chance, 99.999) do with_cable_config trim_batch_size: 2, message_rention: "1.second" do 4.times do SolidCable::Message.broadcast("foo", "bar") SolidCable::Message.update_all(created_at: 2.days.ago) end assert_difference -> { SolidCable::Message.count }, -2 do SolidCable::TrimJob.perform_now end end end end test "trims when out of band with autotrim disabled" do SolidCable.stub(:trim_chance, 0) do with_cable_config autotrim: false, trim_batch_size: 2, message_rention: "1.second" do 4.times do SolidCable::Message.broadcast("foo", "bar") SolidCable::Message.update_all(created_at: 2.days.ago) end assert_difference -> { SolidCable::Message.count }, -2 do SolidCable::TrimJob.perform_now end end end end end ================================================ FILE: test/lib/action_cable/subscription_adapter/solid_cable_test.rb ================================================ # frozen_string_literal: true require "test_helper" require "concurrent" require "active_support/core_ext/hash/indifferent_access" require "pathname" require "config_stubs" class ActionCable::SubscriptionAdapter::SolidCableTest < ActionCable::TestCase include ConfigStubs WAIT_WHEN_EXPECTING_EVENT = 1 WAIT_WHEN_NOT_EXPECTING_EVENT = 0.2 setup do server = ActionCable::Server::Base.new server.config.cable = cable_config.with_indifferent_access server.config.logger = Logger.new(StringIO.new).tap do |l| l.level = Logger::UNKNOWN end adapter_klass = server.config.pubsub_adapter @rx_adapter = adapter_klass.new(server) @tx_adapter = adapter_klass.new(server) @tx_adapter.shutdown @tx_adapter = @rx_adapter end teardown do [@rx_adapter, @tx_adapter].uniq.compact.each(&:shutdown) end test "subscribe_and_unsubscribe" do subscribe_as_queue("channel") do |queue| end end test "basic_broadcast" do subscribe_as_queue("channel") do |queue| @tx_adapter.broadcast("channel", "hello world") assert_equal "hello world", next_message_in_queue(queue) end end test "broadcast_after_unsubscribe" do keep_queue = nil subscribe_as_queue("channel") do |queue| keep_queue = queue @tx_adapter.broadcast("channel", "hello world") assert_equal "hello world", next_message_in_queue(queue) end @tx_adapter.broadcast("channel", "hello void") sleep WAIT_WHEN_NOT_EXPECTING_EVENT assert_empty keep_queue end test "trims_after_unsubscribe" do SolidCable.stub(:trim_chance, 99.999999) do with_cable_config message_retention: "2.seconds", trim_batch_size: 2 do subscribe_as_queue("channel") do |queue| 4.times do @tx_adapter.broadcast("channel", "hello world") sleep 3 end queue.clear end assert_equal 1, SolidCable::Message.where(channel: "channel").count end end end test "multiple_broadcast" do subscribe_as_queue("channel") do |queue| @tx_adapter.broadcast("channel", "bananas") @tx_adapter.broadcast("channel", "apples") received = [] 2.times { received << next_message_in_queue(queue) } assert_equal %w(apples bananas), received.sort end end test "identical_subscriptions" do subscribe_as_queue("channel") do |queue| subscribe_as_queue("channel") do |queue_2| @tx_adapter.broadcast("channel", "hello") assert_equal "hello", next_message_in_queue(queue_2) end assert_equal "hello", next_message_in_queue(queue) end end test "simultaneous_subscriptions" do subscribe_as_queue("channel") do |queue| subscribe_as_queue("other channel") do |queue_2| @tx_adapter.broadcast("channel", "apples") @tx_adapter.broadcast("other channel", "oranges") assert_equal "apples", next_message_in_queue(queue) assert_equal "oranges", next_message_in_queue(queue_2) end end end test "channel_filtered_broadcast" do subscribe_as_queue("channel") do |queue| @tx_adapter.broadcast("other channel", "one") @tx_adapter.broadcast("channel", "two") assert_equal "two", next_message_in_queue(queue) end end test "long_identifiers" do channel_1 = "#{'a' * 100}1" channel_2 = "#{'a' * 100}2" subscribe_as_queue(channel_1) do |queue| subscribe_as_queue(channel_2) do |queue_2| @tx_adapter.broadcast(channel_1, "apples") @tx_adapter.broadcast(channel_2, "oranges") assert_equal "apples", next_message_in_queue(queue) assert_equal "oranges", next_message_in_queue(queue_2) end end end test "does not raise error when polling with no Active Record logger" do with_active_record_logger(nil) do assert_nothing_raised do subscribe_as_queue("channel") do |queue| @tx_adapter.broadcast("channel", "hello world") assert_equal "hello world", next_message_in_queue(queue) end end end end test "does not send old messages" do @tx_adapter.broadcast("channel", "channel1") @tx_adapter.broadcast("channel", "channel2") subscribe_as_queue("channel") do |queue| assert_empty queue @tx_adapter.broadcast("channel", "channel3") @tx_adapter.broadcast("channel", "channel4") @tx_adapter.broadcast("other", "other1") @tx_adapter.broadcast("other", "other2") subscribe_as_queue("other") do |other_queue| assert_empty other_queue end assert_equal "channel3", next_message_in_queue(queue) assert_equal "channel4", next_message_in_queue(queue) end @tx_adapter.broadcast("channel", "channel5") @tx_adapter.broadcast("channel", "channel6") subscribe_as_queue("channel") do |queue| assert_empty queue end end test "retries after a connection failure and keeps listening" do with_cable_config reconnect_attempts: [0] do raised = false original = SolidCable::Message.method(:broadcastable) SolidCable::Message.stub(:broadcastable, lambda { |channels, last_id| if raised original.call(channels, last_id) else raised = true raise ActiveRecord::ConnectionFailed, "boom" end }) do subscribe_as_queue("reconnect-channel") do |queue| @tx_adapter.broadcast("reconnect-channel", "hello") assert_equal "hello", next_message_in_queue(queue) end end assert raised end end private def cable_config { adapter: "solid_cable", message_retention: "1.second", polling_interval: "0.01.seconds" } end def subscribe_as_queue(channel, adapter = @rx_adapter) queue = Queue.new callback = ->(data) { queue << data } subscribed = Concurrent::Event.new adapter.subscribe(channel, callback, proc { subscribed.set }) subscribed.wait(WAIT_WHEN_EXPECTING_EVENT) assert_predicate subscribed, :set? yield queue sleep WAIT_WHEN_NOT_EXPECTING_EVENT assert_empty queue ensure adapter.unsubscribe(channel, callback) if subscribed.set? end def with_active_record_logger(logger) old_logger = ActiveRecord::Base.logger ActiveRecord::Base.logger = logger yield ensure ActiveRecord::Base.logger = old_logger end def next_message_in_queue(queue) Timeout.timeout(5, nil, "Failed to get next item in queue") { queue.pop } end end ================================================ FILE: test/lib/generators/solid_cable/install/install_generator_test.rb ================================================ require "test_helper" require_relative "../../../../../lib/generators/solid_cable/install/install_generator" class SolidCable::InstallGeneratorTest < Rails::Generators::TestCase tests SolidCable::InstallGenerator destination File.expand_path("../../../../../tmp", __dir__) setup :prepare_destination setup :run_generator test "cable_schema exists" do assert_file "db/cable_schema.rb" end test "cable.yml exists" do assert_file "config/cable.yml" end end ================================================ FILE: test/lib/generators/solid_cable/update/update_generator_test.rb ================================================ require "test_helper" require_relative "../../../../../lib/generators/solid_cable/update/update_generator" class SolidCable::UpdateGeneratorTest < Rails::Generators::TestCase tests SolidCable::UpdateGenerator destination File.expand_path("../../../../../tmp", __dir__) setup :prepare_destination setup :run_generator test "cable_schema exists" do assert_migration "db/cable_migrate/create_compact_channel.rb" end end ================================================ FILE: test/solid_cable_test.rb ================================================ # frozen_string_literal: true require "test_helper" require "config_stubs" class SolidCableTest < ActiveSupport::TestCase include ConfigStubs test "it has a version number" do assert SolidCable::VERSION end test "autotrimming when nothing is set" do assert_not Rails.application.config_for("cable").key?(:autotrim) assert SolidCable.autotrim? end test "autotrimming when set to false" do with_cable_config autotrim: false do assert_not SolidCable.autotrim? end end test "autotrimming when set to true" do with_cable_config autotrim: true do assert SolidCable.autotrim? end end test "default trim_batch_size is 100" do assert_equal 100, SolidCable.trim_batch_size end test "trim_batch_size when set badly" do with_cable_config trim_batch_size: "weird" do assert_equal 100, SolidCable.trim_batch_size end with_cable_config trim_batch_size: "0" do assert_equal 100, SolidCable.trim_batch_size end end test "trim_batch_size when set" do with_cable_config trim_batch_size: 42 do assert_equal 42, SolidCable.trim_batch_size end end test "reconnect_attempts defaults to a single zero" do assert_equal [ 0 ], SolidCable.reconnect_attempts end test "reconnect_attempts accepts an integer" do with_cable_config reconnect_attempts: 3 do assert_equal [ 0, 0, 0 ], SolidCable.reconnect_attempts end end test "reconnect_attempts accepts an array" do with_cable_config reconnect_attempts: [ 0, 1, 2 ] do assert_equal [ 0, 1, 2 ], SolidCable.reconnect_attempts end end end ================================================ FILE: test/test_helper.rb ================================================ # frozen_string_literal: true # Configure Rails Environment ENV["RAILS_ENV"] = "test" require_relative "../test/dummy/config/environment" ActiveRecord::Migrator.migrations_paths = [ File.expand_path( "../test/dummy/db/migrate", __dir__ ) ] require "rails/test_help" require "minitest/autorun" # Load fixtures from the engine if ActiveSupport::TestCase.respond_to?(:fixture_paths=) ActiveSupport::TestCase.fixture_paths = [ File.expand_path("fixtures", __dir__) ] ActionDispatch::IntegrationTest.fixture_paths = ActiveSupport::TestCase.fixture_paths ActiveSupport::TestCase.file_fixture_path = "#{File.expand_path('fixtures', __dir__)}/files" ActiveSupport::TestCase.fixtures :all end