June 26, 2017·4 min read·engineeringrubyrailsconcurrency

Rails 5.1.2 synchronizes model schema loading

Rails 5.1.2 protects lazy model schema initialization with a monitor. The maintenance release also restores collection-proxy behavior and registers additional system-test drivers.

Lazy initialization becomes shared mutable state when several threads can request the same model metadata. Loading a model's schema requires more than retrieving column names. Active Record also builds attribute definitions and defaults from that information. Rails 5.1.2 adds synchronization around this process so concurrent access does not treat a partially initialized model as ready.

The June 26 announcement publishes the maintenance release and links its component changelogs. The Active Record changelog specifically identifies thread-safe loading of model schema, credited to Vikrant Chaudhary and David Abdemoulaie. It also restores earlier collection-proxy behavior. These repairs concern existing framework operations on the 5.1 branch.

I find Ruby's object model straightforward to read, but a class can hold state that several requests reach at once. A reader looking at one model operation should not have to infer whether another thread has finished establishing the metadata beneath it. The framework needs to define that initialization boundary.

Completion has a separate marker

The tagged schema implementation creates a Monitor for schema loading and initializes a separate monitor for subclasses. load_schema first checks whether the schema is already marked loaded. Otherwise it enters the monitor, performs the required loading and marks completion afterward.

The central sequence makes the ordering visible. This excerpt omits surrounding helper definitions but preserves the synchronization and completion steps.

@load_schema_monitor.synchronize do
  return if defined?(@columns_hash) && @columns_hash

  load_schema!
  @schema_loaded = true
end

load_schema! obtains the column hash from the connection's schema cache, excludes ignored columns and defines each attribute using its cast type and database default. The completion flag follows that work. Merely assigning a column hash is therefore not the same as completing every attribute definition derived from it.

The ordinary public metadata methods converge on this loader. The source calls it for columns_hash, columns, attribute_types and column_defaults. Several different model operations can consequently request the same initialization. Synchronizing the common path keeps those callers from each needing separate coordination.

The loaded flag also permits the completed path to return without entering the monitor again. That preserves lazy setup without making every metadata access repeat the entire initialization. The important distinction is whether initialization has finished, not merely whether some related instance variable has acquired a value.

Resetting metadata reverses the completed state

The same source has a reload path that clears derived metadata and sets @schema_loaded to false. Its cleared state includes column names, attribute types, defaults and the attributes builder. Descendants also reload their schema caches. This matters because a completion flag must agree with the data it describes after invalidation.

reset_column_information clears connection and data-source caches, undefines attribute methods and invokes the schema reload path. A model whose schema has been invalidated needs a later access to initialize it again. Retaining the loaded marker while clearing the dependent values would make the fast return path incorrect.

This repair should not be expanded into a claim that every operation on an Active Record model becomes thread-safe. The changelog names schema loading, and the implementation protects that initialization path. Other shared application state still has its own rules. The narrower scope is enough to explain why this patch can affect a concurrent application without changing its model declarations.

Collection proxies preserve existing customization

The release restores collection-proxy behavior so values can have methods stubbed and respect extension modules applied by a default scope. These are compatibility repairs to the object exposed by an association. An application or test may depend on a customized collection continuing to behave like the expected object.

That is separate from database schema initialization, but both changes preserve assumptions under familiar model calls. A proxy that ignores an extension can break an operation despite returning the expected records. Returning correct data is insufficient if the object no longer supports the behavior the application attached to it.

I value maintenance fixes that name this kind of behavioral contract. A concise association expression can depend on substantial framework machinery. The changelog gives maintainers a specific reason to inspect proxy customization when evaluating the release, instead of treating every failure as an unexplained application difference.

System-test drivers receive their options

The Action Pack changelog adds registration for Poltergeist and capybara-webkit through driven_by. The tagged driver implementation constructs the selected backend and applies additional options. This extends the browser-test integration introduced in Rails 5.1.

An application's driver choice needs to reach the actual Capybara registration, not remain only a declaration in its test class. The release connects those options to the chosen driver while preserving the framework's setup point. Alongside schema-loading synchronization and proxy compatibility, it makes existing 5.1 facilities behave according to the configuration and state their callers already express.