Skip to content

Commit

Permalink
Merge pull request #771 from eregon/thread-safe-CachedEnvironment
Browse files Browse the repository at this point in the history
Fix thread safety of Sprockets::CachedEnvironment and Sprockets::Cache::MemoryStore
  • Loading branch information
byroot authored Dec 20, 2022
2 parents 1276b43 + 39490de commit cce2c0a
Show file tree
Hide file tree
Showing 8 changed files with 46 additions and 28 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ jobs:
include:
- { ruby: jruby, experimental: true }
- { ruby: jruby-head, experimental: true }
- { ruby: truffleruby, experimental: true }
- { ruby: truffleruby-head, experimental: true }
- { ruby: head, experimental: true }

steps:
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

Get upgrade notes from Sprockets 3.x to 4.x at https://github.com/rails/sprockets/blob/master/UPGRADING.md

- Fix thread safety of `Sprockets::CachedEnvironment` and `Sprockets::Cache::MemoryStore`. [#771](https://github.com/rails/sprockets/pull/771)
- Add support for Rack 3.0. Headers set by sprockets will now be lower case. [#758](https://github.com/rails/sprockets/pull/758)
- Make `Sprockets::Utils.module_include` thread safe on JRuby. [#759](https://github.com/rails/sprockets/pull/759)

Expand Down
31 changes: 20 additions & 11 deletions lib/sprockets/cache/memory_store.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ class MemoryStore
def initialize(max_size = DEFAULT_MAX_SIZE)
@max_size = max_size
@cache = {}
@mutex = Mutex.new
end

# Public: Retrieve value from cache.
Expand All @@ -32,12 +33,14 @@ def initialize(max_size = DEFAULT_MAX_SIZE)
#
# Returns Object or nil or the value is not set.
def get(key)
exists = true
value = @cache.delete(key) { exists = false }
if exists
@cache[key] = value
else
nil
@mutex.synchronize do
exists = true
value = @cache.delete(key) { exists = false }
if exists
@cache[key] = value
else
nil
end
end
end

Expand All @@ -50,24 +53,30 @@ def get(key)
#
# Returns Object value.
def set(key, value)
@cache.delete(key)
@cache[key] = value
@cache.shift if @cache.size > @max_size
@mutex.synchronize do
@cache.delete(key)
@cache[key] = value
@cache.shift if @cache.size > @max_size
end
value
end

# Public: Pretty inspect
#
# Returns String.
def inspect
"#<#{self.class} size=#{@cache.size}/#{@max_size}>"
@mutex.synchronize do
"#<#{self.class} size=#{@cache.size}/#{@max_size}>"
end
end

# Public: Clear the cache
#
# Returns true
def clear(options=nil)
@cache.clear
@mutex.synchronize do
@cache.clear
end
true
end
end
Expand Down
20 changes: 10 additions & 10 deletions lib/sprockets/cached_environment.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@ def initialize(environment)
initialize_configuration(environment)

@cache = environment.cache
@stats = {}
@entries = {}
@uris = {}
@processor_cache_keys = {}
@resolved_dependencies = {}
@stats = Concurrent::Map.new
@entries = Concurrent::Map.new
@uris = Concurrent::Map.new
@processor_cache_keys = Concurrent::Map.new
@resolved_dependencies = Concurrent::Map.new
end

# No-op return self as cached environment.
Expand All @@ -31,27 +31,27 @@ def cached

# Internal: Cache Environment#entries
def entries(path)
@entries.fetch(path){ @entries[path] = super(path) }
@entries.fetch_or_store(path) { super(path) }
end

# Internal: Cache Environment#stat
def stat(path)
@stats.fetch(path){ @stats[path] = super(path) }
@stats.fetch_or_store(path) { super(path) }
end

# Internal: Cache Environment#load
def load(uri)
@uris.fetch(uri){ @uris[uri] = super(uri) }
@uris.fetch_or_store(uri) { super(uri) }
end

# Internal: Cache Environment#processor_cache_key
def processor_cache_key(str)
@processor_cache_keys.fetch(str){ @processor_cache_keys[str] = super(str) }
@processor_cache_keys.fetch_or_store(str) { super(str) }
end

# Internal: Cache Environment#resolve_dependency
def resolve_dependency(str)
@resolved_dependencies.fetch(str){ @resolved_dependencies[str] = super(str) }
@resolved_dependencies.fetch_or_store(str) { super(str) }
end

private
Expand Down
2 changes: 1 addition & 1 deletion lib/sprockets/uri_utils.rb
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ def split_file_uri(uri)
path = path[1..-1]
end

[scheme, host, path, query]
[scheme, host || '', path, query]
end

# Internal: Join file: URI component parts into String.
Expand Down
2 changes: 2 additions & 0 deletions test/test_encoding_utils.rb
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ def test_fail_to_unmarshal_older_major_version
end

def test_fail_to_unmarshal_not_enough_data
skip "TypeError on TruffleRuby" if RUBY_ENGINE == "truffleruby"

assert_raises ArgumentError do
Marshal.load("")
end
Expand Down
4 changes: 4 additions & 0 deletions test/test_erb_processor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ def test_compile_erb_template_that_depends_on_env
old_env_value = ::ENV['ERB_ENV_TEST_VALUE']
::ENV['ERB_ENV_TEST_VALUE'] = 'success'

skip 'https://github.com/oracle/truffleruby/issues/2810' if RUBY_ENGINE == 'truffleruby'

root = File.expand_path("../fixtures", __FILE__)
environment = Sprockets::Environment.new(root)
environment.append_path 'default'
Expand All @@ -56,6 +58,8 @@ def test_compile_erb_template_that_depends_on_env
def test_compile_erb_template_that_depends_on_empty_env
old_env_value = ::ENV.delete('ERB_ENV_TEST_VALUE')

skip 'https://github.com/oracle/truffleruby/issues/2810' if RUBY_ENGINE == 'truffleruby'

root = File.expand_path("../fixtures", __FILE__)
environment = Sprockets::Environment.new(root)
environment.append_path 'default'
Expand Down
12 changes: 6 additions & 6 deletions test/test_uri_utils.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,23 +39,23 @@ def test_split_file_uri
assert_equal ['file', 'localhost', '/etc/fstab', nil], parts

parts = split_file_uri("file:///etc/fstab")
assert_equal ['file', nil, '/etc/fstab', nil], parts
assert_equal ['file', '', '/etc/fstab', nil], parts

parts = split_file_uri("file:///usr/local/bin/ruby%20on%20rails")
assert_equal ['file', nil, '/usr/local/bin/ruby on rails', nil], parts
assert_equal ['file', '', '/usr/local/bin/ruby on rails', nil], parts

parts = split_file_uri("file:///usr/local/var/github/app/assets/javascripts/application.js")
assert_equal ['file', nil, '/usr/local/var/github/app/assets/javascripts/application.js', nil], parts
assert_equal ['file', '', '/usr/local/var/github/app/assets/javascripts/application.js', nil], parts

if DOSISH
parts = split_file_uri("file:///C:/Documents%20and%20Settings/davris/FileSchemeURIs.doc")
assert_equal ['file', nil, 'C:/Documents and Settings/davris/FileSchemeURIs.doc', nil], parts
assert_equal ['file', '', 'C:/Documents and Settings/davris/FileSchemeURIs.doc', nil], parts

parts = split_file_uri("file:///D:/Program%20Files/Viewer/startup.htm")
assert_equal ['file', nil, 'D:/Program Files/Viewer/startup.htm', nil], parts
assert_equal ['file', '', 'D:/Program Files/Viewer/startup.htm', nil], parts

parts = split_file_uri("file:///C:/Program%20Files/Music/Web%20Sys/main.html?REQUEST=RADIO")
assert_equal ['file', nil, 'C:/Program Files/Music/Web Sys/main.html', 'REQUEST=RADIO'], parts
assert_equal ['file', '', 'C:/Program Files/Music/Web Sys/main.html', 'REQUEST=RADIO'], parts
end
end

Expand Down

0 comments on commit cce2c0a

Please sign in to comment.