Module: RactorRailsShim::MarcelPatch

Defined in:
lib/ractor_rails_shim/patches/marcel.rb

Overview

Marcel ships a handful of class/singleton methods that are implemented with blocks (each { }, map { }, reduce { }, &block, etc.). Those blocks are compiled in the main Ractor, so any worker Ractor that calls into Marcel (ActiveStorage uses it for content-type detection when attaching files) blows up with "defined with an un-shareable Proc in a different Ractor".

We also freeze Marcel's global lookup tables (EXTENSIONS / MAGIC / TYPE_EXTS / TYPE_PARENTS) so they are Ractor-shareable.

Both fixes are applied by re-defining the methods with string-eval defs that contain no blocks, and by making every Marcel constant shareable.

Class Method Summary collapse

Class Method Details

.apply!Object



18
19
20
21
22
# File 'lib/ractor_rails_shim/patches/marcel.rb', line 18

def apply!
  make_constants_shareable!
  patch_mime_type!
  patch_magic!
end

.make_constants_shareable!Object

Freeze every top-level Marcel constant (and the nested arrays/hashes) so the lookup tables can be transferred into worker Ractors.



26
27
28
29
30
31
32
33
34
35
36
# File 'lib/ractor_rails_shim/patches/marcel.rb', line 26

def make_constants_shareable!
  return unless defined?(::Marcel)

  ::Marcel.constants(false).each do |name|
    obj = ::Marcel.const_get(name)
    Ractor.make_shareable(obj) if Ractor.respond_to?(:make_shareable) && !Ractor.shareable?(obj)
    ::Marcel.const_set(name, obj)
  rescue StandardError
    nil
  end
end

.patch_magic!Object

Override Magic.add / remove / all_by_magic / child? / magic_match without blocks.



80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
# File 'lib/ractor_rails_shim/patches/marcel.rb', line 80

def patch_magic!
  return unless defined?(::Marcel::Magic)

  ::Marcel::Magic.singleton_class.class_eval <<-RUBY, __FILE__, __LINE__ + 1
    def add(type, options)
      extensions = [options[:extensions]].flatten.compact
      ::Marcel::TYPE_EXTS[type] = extensions
      parents = [options[:parents]].flatten.compact
      ::Marcel::TYPE_PARENTS[type] = parents unless parents.empty?
      i = 0
      while i < extensions.length
        ::Marcel::EXTENSIONS[extensions[i]] = type
        i += 1
      end
      ::Marcel::MAGIC.unshift [type, options[:magic]] if options[:magic]
    end

    def remove(type)
      ext_keys = ::Marcel::EXTENSIONS.keys
      i = 0
      while i < ext_keys.length
        k = ext_keys[i]
        ::Marcel::EXTENSIONS.delete(k) if ::Marcel::EXTENSIONS[k] == type
        i += 1
      end
      j = 0
      while j < ::Marcel::MAGIC.length
        if ::Marcel::MAGIC[j][0] == type
          ::Marcel::MAGIC.delete_at(j)
        else
          j += 1
        end
      end
      ::Marcel::TYPE_EXTS.delete(type)
      ::Marcel::TYPE_PARENTS.delete(type)
    end

    def all_by_magic(io)
      matches = magic_match(io, :select)
      result = []
      i = 0
      while i < matches.length
        result << new(matches[i][0])
        i += 1
      end
      result
    end

    def child?(child, parent)
      return true if child == parent
      parents = ::Marcel::TYPE_PARENTS[child]
      return false unless parents
      i = 0
      while i < parents.length
        return true if child?(parents[i], parent)
        i += 1
      end
      false
    end

    def magic_match(io, method)
      return magic_match(::StringIO.new(io.to_s), method) unless io.respond_to?(:read)
      buffer = "".b
      if method == :find
        i = 0
        while i < ::Marcel::MAGIC.length
          entry = ::Marcel::MAGIC[i]
          return [entry[0], entry[1]] if magic_match_io(io, entry[1], buffer)
          i += 1
        end
        nil
      else
        result = []
        i = 0
        while i < ::Marcel::MAGIC.length
          entry = ::Marcel::MAGIC[i]
          result << [entry[0], entry[1]] if magic_match_io(io, entry[1], buffer)
          i += 1
        end
        result
      end
    end

    def magic_match_io(io, matches, buffer)
      i = 0
      while i < matches.length
        offset, value, children = matches[i]
        match = nil
        if value
          if value.is_a?(::Regexp)
            match = match_regex(io, offset, value, buffer)
          elsif ::Range === offset
            io.read(offset.begin, buffer)
            x = io.read(offset.end - offset.begin + value.bytesize, buffer)
            match = x && x.include?(value)
          else
            io.read(offset, buffer)
            match = io.read(value.bytesize, buffer) == value
          end
        end
        io.rewind
        return true if match && (!children || magic_match_io(io, children, buffer))
        i += 1
      end
      false
    end
  RUBY
end

.patch_mime_type!Object

Override MimeType.for_data / with_io / most_specific_type without blocks.



39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
# File 'lib/ractor_rails_shim/patches/marcel.rb', line 39

def patch_mime_type!
  return unless defined?(::Marcel::MimeType)

  ::Marcel::MimeType.singleton_class.class_eval <<-RUBY, __FILE__, __LINE__ + 1
    def for_data(pathname_or_io)
      if pathname_or_io
        io = with_io(pathname_or_io)
        begin
          if magic = ::Marcel::Magic.by_magic(io)
            magic.type.downcase
          end
        ensure
          io.close if defined?(::Pathname) && pathname_or_io.is_a?(::Pathname)
        end
      end
    end

    def with_io(pathname_or_io)
      if defined?(::Pathname) && pathname_or_io.is_a?(::Pathname)
        pathname_or_io.open
      else
        pathname_or_io
      end
    end

    def most_specific_type(*candidates)
      arr = candidates.compact.uniq
      return nil if arr.empty?
      type = arr[0]
      i = 1
      while i < arr.length
        candidate = arr[i]
        type = ::Marcel::Magic.child?(candidate, type) ? candidate : type
        i += 1
      end
      type
    end
  RUBY
end