├── LICENSE.txt ├── README.md ├── debian └── dot-test-host-resolution.md ├── firefox └── dark-mode.md ├── kamal └── assets.md ├── rails ├── active-record-connection-pool.md ├── column-defaults.md ├── deleting-files-after-sending.md ├── enqueue-after-transaction-commit.md ├── gems-i-use.md ├── headless-system-tests.md ├── postgres-backed-enums.md ├── recyclable-cache-keys.md ├── rspec-rails-types.md └── transactional-tests.md └── ruby ├── condition-variables.md ├── exception-keyword-arguments.md ├── forwardable.md ├── gdbm.md ├── pass-by.md └── prepending-modules.md /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Tobias Bühlmann 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Today I Learned 2 | 3 | 19 TILs and counting… 4 | 5 | ### Debian 6 | 7 | - [\*.test Host Resolution](debian/dot-test-host-resolution.md) 8 | 9 | ### Firefox 10 | 11 | - [Dark Mode](firefox/dark-mode.md) 12 | 13 | ### Rails 14 | 15 | - [Active Record Connection Pool](rails/active-record-connection-pool.md) 16 | - [Column Defaults](rails/column-defaults.md) 17 | - [Enqueue after Transaction Commit](rails/enqueue-after-transaction-commit.md) 18 | - [Gems I Use](rails/gems-i-use.md) 19 | - [Headless System Tests](rails/headless-system-tests.md) 20 | - [Postgres-Backed Enums](rails/postgres-backed-enums.md) 21 | - [Recyclable Cache Keys](rails/recyclable-cache-keys.md) 22 | - [RSpec::Rails Types](rails/rspec-rails-types.md) 23 | - [Deleting Files after Sending](rails/deleting-files-after-sending.md) 24 | - [Transactional Tests](rails/transactional-tests.md) 25 | 26 | ### Ruby 27 | 28 | - [Condition Variables](ruby/condition-variables.md) 29 | - [Exception Keyword Arguments](ruby/exception-keyword-arguments.md) 30 | - [Forwardable](ruby/forwardable.md) 31 | - [GDBM](ruby/gdbm.md) 32 | - [Pass By](ruby/pass-by.md) 33 | - [Prepending Modules](ruby/prepending-modules.md) 34 | 35 | ### Kamal 36 | 37 | - [Assets](kamal/assets.md) 38 | 39 | ### About 40 | 41 | I shamelessly stole this idea from [jbranchaud/til](https://github.com/jbranchaud/til) who shamelessly stole this idea from [thoughtbot/til](https://github.com/thoughtbot/til). 42 | 43 | ### License 44 | 45 | The repository is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). 46 | -------------------------------------------------------------------------------- /debian/dot-test-host-resolution.md: -------------------------------------------------------------------------------- 1 | # \*.test Host Resolution 2 | 3 | When using Debian, foo.localhost resolves to localhost. There are several options for adding other hosts to do the same, and one is using NetworkManager. 4 | 5 | First, add a /etc/NetworkManager/dnsmasq.d/00-test.conf file with the following content: 6 | 7 | ``` 8 | address=/test/127.0.0.1 9 | ``` 10 | 11 | Then, add a `dns=dnsmasq` line to the `[main]` block in /etc/NetworkManager/NetworkManager.conf: 12 | 13 | ``` 14 | # Before 15 | [main] 16 | plugins=ifupdown,keyfile 17 | 18 | # After 19 | [main] 20 | plugins=ifupdown,keyfile 21 | dns=dnsmasq 22 | ``` 23 | 24 | After that, reload the NetworkManager using `sudo systemctl reload NetworkManager` and foo.test will resolve to localhost. 25 | -------------------------------------------------------------------------------- /firefox/dark-mode.md: -------------------------------------------------------------------------------- 1 | # Dark Mode 2 | 3 | Firefox can be setup with a dark theme: 4 | 5 | ``` 6 | Add-Ons --> Themes --> Enabled "Dark" 7 | ``` 8 | 9 | However, this will only change the appearance of Firefox elements like the address bar and tabs. It doesn't tell websites that a dark mode is preferred. 10 | 11 | In order to do so, open `about:config` in the browser address bar and add an integer option named `ui.systemUsesDarkTheme` with the value `1`. With that, CSS knows about the preference and can display things appropriately. 12 | 13 | ## CSS 14 | 15 | Using CSS, we can use the browser's dark mode preference like this: 16 | 17 | ```css 18 | @media (prefers-color-scheme: dark) { 19 | body { 20 | background-color: black; 21 | } 22 | } 23 | ``` 24 | -------------------------------------------------------------------------------- /kamal/assets.md: -------------------------------------------------------------------------------- 1 | # Assets 2 | 3 | When deploying with Kamal, it will first start the new version of the application before stopping the old version. That means that there is a short period of time where both versions are running. That's why it's possible that the old version gets a request meant for the new version and vice versa, resulting in 404s. 4 | 5 | For assets, Kamal offers a feature that'll avoid getting such 404s while deploying. It basically copies new assets to the old version of the application and it copies old assets to the new version of the application. This works by using the `asset_path` config option, like: 6 | 7 | ```yml 8 | asset_path: /rails/public/assets 9 | ``` 10 | 11 | The deploy process looks like this: 12 | 13 | ### 1. [Extract Assets](https://github.com/basecamp/kamal/blob/v1.1.0/lib/kamal/commands/app/assets.rb#L2-L12) 14 | 15 | Before starting the new version of the application, run a container that sleeps and copy the assets out of that into a directory on the host: 16 | 17 | ``` 18 | $ docker run --name foo-web-production-assets --detach --rm :latest sleep 1000000 19 | $ docker cp -L foo-web-production-assets:/rails/public/assets/. .kamal/assets/extracted/foo-web-production- 20 | $ docker stop -t 1 foo-web-production-assets 21 | ``` 22 | 23 | ### 2. [Sync Asset Volumes](https://github.com/basecamp/kamal/blob/v1.1.0/lib/kamal/commands/app/assets.rb#L14-L28) 24 | 25 | After extracting assets, they will be copied into a volume directory on the host: 26 | 27 | ``` 28 | $ mkdir -p .kamal/assets/volumes/foo-web-production- 29 | $ cp -rnT .kamal/assets/extracted/foo-web-production- .kamal/assets/volumes/foo-web-production- 30 | ``` 31 | 32 | … and, if there's an old version of the application running on the host, add the new assets to the old version of the application and the old assets to the new version of the application: 33 | 34 | ``` 35 | $ cp -rnT .kamal/assets/extracted/foo-web-production- .kamal/assets/volumes/foo-web-production- || true 36 | $ cp -rnT .kamal/assets/extracted/foo-web-production- .kamal/assets/volumes/foo-web-production- || true 37 | ``` 38 | 39 | ### 3. Run new Application 40 | 41 | The new version of the application will be started and the volume directory will be mounted: 42 | 43 | ``` 44 | $ docker run --detach --name foo-web-production- --volume $(pwd)/.kamal/assets/volumes/foo-web-production-:/rails/public/assets : 45 | ``` 46 | 47 | By doing so, the application's asset directory is kind of discarded and the host's volume directory is used instead. 48 | 49 | ### 4. Stop old Application 50 | 51 | If there's an old version of the application running on the host, stop it: 52 | 53 | ``` 54 | $ docker container ls --all --filter name=^foo-web-production-$ --quiet | xargs docker stop 55 | ``` 56 | 57 | ### 5. [Clean Up Assets](https://github.com/basecamp/kamal/blob/v1.1.0/lib/kamal/commands/app/assets.rb#L30-L34) 58 | 59 | If there was an old version of the application running on the host, clean up any assets from any old version. It only keeps the new extracted assets and the new volume directory: 60 | 61 | ``` 62 | $ find .kamal/assets/extracted -maxdepth 1 -name 'foo-web-production-*' ! -name foo-web-production- -exec rm -rf "{}" + 63 | $ find .kamal/assets/volumes -maxdepth 1 -name 'foo-web-production-*' ! -name foo-web-production- -exec rm -rf "{}" + 64 | ``` 65 | -------------------------------------------------------------------------------- /rails/active-record-connection-pool.md: -------------------------------------------------------------------------------- 1 | # Active Record Connection Pool 2 | 3 | ``` 4 | ActiveRecord::ConnectionTimeoutError: could not obtain a connection from the pool within 5.000 seconds (waited 5.002 seconds); all pooled connections were in use 5 | ``` 6 | 7 | Encountering this? Seems like your Puma/Sidekiq or Rails configuration isn't quite right. This post is about correctly configuring them. There's also a [TL;DR](#tldr). 8 | 9 | ## The Connection Pool 10 | 11 | In order to understand why the error above is happening, you need to understand Active Record's connection pool. The connection pool is a pool of database connections. The connection pool's maximum number of connections (or size) is defined by the `pool` setting of your config/database.yml file. The default is 5, which means there won't be more than 5 connections in the pool. The connections are lazily created, which means the pool starts with 0 connections and only ever creates one when needed. When a connection is idling in the pool for 300 seconds, it will be disconnected and removed. 12 | 13 | Whenever Active Record is used and the database is to be queried, Rails will checkout a connection from the pool and use it. The connection will only be returned back to the pool if the thread that is using the connection dies or the thread returns it manually. This is a per-thread behaviour, so each thread will checkout a single connection. And that's basically the problem we've seen at the top: When all connections from the pool are in use and a thread tries to checkout a connection, it tries so for 5 seconds and then raises the mentioned exception. 14 | 15 | The defaults are described at [api.rubyonrails.org](https://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/ConnectionPool.html#class-ActiveRecord::ConnectionAdapters::ConnectionPool-label-Options). 16 | 17 | It's worth mentioning that each Rails process has its own independent connection pool. When having a Puma server running with 2 workers (= 2 processes), each worker would have its own connection pool. 18 | 19 | ## Approaching the Issue 20 | 21 | The error above occurs when a thread tries to checkout a connection from the pool and all connections are in use for more than 5 seconds. This happens when the connection pool's size is smaller than the concurrency configuration of your application (may it be Puma, Sidekiq or whatever). Having less connections than your concurrency configuration doesn't mean there will be errors, though, but if the error raises, that's the reason. 22 | 23 | The solution is simple, do either of these: 24 | 25 | 1. Increase/decrease the connection pool's size to match the application's concurrency configuration 26 | 2. Increase/decrease the application's concurrency configuration to match the connection pool's size 27 | 28 | ## Puma 29 | 30 | Puma allows multi-process and multi-thread request processing. For Puma, "workers" are processes and "threads" are threads. To avoid `ActiveRecord::ConnectionTimeoutError` errors, make sure your Puma thread configuration is ≤ your connection pool's size. 31 | 32 | When used in Rails 6.0.3.4, the default Puma thread configuration from config/puma.rb is `ENV.fetch("RAILS_MAX_THREADS") { 5 }`. So if `ENV["RAILS_MAX_THREADS"]` is present, that is used, if not, use 5 threads. Fortunately, the exact same applies for the connection pool's size. So when using the environment variable, adjusting `ENV["RAILS_MAX_THREADS"]` will just work and you shouldn't experience a `ActiveRecord::ConnectionTimeoutError`. When not using `ENV["RAILS_MAX_THREADS"]`, there's no problem as well as both the Puma threads and the connection pool's size will be 5. 33 | 34 | ## Sidekiq 35 | 36 | Sidekiq allows multi-thread job processing. For Sidekiq, the concurrency configuration defines how many "workers" (= threads) will perform jobs. To avoid `ActiveRecord::ConnectionTimeoutError` errors, make sure your Sidekiq concurrency is ≤ your connection pool's size. 37 | 38 | In Sidekiq 6.1.2, the concurrency is defined by `ENV["RAILS_MAX_THREADS"]` when present. If it's not present, the fallback is 10 threads (!). This can lead to errors as the connection pool's default size is 5. 39 | 40 | In Sidekiq 7.0.0, the default fallback is 5 threads and matches Rails' default concurrency setting. 41 | 42 | When using `ENV["RAILS_MAX_THREADS"]`, you shouldn't experience a `ActiveRecord::ConnectionTimeoutError`. So make sure to either have `ENV["RAILS_MAX_THREADS"]` present, use the Sidekiq CLI options (like `bundle exec sidekiq -c 5)`) or have a config/sidekiq.yml with `concurrency: 5`. 43 | 44 | When multiple concurrency settings are present, the most left value has priority: 45 | 46 | ``` 47 | sidekiq.yml > CLI Option > ENV["RAILS_MAX_THREADS"] > 10 48 | ``` 49 | 50 | Note: Sidekiq 5.2.2 and earlier have a default value of 25. 51 | 52 | ## TL;DR 53 | 54 | Make sure your concurrency configuration is ≤ your connection pool's size: 55 | 56 | - Puma thread configuration ≤ connection pool size 57 | - Sidekiq concurrency ≤ connection pool size 58 | - If possible, use `ENV["RAILS_MAX_THREADS"]` 59 | -------------------------------------------------------------------------------- /rails/column-defaults.md: -------------------------------------------------------------------------------- 1 | # Column Defaults 2 | 3 | A column default will be set by the database if we're creating a new record and when there's no value set for such a column. 4 | 5 | ## Adding Column Defaults 6 | 7 | When adding a new column to a table, we can add a column default for that column: 8 | 9 | ```ruby 10 | class AddDoneToTasks < ActiveRecord::Migration[5.1] 11 | def change 12 | add_column :tasks, :done, :boolean, default: false 13 | end 14 | end 15 | ``` 16 | 17 | When migrating, the database will update all existing rows in the table and set the new column default: 18 | 19 | ```ruby 20 | Task.first.done # => false 21 | ``` 22 | 23 | It will also be set when initializing new records and a value is omitted: 24 | 25 | ```ruby 26 | Task.new.done # => false 27 | Task.new(done: true).done # => true 28 | ``` 29 | 30 | Changing a column default using `change_column_default` will change the column default for new records but will not update any existing records. 31 | 32 | ## Defining Dynamic Column Defaults 33 | 34 | Column defaults as shown above are static, when Rails adds them they will be quoted. If we want to use database functions as column defaults, we need to use a proc: 35 | 36 | ```ruby 37 | class AddUuidToTasks < ActiveRecord::Migration[5.1] 38 | def change 39 | enable_extension 'uuid-ossp' 40 | add_column :tasks, :uuid, :uuid, default: -> { 'uuid_generate_v4()' } 41 | end 42 | end 43 | ``` 44 | 45 | This will generate a new UUID in the database if there's no value set for the column. 46 | 47 | Internally, Rails checks for the type of a column default. If it's a proc, it will use `proc.call` as the column default. If it's not a proc, it will quote the value and use that as the column default. 48 | 49 | When using dynamic column defaults, Rails will not set the value when initializing new records: 50 | 51 | ```ruby 52 | Task.new.uuid # => nil 53 | ``` 54 | 55 | Also, Rails will not return the generated value when creating new records: 56 | 57 | ```ruby 58 | task = Task.create 59 | task.uuid # => nil 60 | 61 | task.reload 62 | task.uuid # => '958f793e-91c9-4f0d-a8f0-f46d3d9478a3' 63 | ``` 64 | 65 | There's a closed issue for that, see [rails/rails#17605](https://github.com/rails/rails/issues/17605). 66 | 67 | ## Generated SQL Queries 68 | 69 | If we're trying to save a record and the value for a column with a column default is either `nil` or exactly the column default, Rails will omit the value and will not send it at all: 70 | 71 | ```ruby 72 | Task.create 73 | # generates: INSERT INTO "tasks" ("created_at", "updated_at") VALUES ($1, $2) RETURNING "id" [["created_at", "2018-02-14 18:35:21.637008"], ["updated_at", "2018-02-14 18:35:21.637008"]] 74 | 75 | Task.create(done: false) 76 | # generates: INSERT INTO "tasks" ("created_at", "updated_at") VALUES ($1, $2) RETURNING "id" [["created_at", "2018-02-14 18:35:50.507602"], ["updated_at", "2018-02-14 18:35:50.507602"]] 77 | 78 | Task.create(done: true) 79 | # generates: INSERT INTO "tasks" ("created_at", "updated_at", "done") VALUES ($1, $2, $3) RETURNING "id" [["created_at", "2018-02-14 18:36:06.606795"], ["updated_at", "2018-02-14 18:36:06.606795"], ["done", "t"]] 80 | ``` 81 | 82 | This is also the reason for the issue mentioned above: Rails is not telling the database to return dynamically generated column defaults when creating records; It's not necessary for static column defaults and not implemented for dynamic column defaults. 83 | -------------------------------------------------------------------------------- /rails/deleting-files-after-sending.md: -------------------------------------------------------------------------------- 1 | # Deleting Files after Sending 2 | 3 | From a controller action, [`send_file`](https://github.com/rails/rails/blob/v7.0.4/actionpack/lib/action_controller/metal/data_streaming.rb#L69-L78) lets you send a file: 4 | 5 | ```ruby 6 | class ReportsController < ApplicationController 7 | def show 8 | file = Report.new.generate_csv 9 | send_file file, filename: 'report.csv' 10 | end 11 | end 12 | ``` 13 | 14 | Now, `send_file` doesn't send the file immediately, but rather saves the path in a variable and uses it for delivery later on. That's why you shouldn't delete the file right after calling `send_file`. This will not work: 15 | 16 | ```ruby 17 | def show 18 | file = Report.new.generate_csv 19 | send_file file, filename: 'report.csv' 20 | 21 | File.delete(file) 22 | File.unlink(file) 23 | end 24 | ``` 25 | 26 | Using an `after_action` callback will behave the same, so that won't work either. If you're only using `delete` and not `unlink`, you might be lucky and it might work, but don't count on it. 27 | 28 | A proper way would be to keep track of files and use a Rack middleware that deletes them after the response body was closed. Something like this: 29 | 30 | ```ruby 31 | class FileReaper 32 | def initialize(app) 33 | @app = app 34 | end 35 | 36 | def call(env) 37 | env['app.files_to_reap'] ||= [] 38 | status, headers, body = @app.call(env) 39 | 40 | body_proxy = Rack::BodyProxy.new(body) do 41 | env['app.files_to_reap'].each do |file| 42 | File.delete(file) if File.file?(file) 43 | end 44 | end 45 | 46 | [status, headers, body_proxy] 47 | end 48 | end 49 | 50 | 51 | # config/application.rb 52 | config.middleware.use(FileReaper) 53 | 54 | # app/controllers/reports_controller.rb 55 | def show 56 | file = Report.new.generate_csv 57 | send_file file, filename: 'report.csv' 58 | 59 | request.env['app.files_to_reap'] << file 60 | end 61 | ``` 62 | 63 | If you're dealing with `Tempfile` objects, there's already a `Rack::TempfileReaper` middleware for that that is enabled in Rails per default: 64 | 65 | ```ruby 66 | def show 67 | tempfile = Report.new.generate_csv_tempfile 68 | send_file tempfile, filename: 'report.csv' 69 | 70 | request.env['rack.tempfiles'] << tempfile 71 | end 72 | ``` 73 | -------------------------------------------------------------------------------- /rails/enqueue-after-transaction-commit.md: -------------------------------------------------------------------------------- 1 | # Enqueue after Transaction Commit 2 | 3 | https://github.com/rails/rails/pull/51426 ([this week in rails](https://rubyonrails.org/2024/4/5/this-week-in-rails)) introduced a feature that, when inside a transaction, will enqueue Active Jobs only after the transaction commits: 4 | 5 | ```ruby 6 | Topic.transaction do 7 | topic = Topic.create 8 | NewTopicNotificationJob.perform_later(topic) 9 | end 10 | ``` 11 | 12 | In this example, `NewTopicNotificationJob` will not be enqueued inside the transaction, it enqueues when the transactions commits. This feature is enabled by default for Job Backends that don't share the database with Active Record (Async, Sidekiq, SuckerPunch, the Test Adapter, …). 13 | -------------------------------------------------------------------------------- /rails/gems-i-use.md: -------------------------------------------------------------------------------- 1 | # Gems I Use 2 | 3 | A collection of rails gems I like and use: 4 | 5 | ## Configuration 6 | 7 | - [dotenv-rails](https://github.com/bkeepers/dotenv) 8 | 9 | ## Background Jobs 10 | 11 | - [sidekiq](https://github.com/sidekiq/sidekiq) 12 | - [sidekiq-scheduler](https://github.com/sidekiq-scheduler/sidekiq-scheduler) 13 | - [good_job](https://github.com/bensheldon/good_job) 14 | - [solid_queue](https://github.com/rails/solid_queue) 15 | 16 | ## Templating 17 | 18 | - [slim-rails](https://github.com/slim-template/slim-rails) 19 | - [liquid](https://github.com/Shopify/liquid) 20 | 21 | ## Decoration 22 | 23 | - [dekorator](https://github.com/komposable/dekorator) 24 | - [draper](https://github.com/drapergem/draper) 25 | 26 | ## Authentication 27 | 28 | - [action_auth](https://github.com/kobaltz/action_auth) 29 | - [omniauth](https://github.com/omniauth/omniauth) 30 | - [ruby-saml](https://github.com/SAML-Toolkits/ruby-saml) 31 | 32 | ## Authorization 33 | 34 | - [pundit](https://github.com/varvet/pundit) 35 | 36 | ## Pagination 37 | 38 | - [pagy](https://github.com/ddnexus/pagy) 39 | - [kaminari](https://github.com/kaminari/kaminari) 40 | 41 | ## Testing 42 | 43 | - [rspec-rails](https://github.com/rspec/rspec-rails) 44 | - [factory_bot_rails](https://github.com/thoughtbot/factory_bot_rails) 45 | 46 | ## Deployment 47 | 48 | - [kamal](https://github.com/basecamp/kamal) 49 | - [capistrano](https://github.com/capistrano/capistrano) 50 | 51 | ## Application Server 52 | 53 | - [puma](https://github.com/puma/puma) 54 | - [pitchfork](https://github.com/Shopify/pitchfork) 55 | - [thruster](https://github.com/basecamp/thruster) 56 | 57 | ## Debugging 58 | 59 | - [pry-rails](https://github.com/pry/pry-rails) 60 | - [pry-byebug](https://github.com/deivid-rodriguez/pry-byebug) 61 | 62 | ## Linter 63 | 64 | - [rubocop](https://github.com/rubocop/rubocop) 65 | - [standard](https://github.com/standardrb/standard) 66 | 67 | ## Security 68 | 69 | - [bundler-audit](https://github.com/rubysec/bundler-audit) 70 | - [rack-attack](https://github.com/rack/rack-attack) 71 | 72 | ## Excel 73 | 74 | - [caxlsx](https://github.com/caxlsx/caxlsx) 75 | - [roo](https://github.com/roo-rb/roo) 76 | 77 | # Profiling 78 | 79 | - [rack-mini-profiler](https://github.com/MiniProfiler/rack-mini-profiler) 80 | - [memory_profiler](https://github.com/SamSaffron/memory_profiler) 81 | - [stackprof](https://github.com/tmm1/stackprof) 82 | 83 | ## Misc 84 | 85 | - [activerecord-typedstore](https://github.com/byroot/activerecord-typedstore) 86 | - [asset_ram](https://github.com/dogweather/asset_ram) 87 | - [faker](https://github.com/faker-ruby/faker) 88 | - [frozen_record](https://github.com/byroot/frozen_record) 89 | - [http](https://github.com/httprb/http) 90 | - [money-rails](https://github.com/RubyMoney/money-rails) 91 | - [rack-cors](https://github.com/cyu/rack-cors) 92 | - [simple_calendar](https://github.com/excid3/simple_calendar) 93 | - [super_diff](https://github.com/mcmire/super_diff) 94 | - [turbo_power](https://github.com/marcoroth/turbo_power) 95 | - [with_advisory_lock](https://github.com/ClosureTree/with_advisory_lock) 96 | -------------------------------------------------------------------------------- /rails/headless-system-tests.md: -------------------------------------------------------------------------------- 1 | # Headless System Tests 2 | 3 | System Tests internally use Capybara. The default driver used by Capybara is `:selenium`. This driver can use different browsers to request pages, where Google Chrome is the default. 4 | 5 | A regular System Test setup looks like this: 6 | 7 | ```ruby 8 | # test/system/projects_test.rb 9 | 10 | require 'application_system_test_case' 11 | 12 | class ProjectsTest < ApplicationSystemTestCase 13 | test 'visiting the index' do 14 | visit projects_url 15 | assert_selector 'h1', text: 'Projects' 16 | end 17 | end 18 | ``` 19 | 20 | where `ApplicationSystemTestCase` is defined as: 21 | 22 | ```ruby 23 | # test/application_system_test_case.rb 24 | 25 | require 'test_helper' 26 | 27 | class ApplicationSystemTestCase < ActionDispatch::SystemTestCase 28 | driven_by :selenium, using: :chrome, screen_size: [1400, 1400] 29 | end 30 | ``` 31 | 32 | Running this test will visually open up Google Chrome, visit the page and run the assertion. 33 | 34 | ## Beheading the Browser 35 | 36 | If we don't want to open up Google Chrome (or if we can't do so, think of a CI pipeline), we can configure the System Test to use a headless Google Chrome as its browser: 37 | 38 | ```ruby 39 | class ApplicationSystemTestCase < ActionDispatch::SystemTestCase 40 | driven_by :selenium, using: :headless_chrome, screen_size: [1400, 1400] 41 | end 42 | ``` 43 | 44 | This will not open up Google Chrome visually, but instead headless, not visible at all. 45 | 46 | ## System Specs in RSpec 47 | 48 | System Specs in RSpec don't use the `ApplicationSystemTestCase`, but we can use `driven_by` to override the default behaviour in our spec: 49 | 50 | ```ruby 51 | # spec/system/projects_spec.rb 52 | require 'rails_helper' 53 | 54 | RSpec.describe 'Project management', type: :system do 55 | before do 56 | driven_by :selenium, using: :headless_chrome, screen_size: [1400, 1400] 57 | end 58 | 59 | it 'displays projects' do 60 | visit projects_url 61 | expect(page).to have_text('Projects') 62 | end 63 | end 64 | ``` 65 | 66 | ## Using Docker 67 | 68 | When using Docker (or rather: using root), Google Chrome won't work as expected and needs the `--no-sandbox` option to properly function: 69 | 70 | ```ruby 71 | before do 72 | driven_by :selenium, using: :headless_chrome, screen_size: [1400, 1400] do |driver_option| 73 | driver_option.add_argument('--no-sandbox') 74 | end 75 | end 76 | ``` 77 | -------------------------------------------------------------------------------- /rails/postgres-backed-enums.md: -------------------------------------------------------------------------------- 1 | # Postgres-Backed Enums 2 | 3 | Rails supports soft [Enums](http://guides.rubyonrails.org/v5.2.0/active_record_querying.html#enums) but doesn't enforce data integrity on the database level. Luckily, we can make use of Postgres' Data Types for that: 4 | 5 | ```ruby 6 | class AddFavoriteSeasonToPeople < ActiveRecord::Migration[5.2] 7 | def up 8 | execute "CREATE TYPE season AS ENUM ('spring', 'summer', 'fall', 'winter');" 9 | add_column :people, :favorite_season, :season 10 | end 11 | 12 | def down 13 | remove_column :people, :favorite_season 14 | execute 'DROP TYPE season' 15 | end 16 | end 17 | 18 | class Person < ApplicationRecord 19 | enum favorite_season: { 20 | spring: 'spring', 21 | summer: 'summer', 22 | fall: 'fall', 23 | winter: 'winter' 24 | } 25 | end 26 | 27 | person = Person.first 28 | 29 | # happy path 30 | person.update!(favorite_season: 'summer') 31 | person.favorite_season # => 'summer' 32 | 33 | # soft validation 34 | person.update!(favorite_season: 'not a season') # => ArgumentError: 'not a season' is not a valid favorite_season 35 | 36 | # hard constraint 37 | person.update_column(:favorite_season, 'not a season') # => ActiveRecord::StatementInvalid: PG::InvalidTextRepresentation: ERROR: invalid input value for enum favorite_season: "not a season" 38 | ``` 39 | -------------------------------------------------------------------------------- /rails/recyclable-cache-keys.md: -------------------------------------------------------------------------------- 1 | # Recyclable Cache Keys 2 | 3 | In earlier versions, Rails would generate cache keys for Active Record objects using the model name, the id and the updated_at timestamp: 4 | 5 | ```ruby 6 | Project.first.cache_key # => 'projects/1-20180522081941758726000' 7 | ``` 8 | 9 | When an object changed a lot, we would have a lot of trash cache keys in the cache store, waiting for expiration. 10 | 11 | Since Rails 5.2, cache keys for Active Record objects are stable and generated without a timestamp: 12 | 13 | ```ruby 14 | Project.first.cache_key # => 'projects/1' 15 | ``` 16 | 17 | Having this, we don't trash our cache store with cache keys that are stale, but instead override the same cache key over and over. Therefore the term "recyclable cache keys". 18 | 19 | ## Internals 20 | 21 | Internally, when caching an object, Rails will not cache that object directly. Instead, it will cache an `ActiveSupport::Cache::Entry` which wraps the object and adds some information: The creation date of that entry and the expiration time in seconds (for cache stores that can't expire on their own) and a version. The version part is new and consists of the updated_at timestamp for ActiveRecord object cache keys. 22 | 23 | When using `Rails.cache.fetch`, the actual version is compared to the cached version and if there's a mismatch it is considered a cache miss. 24 | 25 | ## Opting out 26 | 27 | When we want to use old-fashioned cache keys, we can use the `#cache_key_with_version` method: 28 | 29 | ```ruby 30 | Project.first.cache_key_with_version # => 'projects/1-20180522081941758726000' 31 | ``` 32 | 33 | … or disable cache versioning completely using: 34 | 35 | ```ruby 36 | config.active_record.cache_versioning = false 37 | ``` 38 | -------------------------------------------------------------------------------- /rails/rspec-rails-types.md: -------------------------------------------------------------------------------- 1 | # RSpec::Rails Types 2 | 3 | Controller-related RSpec::Rails Spec Types are: 4 | 5 | 6 | | Tests | Rails Name | Uses | RSpec Name | RSpec Type | 7 | |:----------:|:----------------:|:-------------------------------:|:---------------:|:-------------:| 8 | | Model | Model Test | ActiveSupport::TestCase | Model Spec | `:model` | 9 | | Controller | Controller Test | ActionController::TestCase | Controller Spec | `:controller` | 10 | | Controller | Integration Test | ActionDispatch::IntegrationTest | Request Spec | `:request` | 11 | | System | System Test | ActionDispatch::SystemTestCase | System Spec | `:system` | 12 | | System | | | Feature Spec | `:feature` | 13 | 14 | # Controller Spec 15 | 16 | Controller Specs don't use Rails' middleware stack (and therefore no routing). They internally run `ProjectsController.new(…).dispatch!` in the same thread. 17 | 18 | Example: 19 | 20 | ```ruby 21 | RSpec.describe ProjectsController, type: :controller do 22 | describe 'GET index' do 23 | it 'responds with OK' do 24 | get :index 25 | expect(response).to have_http_status(:ok) 26 | end 27 | end 28 | end 29 | ``` 30 | 31 | # Request Spec 32 | 33 | Request Specs use Rails's middleware stack (therefore using routing), effectively using `Rails.application`. They internally create an `ActionDispatch::Integration::Session` (which uses `Rack::Test::Session.new(Rack::MockSession.new(@app, host))`) in the same thread. 34 | 35 | Example: 36 | 37 | ```ruby 38 | RSpec.describe 'Projects', type: :request do 39 | describe 'GET /projects' do 40 | it 'responds with OK' do 41 | get projects_path 42 | expect(response).to have_http_status(:ok) 43 | end 44 | end 45 | end 46 | ``` 47 | 48 | # System Spec 49 | 50 | System Specs run an application server (like Puma) in a different thread using Capybara. Calling `visit` sends a request to that server. 51 | 52 | Example: 53 | 54 | ```ruby 55 | RSpec.describe 'Projects', type: :system do 56 | describe 'GET /projects' do 57 | it 'displays a title' do 58 | visit projects_url 59 | expect(page).to have_text('Projects') 60 | end 61 | end 62 | end 63 | ``` 64 | 65 | # Feature Spec 66 | 67 | Feature Specs work like System Specs, they simply existed before Rails added System Tests. If possible, use System Specs over Feature Specs. 68 | -------------------------------------------------------------------------------- /rails/transactional-tests.md: -------------------------------------------------------------------------------- 1 | # Transactional Tests 2 | 3 | Transactional Tests (known as Transactional Fixtures in Rails < 5.0.0) is a configuration option that, when enabled, wraps each test in a database transaction which is being rolled back in a teardown. 4 | 5 | The configuration is enabled per [default](https://github.com/rails/rails/blob/v5.2.0/activerecord/lib/active_record/fixtures.rb#L873). 6 | 7 | ## Problem 8 | 9 | A recurring problem with integration tests was that the application server used by Capybara was running in a different thread than the test and therefore used a different database connection. Since a database transaction was wrapping the test, the application server wouldn't see any changes to the database. 10 | 11 | This lead to disabling Transactional Tests/Fixtures and using a database cleaner library to clear the database state before/after each test like [DatabaseRewinder](https://github.com/amatsuda/database_rewinder) or [DatabaseCleaner](https://github.com/DatabaseCleaner/database_cleaner). 12 | 13 | This behaviour changed with [rails!28083](https://github.com/rails/rails/pull/28083). From Rails 5.1.0 onwards, the test and application server would be locked to use the same connection, effectively fixing the problem and removing the need for a database cleaner. 14 | 15 | This new behaviour looks to be present for all kinds of tests but [is assumed](https://github.com/rails/rails/pull/28083#issuecomment-313493049) to only work for System Tests. 16 | 17 | ## RSpec::Rails 18 | 19 | The same behaviour is available for RSpec::Rails when enabling the config: 20 | 21 | ```ruby 22 | RSpec.configure do |config| 23 | config.use_transactional_fixtures = true 24 | end 25 | ``` 26 | 27 | The config's name might change in the future, but now (RSpec::Rails 3.7.2) it's `use_transactional_fixtures`. 28 | -------------------------------------------------------------------------------- /ruby/condition-variables.md: -------------------------------------------------------------------------------- 1 | # Condition Variables 2 | 3 | Condition variables can put threads to sleep and wake them later on when a certain condition is met. This is useful for implementing blocking method calls without having to poll for other objects or using expensive loops. They have to be used in a `Mutex#synchronize` call. 4 | 5 | When we want to put a thread to sleep, we can call `condition_variable.wait(some_mutex)`. This will put the calling thread to sleep and unlock the mutex. This is not limited to a single thread but several threads can wait on a single condition variable. In order to wake exactly one thread up again, we can call `condition_variable.signal`. This will wake up the first sleeping/waiting thread, locking the mutex again. 6 | 7 | We could also wake up all sleeping threads by calling `condition_variable.broadcast`. 8 | 9 | ## Example 10 | 11 | Below is an example of a Queue class with a blocking `shift` method. When the queue is empty and a thread is trying to shift an item off of it, the condition variable will put the thread to sleep, effectively blocking the method call. Once there is an item added to the queue, the condition variable will signal and wake up the thread. 12 | 13 | ```ruby 14 | class BlockingQueue 15 | def initialize 16 | @items = [] 17 | @mutex = Mutex.new 18 | @condition_variable = ConditionVariable.new 19 | end 20 | 21 | def <<(item) 22 | @mutex.synchronize do 23 | @items << item 24 | @condition_variable.signal 25 | end 26 | end 27 | 28 | def shift 29 | @mutex.synchronize do 30 | while @items.empty? 31 | @condition_variable.wait(@mutex) 32 | end 33 | 34 | @items.shift 35 | end 36 | end 37 | end 38 | 39 | queue = BlockingQueue.new 40 | 41 | consumer = Thread.new do 42 | puts 'Consumer: Waiting for an Item' 43 | item = queue.shift 44 | puts 'Consumer: Shifted an Item' 45 | end 46 | 47 | producer = Thread.new do 48 | sleep 2 49 | puts 'Producer: Adding an Item' 50 | queue << 'Some Item' 51 | end 52 | 53 | [consumer, producer].each(&:join) 54 | ``` 55 | 56 | … will output: 57 | 58 | ``` 59 | Consumer: Waiting for an Item 60 | Producer: Adding an Item 61 | Consumer: Shifted an Item 62 | ``` 63 | 64 | ## Spurious Wakeups 65 | 66 | It's possible for threads to spuriously wakeup from a condition variable induced sleep without receiving a signal or broadcast. This has to do with speeding up condition variable operations and can be read up [here](https://stackoverflow.com/questions/8594591/why-does-pthread-cond-wait-have-spurious-wakeups/8594644#8594644). That's also a reason for always using a `while` loop around a `condition_variable.wait(mutex)`, so a spurios wakeup won't do any harm. 67 | 68 | ## Credits 69 | 70 | Credits go to Tom Van Eyck for his excellent blog post on [Ruby concurrency: in praise of condition variables](https://vaneyckt.io/posts/ruby_concurrency_in_praise_of_condition_variables/). 71 | -------------------------------------------------------------------------------- /ruby/exception-keyword-arguments.md: -------------------------------------------------------------------------------- 1 | # Exception Keyword Argument 2 | 3 | Ruby 2.6 introduces a new `exception` Keyword Argument for certain Kernel methods which changes their behaviour: 4 | 5 | ```ruby 6 | Integer('not-a-number') # => ArgumentError (invalid value for Integer(): "not-a-number") 7 | Integer('not-a-number', exception: false) # nil 8 | 9 | Float('not-a-number') # => ArgumentError (invalid value for Float(): "not-a-number") 10 | Float('not-a-number', exception: false) # => nil 11 | 12 | Complex('not-a-number') # => ArgumentError (invalid value for convert(): "not-a-number") 13 | Complex('not-a-number', exception: false) # => nil 14 | 15 | Rational('not-a-number') # => ArgumentError (invalid value for convert(): "not-a-number") 16 | Rational('not-a-number', exception: false) # => nil 17 | 18 | system('not-a-command') # => nil 19 | system('not-a-command', exception: true) # => Errno::ENOENT (No such file or directory - not-a-command) 20 | ``` 21 | 22 | For `Kernel#system`, `exception: true` will trigger if execution succeeds but the exit status is non-zero: 23 | 24 | ```ruby 25 | system('exit 1') # => false 26 | system('exit 2', exception: true) # => RuntimeError (Command failed with exit 1: exit 1) 27 | ``` 28 | 29 | Props to Shannon Skipper for his [article](https://medium.com/square-corner-blog/rubys-new-exception-keyword-arguments-4d5bbb504d37) on the topic. 30 | -------------------------------------------------------------------------------- /ruby/forwardable.md: -------------------------------------------------------------------------------- 1 | # Forwardable and SingleForwardable 2 | 3 | [Forwardable](https://ruby-doc.org/stdlib-2.5.1/libdoc/forwardable/rdoc/Forwardable.html) and [SingleForwardable](http://ruby-doc.org/stdlib-2.5.1/libdoc/forwardable/rdoc/SingleForwardable.html) offer methods to forward (or delegate) method calls to a designated object. 4 | 5 | ## Forwardable 6 | 7 | Forwardable supports three methods of delegating methods: `def_delegator`, `def_delegators` and `delegate`. 8 | 9 | ### def_delegator 10 | 11 | `def_delegator` delegates single methods: 12 | 13 | ```ruby 14 | class User 15 | extend Forwardable 16 | 17 | def_delegator :@address, :street 18 | def_delegator :@address, :city, :address_city 19 | 20 | # basically defines: 21 | # 22 | # def street 23 | # @address.street 24 | # end 25 | # 26 | # def address_city 27 | # @address.city 28 | # end 29 | end 30 | ``` 31 | 32 | ### def_delegators 33 | 34 | `def_delegators` delegates several methods (using `def_delegator`): 35 | 36 | ```ruby 37 | class User 38 | extend Forwardable 39 | 40 | def_delegators :@address, :street, :city 41 | 42 | # is basically the same as: 43 | # 44 | # def_delegator :@address, :street 45 | # def_delegator :@address, :city 46 | end 47 | ``` 48 | 49 | When using `def_delegators`, the `__send__` and `__id__` methods are ignored so they are not overriden. 50 | 51 | ### delegate 52 | 53 | `delegate` delegates methods (using `def_delegator`): 54 | 55 | ```ruby 56 | class User 57 | extend Forwardable 58 | 59 | delegate :street => :@address 60 | delegate :city => :@address 61 | 62 | # or: 63 | delegate [:street, :city] => :@address 64 | 65 | # or: 66 | delegate street: :@address, city: :@address 67 | 68 | # is the same as: 69 | # 70 | # def_delegator :@address, :street 71 | # def_delegator :@address, :city 72 | end 73 | ``` 74 | 75 | ## SingleForwardable 76 | 77 | SingleForwardable works like Forwardable but on the object level: 78 | 79 | ```ruby 80 | user = User.new 81 | user.extend(SingleForwardable) 82 | 83 | user.def_delegator(:@address, :street) 84 | user.def_delegator(:@address, :city) 85 | 86 | # or 87 | user.def_delegators(:@address, :street, :city) 88 | 89 | # or 90 | user.delegate([:street, :city] => :@address) 91 | ``` 92 | -------------------------------------------------------------------------------- /ruby/gdbm.md: -------------------------------------------------------------------------------- 1 | # GDBM 2 | 3 | [GNU dbm](https://www.gnu.org.ua/software/gdbm/) (GDBM) is a simple database for storing key value pairs in a file. Keys and values are strings and a database can only be accessed by either many readers or exactly one writer at a time. 4 | 5 | Ruby's standard library includes a [gdbm](https://github.com/ruby/gdbm) C extension that wraps around the library and let's us use it like a Hash. 6 | 7 | ## Writing 8 | 9 | Writing key value pairs: 10 | 11 | ```ruby 12 | require 'gdbm' 13 | 14 | gdbm = GDBM.new('database.db') 15 | gdbm['foo'] = 'bar' 16 | gdbm.close 17 | ``` 18 | 19 | ## Reading 20 | 21 | ```ruby 22 | gdbm = GDBM.new('database.db') 23 | gdbm['foo'] # => 'bar' 24 | gdbm.close 25 | ``` 26 | 27 | As `GDBM` includes the `Enumerable` module, all its goodies are available. 28 | 29 | ## Block Shortcut 30 | 31 | Instead of `.new` we can use `.open` which will open a database file, yield the `GDBM` object and close the database when it's done: 32 | 33 | ```ruby 34 | GDBM.open('database.db') do |gdbm| 35 | gdbm['foo'] # => 'bar' 36 | end 37 | ``` 38 | 39 | ## Read-Only Access 40 | 41 | The default access is `GDBM::WRITER` which can read and write data. If we only want to read data, we can open a database as a `GDBM::READER`: 42 | 43 | ```ruby 44 | read_only_gdbm = GDBM.new('database.db', mode = 0666, GDBM::READER) 45 | read_only_gdbm['foo'] # => 'bar' 46 | ``` 47 | 48 | … which will raise when trying to write: 49 | 50 | ``` 51 | read_only_gdbm['foo'] = 'baz' # => GDBMError: Reader can't store 52 | ``` 53 | 54 | ## Concurrent Access 55 | 56 | As stated, only many readers or exactly one writer can access a database at a time. But it seems the wrapper is clever for a single process, as: 57 | 58 | ```ruby 59 | gdbm_writer = GDBM.new('database.db', mode = 0666, GDBM::WRITER) 60 | gdbm_reader = GDBM.new('database.db', mode = 0666, GDBM::READER) 61 | ``` 62 | 63 | … works, but this doesn't: 64 | 65 | ```ruby 66 | gdbm_writer = GDBM.new('database.db', mode = 0666, GDBM::WRITER) 67 | 68 | fork do 69 | gdbm_reader = GDBM.new('database.db', mode = 0666, GDBM::READER) # raises Errno::EAGAIN: Resource temporarily unavailable - database.db 70 | end 71 | ``` 72 | -------------------------------------------------------------------------------- /ruby/pass-by.md: -------------------------------------------------------------------------------- 1 | # Pass By 2 | 3 | What's the difference between Pass By Value and Pass By Reference and what does Ruby use? 4 | 5 | ## Pass By Value 6 | 7 | Pass By Value means that when we're passing anything into a method, a mere copy of its value is used inside the method. If Ruby were strictly Pass By Value, this would happen: 8 | 9 | ```ruby 10 | def change(just_a_copy) 11 | just_a_copy[:foo] = 'bar' 12 | end 13 | 14 | some_hash = {} 15 | change(some_hash) 16 | 17 | some_hash # => {} 18 | ``` 19 | 20 | ## What is Pass By Reference? 21 | 22 | Pass By Reference means that when we're passing anything into a method, the same reference will be used inside the method. If Ruby were Pass By Reference, this would happen: 23 | 24 | ```ruby 25 | def change(same_variable) 26 | same_variable = 'something else' 27 | end 28 | 29 | variable = 'something' 30 | change(variable) 31 | 32 | variable # => 'something else' 33 | ``` 34 | 35 | ## What is Ruby? 36 | 37 | Ruby is Pass By Value, but the copied values are actually Object References. That's why changing an object in a method changes it for all variables referencing that Object. 38 | 39 | See apeiros' [Gist](https://gist.github.com/apeiros/bf3ddb3d332dc01ac43840e0d08b382d) and Cezar's [blog post](https://mixandgo.com/learn/is-ruby-pass-by-reference-or-pass-by-value). 40 | -------------------------------------------------------------------------------- /ruby/prepending-modules.md: -------------------------------------------------------------------------------- 1 | # Prepending Modules 2 | 3 | When calling `SomeClass.new.some_method`, Ruby first looks for the method `some_method` in `SomeClass`, then `Object`, then `Kernel` and then `BasicObject`. That's the class's ancestor chain: 4 | 5 | ```ruby 6 | SomeClass = Class.new 7 | SomeClass.ancestors # => [SomeClass, Object, Kernel, BasicObject] 8 | ``` 9 | 10 | When including a module into a class, that module comes right after the class itself: 11 | 12 | ```ruby 13 | SomeModule = Module.new 14 | 15 | class SomeClass 16 | include SomeModule 17 | end 18 | 19 | SomeClass.ancestors # => [SomeClass, SomeModule, Object, Kernel, BasicObject] 20 | ``` 21 | 22 | Since Ruby 2.0.0 we can prepend a module to a class, which will place it to the ancestor chain's first position: 23 | 24 | ```ruby 25 | SomeModule = Module.new 26 | 27 | class SomeClass 28 | prepend SomeModule 29 | end 30 | 31 | SomeClass.ancestors # => [SomeModule, SomeClass, Object, Kernel, BasicObject] 32 | ``` 33 | 34 | Note: The actual method lookup described here is simplified and does not mention singleton classes and other internals. 35 | --------------------------------------------------------------------------------