Skip to content

Commit

Permalink
Add readonly mode
Browse files Browse the repository at this point in the history
Fix: #423

When you know that the cache won't be used again, it avoid
some useless work and IOs.

Typically this might be the case for dockerized applications. You
generate a cache when building the image, but then when you boot the
application any cache update won't be persisted, so thre is no point.
  • Loading branch information
byroot committed Nov 24, 2022
1 parent 86612a5 commit b51397f
Show file tree
Hide file tree
Showing 8 changed files with 76 additions and 13 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ Bootsnap.setup(
load_path_cache: true, # Optimize the LOAD_PATH with a cache
compile_cache_iseq: true, # Compile Ruby code into ISeq cache, breaks coverage reporting.
compile_cache_yaml: true, # Compile YAML into a cache
readonly: true, # Use the caches but don't update them on miss or stale entries.
)
```

Expand All @@ -77,6 +78,7 @@ well together, and are both included in a newly-generated Rails applications by
- `DISABLE_BOOTSNAP` allows to entirely disable bootsnap.
- `DISABLE_BOOTSNAP_LOAD_PATH_CACHE` allows to disable load path caching.
- `DISABLE_BOOTSNAP_COMPILE_CACHE` allows to disable ISeq and YAML caches.
- `BOOTSNAP_READONLY` configure bootsnap to not update the cache on miss or stale entries.
- `BOOTSNAP_LOG` configure bootsnap to log all caches misses to STDERR.
- `BOOTSNAP_IGNORE_DIRECTORIES` a comma separated list of directories that shouldn't be scanned.
Useful when you have large directories of non-ruby files inside `$LOAD_PATH`.
Expand Down
31 changes: 23 additions & 8 deletions ext/bootsnap/bootsnap.c
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,11 @@ static ID instrumentation_method;
static VALUE sym_miss;
static VALUE sym_stale;
static bool instrumentation_enabled = false;
static bool readonly = false;

/* Functions exposed as module functions on Bootsnap::CompileCache::Native */
static VALUE bs_instrumentation_enabled_set(VALUE self, VALUE enabled);
static VALUE bs_readonly_set(VALUE self, VALUE enabled);
static VALUE bs_compile_option_crc32_set(VALUE self, VALUE crc32_v);
static VALUE bs_rb_fetch(VALUE self, VALUE cachedir_v, VALUE path_v, VALUE handler, VALUE args);
static VALUE bs_rb_precompile(VALUE self, VALUE cachedir_v, VALUE path_v, VALUE handler);
Expand Down Expand Up @@ -166,6 +168,7 @@ Init_bootsnap(void)
rb_global_variable(&sym_stale);

rb_define_module_function(rb_mBootsnap, "instrumentation_enabled=", bs_instrumentation_enabled_set, 1);
rb_define_module_function(rb_mBootsnap_CompileCache_Native, "readonly=", bs_readonly_set, 1);
rb_define_module_function(rb_mBootsnap_CompileCache_Native, "coverage_running?", bs_rb_coverage_running, 0);
rb_define_module_function(rb_mBootsnap_CompileCache_Native, "fetch", bs_rb_fetch, 4);
rb_define_module_function(rb_mBootsnap_CompileCache_Native, "precompile", bs_rb_precompile, 3);
Expand All @@ -182,6 +185,13 @@ bs_instrumentation_enabled_set(VALUE self, VALUE enabled)
return enabled;
}

static VALUE
bs_readonly_set(VALUE self, VALUE enabled)
{
readonly = RTEST(enabled);
return enabled;
}

/*
* Bootsnap's ruby code registers a hook that notifies us via this function
* when compile_option changes. These changes invalidate all existing caches.
Expand Down Expand Up @@ -945,12 +955,17 @@ try_input_to_storage(VALUE arg)
static int
bs_input_to_storage(VALUE handler, VALUE args, VALUE input_data, VALUE pathval, VALUE * storage_data)
{
int state;
struct i2s_data i2s_data = {
.handler = handler,
.input_data = input_data,
.pathval = pathval,
};
*storage_data = rb_protect(try_input_to_storage, (VALUE)&i2s_data, &state);
return state;
if (readonly) {
*storage_data = rb_cBootsnap_CompileCache_UNCOMPILABLE;
return 0;
} else {
int state;
struct i2s_data i2s_data = {
.handler = handler,
.input_data = input_data,
.pathval = pathval,
};
*storage_data = rb_protect(try_input_to_storage, (VALUE)&i2s_data, &state);
return state;
}
}
4 changes: 4 additions & 0 deletions lib/bootsnap.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ def setup(
development_mode: true,
load_path_cache: true,
ignore_directories: nil,
readonly: false,
compile_cache_iseq: true,
compile_cache_yaml: true,
compile_cache_json: true
Expand All @@ -49,6 +50,7 @@ def setup(
cache_path: "#{cache_dir}/bootsnap/load-path-cache",
development_mode: development_mode,
ignore_directories: ignore_directories,
readonly: readonly,
)
end

Expand All @@ -57,6 +59,7 @@ def setup(
iseq: compile_cache_iseq,
yaml: compile_cache_yaml,
json: compile_cache_json,
readonly: readonly,
)
end

Expand Down Expand Up @@ -101,6 +104,7 @@ def default_setup
compile_cache_iseq: !ENV["DISABLE_BOOTSNAP_COMPILE_CACHE"],
compile_cache_yaml: !ENV["DISABLE_BOOTSNAP_COMPILE_CACHE"],
compile_cache_json: !ENV["DISABLE_BOOTSNAP_COMPILE_CACHE"],
readonly: !!ENV["BOOTSNAP_READONLY"],
ignore_directories: ignore_directories,
)

Expand Down
6 changes: 5 additions & 1 deletion lib/bootsnap/compile_cache.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ def UNCOMPILABLE.inspect
Error = Class.new(StandardError)
PermissionError = Class.new(Error)

def self.setup(cache_dir:, iseq:, yaml:, json:)
def self.setup(cache_dir:, iseq:, yaml:, json:, readonly: false)
if iseq
if supported?
require_relative("compile_cache/iseq")
Expand All @@ -37,6 +37,10 @@ def self.setup(cache_dir:, iseq:, yaml:, json:)
warn("[bootsnap/setup] JSON parsing caching is not supported on this implementation of Ruby")
end
end

if supported? && defined?(Bootsnap::CompileCache::Native)
Bootsnap::CompileCache::Native.readonly = readonly
end
end

def self.permission_error(path)
Expand Down
4 changes: 2 additions & 2 deletions lib/bootsnap/load_path_cache.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,13 @@ class << self
alias_method :enabled?, :enabled
remove_method(:enabled)

def setup(cache_path:, development_mode:, ignore_directories:)
def setup(cache_path:, development_mode:, ignore_directories:, readonly: false)
unless supported?
warn("[bootsnap/setup] Load path caching is not supported on this implementation of Ruby") if $VERBOSE
return
end

store = Store.new(cache_path)
store = Store.new(cache_path, readonly: readonly)

@loaded_features_index = LoadedFeaturesIndex.new

Expand Down
5 changes: 3 additions & 2 deletions lib/bootsnap/load_path_cache/store.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@ class Store
NestedTransactionError = Class.new(StandardError)
SetOutsideTransactionNotAllowed = Class.new(StandardError)

def initialize(store_path)
def initialize(store_path, readonly: false)
@store_path = store_path
@txn_mutex = Mutex.new
@dirty = false
@readonly = readonly
load_data
end

Expand Down Expand Up @@ -63,7 +64,7 @@ def mark_for_mutation!
end

def commit_transaction
if @dirty
if @dirty && !@readonly
dump_data
@dirty = false
end
Expand Down
31 changes: 31 additions & 0 deletions test/compile_cache_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@
class CompileCacheTest < Minitest::Test
include(TmpdirHelper)

def teardown
super
Bootsnap::CompileCache::Native.readonly = false
end

def test_compile_option_crc32
# Just assert that this works.
Bootsnap::CompileCache::Native.compile_option_crc32 = 0xffffffff
Expand Down Expand Up @@ -112,6 +117,32 @@ def test_recache_when_size_different
load(path)
end

def test_dont_store_cache_after_a_miss_when_readonly
Bootsnap::CompileCache::Native.readonly = true

path = Help.set_file("a.rb", "a = a = 3", 100)
output = RubyVM::InstructionSequence.compile_file(path)
Bootsnap::CompileCache::ISeq.expects(:input_to_storage).never
Bootsnap::CompileCache::ISeq.expects(:storage_to_output).never
Bootsnap::CompileCache::ISeq.expects(:input_to_output).once.returns(output)

load(path)
end

def test_dont_store_cache_after_a_stale_when_readonly
path = Help.set_file("a.rb", "a = a = 3", 100)
load(path)

Bootsnap::CompileCache::Native.readonly = true

output = RubyVM::InstructionSequence.compile_file(path)
Bootsnap::CompileCache::ISeq.expects(:input_to_storage).never
Bootsnap::CompileCache::ISeq.expects(:storage_to_output).once.returns(output)
Bootsnap::CompileCache::ISeq.expects(:input_to_output).never

load(path)
end

def test_invalid_cache_file
path = Help.set_file("a.rb", "a = a = 3", 100)
cp = Help.cache_path("#{@tmp_dir}-iseq", path)
Expand Down
6 changes: 6 additions & 0 deletions test/setup_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ def test_default_setup
compile_cache_yaml: true,
compile_cache_json: true,
ignore_directories: nil,
readonly: false,
)

Bootsnap.default_setup
Expand All @@ -39,6 +40,7 @@ def test_default_setup_with_ENV_not_dev
compile_cache_yaml: true,
compile_cache_json: true,
ignore_directories: nil,
readonly: false,
)

Bootsnap.default_setup
Expand All @@ -55,6 +57,7 @@ def test_default_setup_with_DISABLE_BOOTSNAP_LOAD_PATH_CACHE
compile_cache_yaml: true,
compile_cache_json: true,
ignore_directories: nil,
readonly: false,
)

Bootsnap.default_setup
Expand All @@ -71,6 +74,7 @@ def test_default_setup_with_DISABLE_BOOTSNAP_COMPILE_CACHE
compile_cache_yaml: false,
compile_cache_json: false,
ignore_directories: nil,
readonly: false,
)

Bootsnap.default_setup
Expand All @@ -94,6 +98,7 @@ def test_default_setup_with_BOOTSNAP_LOG
compile_cache_yaml: true,
compile_cache_json: true,
ignore_directories: nil,
readonly: false,
)
Bootsnap.expects(:logger=).with($stderr.method(:puts))

Expand All @@ -111,6 +116,7 @@ def test_default_setup_with_BOOTSNAP_IGNORE_DIRECTORIES
compile_cache_yaml: true,
compile_cache_json: true,
ignore_directories: %w[foo bar],
readonly: false,
)

Bootsnap.default_setup
Expand Down

0 comments on commit b51397f

Please sign in to comment.