leptris 1.9.280.0 → 1.9.280.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f047f95472eb2eb6a147568db650edcbdcbe77e8411004258ea6ea6df30d52c7
4
- data.tar.gz: e634e41351c2f5889c70fc378c839342e91737b844ef91bd359e920205a4b5d4
3
+ metadata.gz: f8c9a359271bb60e62c4a6fb8e210c725d02b332df53f804411498c82acd1056
4
+ data.tar.gz: 5e196178a59975549b08e1c3b54c8d5377d463d430ad8bceef5df410a5ea4e83
5
5
  SHA512:
6
- metadata.gz: b31e125da3457aedab626e6cc7762b2a04c25d68e5f3d8fa63250a7ba979e786686e810525cd39146aaf0a032b8638e42582619e4d5ffd0e1ad6481f9ca0b389
7
- data.tar.gz: 1cf1e08be3e722f0d29fa4f278dc3513f9b11bf9c43e59c6d2bce5f33ee8dcd853b9e91cb2a92af7103f495884bc9dd6bd8d7bb174ae61812eb4233c67e4f13e
6
+ metadata.gz: f4bbbdb0079f57fb234636a4ec03000a44a67ac6f80ea852fc4ba3bac9c0faddce6231900f137c8c9596369f4ea552120be792670da103f5169f2d796c891a56
7
+ data.tar.gz: 6a96c92cba79412fd69d32bc33482cd8b208808ded2b7d554539f7322647a9c6852e1531342fd428f7fe5826a94ccbfc428e4d094faca3d251dcdb2bddfb6aec
data/CHANGELOG.md CHANGED
@@ -1,3 +1,60 @@
1
+ ## [1.9.280.1] - 2026-09-30
2
+
3
+ ### Added
4
+
5
+ - Ractor support (#ractor): documents can be parsed, queried,
6
+ mutated, and GC-freed inside child ractors, end to end. Five
7
+ blockers cleared:
8
+ - The native ext declares `rb_ext_ractor_safe(true)` — it keeps
9
+ no unsynchronized shared mutable state (class caches are
10
+ GC-marked immortals; wrapper hashes mutate only along the
11
+ engine's per-document path).
12
+ - The UTF8_STRING converter is a MODULE, not a
13
+ Class.new instance: the instance form deadlocks the first
14
+ mapped conversion inside a child Ractor; singleton methods
15
+ on a module dispatch cleanly.
16
+ - The legacy `UTF8_RETURNS` re-wrapper is gone. It redefined
17
+ every string-returning attach through a `method(name)`
18
+ capture (`define_singleton_method` with a Proc defined in
19
+ the loading ractor) — exactly the shape that raises
20
+ "defined with an un-shareable Proc in a different Ractor" in
21
+ a child. The UTF8_STRING converter already tags those 26
22
+ functions' results UTF-8 (a double no-op), so the wrapper
23
+ was deleted, not ported.
24
+ - windows-11-arm only: ffi attaches aarch64-mingw-ucrt
25
+ functions through un-shareable Proc wrappers (its libffi
26
+ #905 workaround), breaking every attached call from a child
27
+ Ractor there. `Leptris::XML::FFI` now freezes itself on that
28
+ platform — ffi's `Library#freeze` redefines the wrappers via
29
+ `FFI.shareable_proc` (verified by a full suite run with the
30
+ freeze forced on; it is a no-op elsewhere). ffi's freeze
31
+ redemption itself needs `Ractor.shareable_proc` (Ruby 3.5+),
32
+ so on that platform with older Rubies the ractor specs skip
33
+ — attached calls from child ractors are impossible there
34
+ until the runtime catches up.
35
+ - `Searchable`'s compiled-expression and CSS translation
36
+ caches moved from module ivars to `Ractor.current[]`-keyed
37
+ storage: the old shape raises `Ractor::IsolationError` from
38
+ non-main ractors, the compiled FFI handles could never
39
+ cross a boundary anyway, and in the main ractor the sharing
40
+ is unchanged (one cache across its threads). Engines without
41
+ Ractor keep the ivar.
42
+ - Note for ractor users: a child ractor's autoload requests
43
+ are serviced by the main ractor, and a main blocked in
44
+ `Ractor#take` is not schedulable — under load that pairing can
45
+ wedge (CRuby ractor-barrier race). Exercise the API once in
46
+ the main ractor (or `Leptris::XML.parse` something trivial)
47
+ before spawning ractors against a cold load.
48
+
49
+ ### Performance
50
+
51
+ - No measurable cost on the serial path: same attaches, same
52
+ converter, cache lookups trade one ivar read for a storage
53
+ read. Parallel parsing scales near-linearly instead — 4
54
+ ractors parse 4x20 documents in ~1/3 the serial time (0.06s
55
+ vs 0.18s on 4 cores).
56
+
57
+
1
58
  ## [1.9.279.0] - 2026-09-30
2
59
 
3
60
  ### Fixed
@@ -2937,6 +2937,12 @@ LEPTRIS_INIT_EXPORT void Init_native(void)
2937
2937
  {
2938
2938
  VALUE m_leptris, m_xml, m_native;
2939
2939
 
2940
+ /* Ractor support: this ext keeps no unsynchronized shared mutable
2941
+ * state — class caches are GC-marked immortals (never freed), the
2942
+ * per-document wrapper hashes are keyed by document and every
2943
+ * entry mutates only under the engine's document path. */
2944
+ rb_ext_ractor_safe(true);
2945
+
2940
2946
  m_leptris = rb_define_module("Leptris");
2941
2947
  m_xml = rb_define_module_under(m_leptris, "XML");
2942
2948
  c_native_node = rb_define_class_under(m_xml, "NativeNode", rb_cObject);
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Leptris
4
- VERSION = "1.9.280.0"
4
+ VERSION = "1.9.280.1"
5
5
  end
@@ -13,21 +13,30 @@ module Leptris
13
13
  # therefore uses this converter: same NUL-terminated copy
14
14
  # semantics as :string, tagged UTF-8. Input arguments keep
15
15
  # plain :string (Ruby-to-C reads bytes regardless of tag).
16
- UTF8_STRING = Class.new do
17
- include ::FFI::DataConverter
18
-
19
- def from_native(ptr, _ctx)
16
+ # A MODULE, not a Class.new instance: the instance form
17
+ # (include DataConverter + tap) deadlocks the first mapped
18
+ # conversion inside a child Ractor, while singleton methods
19
+ # on a module dispatch cleanly (#ractor).
20
+ UTF8_STRING = Module.new do
21
+ extend ::FFI::DataConverter
22
+
23
+ def self.from_native(ptr, _ctx)
20
24
  return nil if ptr.null? || ptr.address.zero?
21
25
 
22
26
  ptr.read_string.force_encoding(Encoding::UTF_8)
23
27
  end
24
28
 
25
- def to_native(value, _ctx)
29
+ def self.to_native(value, _ctx)
26
30
  return nil if value.nil?
27
31
 
28
32
  ::FFI::MemoryPointer.from_string(value.to_s)
29
33
  end
30
- end.new.tap { |c| c.native_type(::FFI::Type::POINTER) }
34
+
35
+ def self.native_type(t = nil)
36
+ t ? (@native_type = t) : (@native_type ||= ::FFI::Type::POINTER)
37
+ end
38
+ end
39
+
31
40
 
32
41
  # The per-OS vendor directory of the ruby-platform gem
33
42
  # (zero-setup TruffleRuby/JRuby, #160): binaries for the
@@ -1392,40 +1401,6 @@ attach_function :leptris_parse_string,
1392
1401
  # XML_PARSE_DTDATTR opt-in parity.
1393
1402
  LEPTRIS_PARSE_DTDATTR = 2
1394
1403
 
1395
- # The headers' contract is UTF-8 for every C string, but FFI's
1396
- # read_string hands back ASCII-8BIT — the platform default
1397
- # leaks through the seam. Every string-returning attached
1398
- # function this binding CALLS is wrapped here so its result
1399
- # arrives as UTF-8; mirror-only attachments stay raw (add the
1400
- # name here when a call site starts using its string).
1401
- UTF8_RETURNS = %i[
1402
- leptris_version
1403
- leptris_status_string leptris_last_error
1404
- leptris_document_last_error leptris_document_encoding
1405
- leptris_document_pi_target leptris_document_pi_data
1406
- leptris_document_comment_content
1407
- leptris_element_name leptris_element_text leptris_element_prefix
1408
- leptris_element_namespace
1409
- leptris_element_attribute leptris_element_attribute_ns
1410
- leptris_attribute_get_name leptris_attribute_get_value
1411
- leptris_attribute_namespace_uri
1412
- leptris_element_namespace_decl_prefix
1413
- leptris_element_namespace_decl_uri
1414
- leptris_text_node_get_content leptris_comment_node_get_content
1415
- leptris_cdata_node_get_content
1416
- leptris_pi_node_get_target leptris_pi_node_get_data
1417
- leptris_pull_attr_name leptris_pull_attr_value
1418
- ].freeze
1419
- private_constant :UTF8_RETURNS
1420
-
1421
- UTF8_RETURNS.each do |name|
1422
- raw = method(name)
1423
- define_singleton_method(name) do |*args|
1424
- str = raw.call(*args)
1425
- str.nil? ? nil : str.force_encoding(Encoding::UTF_8)
1426
- end
1427
- end
1428
-
1429
1404
  # Reads an libleptris-owned char* result and frees it as one unit,
1430
1405
  # so a call site can neither leak nor double-free.
1431
1406
  def self.read_owned_string(ptr)
@@ -1731,6 +1706,15 @@ attach_function :leptris_parse_string,
1731
1706
  end
1732
1707
  ptr
1733
1708
  end
1709
+
1710
+ # ffi attaches aarch64-mingw-ucrt functions through un-shareable
1711
+ # Proc wrappers (the libffi #905 workaround in ffi/function.rb),
1712
+ # which breaks every attached call from a child Ractor on
1713
+ # windows-11-arm. Library#freeze redefines those wrappers via
1714
+ # FFI.shareable_proc — the ffi gem's own Ractor redemption path
1715
+ # for this platform (#ractor). Everything is attached above, at
1716
+ # load time, so freezing closes no live door.
1717
+ freeze if RUBY_PLATFORM == "aarch64-mingw-ucrt"
1734
1718
  end
1735
1719
  end
1736
1720
  end
@@ -14,8 +14,20 @@ module Leptris::XML::Searchable
14
14
  # evaluations keep their dedicated string entries.
15
15
  COMPILED_CACHE_LIMIT = 64
16
16
 
17
+ # Cache storage (Ractor): module/class ivars are off-limits to
18
+ # non-main ractors (Ractor::IsolationError), and the compiled
19
+ # handles are FFI pointers that can never cross a ractor boundary
20
+ # anyway. Ractor#[] storage preserves the old sharing shape in
21
+ # the main ractor (one cache across its threads) and gives every
22
+ # child ractor its own. Engines without Ractor keep the ivar.
23
+ def self.cache_store(name)
24
+ return (@cache_stores ||= {})[name] ||= {} unless defined?(Ractor)
25
+
26
+ Ractor.current[name] ||= {}
27
+ end
28
+
17
29
  def self.compiled_expression(expr)
18
- cache = (@compiled_expressions ||= {})
30
+ cache = cache_store(:compiled_expressions)
19
31
  if (hit = cache[expr])
20
32
  cache.delete(expr)
21
33
  cache[expr] = hit # LRU refresh
@@ -34,7 +46,7 @@ module Leptris::XML::Searchable
34
46
  # it every css call re-ran the regex machinery before the
35
47
  # compiled-expression cache could hit. Same bounded-LRU shape.
36
48
  def self.css_expression(selector, prefix)
37
- cache = (@css_expressions ||= {})
49
+ cache = cache_store(:css_expressions)
38
50
  key = "#{prefix}#{selector}"
39
51
  if (hit = cache[key])
40
52
  cache.delete(key)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: leptris
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.9.280.0
4
+ version: 1.9.280.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.