Class: Wurk::Middleware::Chain

Inherits:
Object
  • Object
show all
Includes:
Enumerable
Defined in:
lib/wurk/middleware/chain.rb

Overview

Ordered list of middleware bound to a config/capsule. Sidekiq contract: add appends (removing any existing entry for the same klass), prepend pushes to index 0, insert_before/insert_after anchor relative to an existing entry, invoke walks the chain handing the inner block to each.

add has one opt-in exception, declared per middleware class: a class may name the classes it must stay OUTSIDE of in a class-level outer_of, and add then places it before the first of those anchors instead of at the tail. A class that declares no anchor keeps the plain append.

Entries store klass + ctor args + the config they were registered with, not the live object: every job gets a fresh instance per entry (invoke builds each as it reaches it, retrieve builds them all up front). This keeps middleware thread-safe by construction and matches Sidekiq's lifecycle.

Spec: docs/target/sidekiq-free.md §10.1.

Defined Under Namespace

Classes: Entry

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(config = nil, on_registration: nil) {|_self| ... } ⇒ Chain

Returns a new instance of Chain.

Yields:

  • (_self)

Yield Parameters:



28
29
30
31
32
33
# File 'lib/wurk/middleware/chain.rb', line 28

def initialize(config = nil, on_registration: nil)
  @config = config
  @on_registration = on_registration
  @entries = []
  yield self if block_given?
end

Instance Attribute Details

#config ⇒ Object

Returns the value of attribute config.



26
27
28
# File 'lib/wurk/middleware/chain.rb', line 26

def config
  @config
end

#entries ⇒ Object (readonly)

Returns the value of attribute entries.



25
26
27
# File 'lib/wurk/middleware/chain.rb', line 25

def entries
  @entries
end

Instance Method Details

#add(klass) ⇒ Object

Appends the middleware — the documented Sidekiq behaviour (spec §10.1): any existing entry for klass is removed and the new one lands at the tail (innermost).

One opt-in exception, declared per class: a middleware may name the classes it must stay OUTSIDE of in a class-level outer_of. When one of those anchors is registered, the entry is placed before the first of them instead of at the tail — so re-registering such a middleware cannot silently move it inside the middleware it depends on being outside of. A class that declares no anchor keeps the plain append.

An entry already registered before the anchor keeps its slot (only its ctor args are refreshed): moving it would be a relocation the caller did not ask for.



71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
# File 'lib/wurk/middleware/chain.rb', line 71

def add(klass, *)
  @on_registration&.call(klass)
  entry = make_entry(klass, *)
  anchors = anchor_classes(klass)
  existing = @entries.index { |e| e.klass == klass }
  anchor = @entries.index { |e| anchors.include?(e.klass) }

  # No anchor registered: remove the old entry, append at the tail.
  if anchor.nil?
    remove(klass)
    @entries << entry
    return @entries
  end

  # Already outside the anchor: keep the slot, refresh the entry.
  if existing && existing < anchor
    @entries[existing] = entry
    return @entries
  end

  # Registered inside the anchor (or not registered at all): move it
  # out, to just before the first anchor.
  remove(klass)
  @entries.insert(anchor, entry)
  @entries
end

#clear ⇒ Object



133
134
135
# File 'lib/wurk/middleware/chain.rb', line 133

def clear
  @entries.clear
end

#copy_for(capsule) ⇒ Object

Bind a duplicate of this chain to capsule. Entries carry their config, so each is duplicated and only the duplicate rebound: the capsule's middleware resolve the capsule while the parent's entries keep the parent config. Array#dup on @entries keeps the two chains independent after the first add/remove/etc.



42
43
44
45
46
47
48
49
50
51
# File 'lib/wurk/middleware/chain.rb', line 42

def copy_for(capsule)
  copy = dup
  copy.instance_variable_set(:@config, capsule)
  copy.entries.map! do |entry|
    rebound = entry.dup
    rebound.config = capsule
    rebound
  end
  copy
end

#each ⇒ Object



35
# File 'lib/wurk/middleware/chain.rb', line 35

def each(&) = @entries.each(&)

#empty? ⇒ Boolean

Returns:

  • (Boolean)


125
126
127
# File 'lib/wurk/middleware/chain.rb', line 125

def empty?
  @entries.empty?
end

#exists?(klass) ⇒ Boolean Also known as: include?

Returns:

  • (Boolean)


120
121
122
# File 'lib/wurk/middleware/chain.rb', line 120

def exists?(klass)
  any? { |entry| entry.klass == klass }
end

#insert_after(oldklass, newklass) ⇒ Object



112
113
114
115
116
117
118
# File 'lib/wurk/middleware/chain.rb', line 112

def insert_after(oldklass, newklass, *)
  @on_registration&.call(newklass)
  i = @entries.index { |entry| entry.klass == newklass }
  new_entry = i.nil? ? make_entry(newklass, *) : @entries.delete_at(i)
  i = @entries.index { |entry| entry.klass == oldklass } || (@entries.size - 1)
  @entries.insert(i + 1, new_entry)
end

#insert_before(oldklass, newklass) ⇒ Object



104
105
106
107
108
109
110
# File 'lib/wurk/middleware/chain.rb', line 104

def insert_before(oldklass, newklass, *)
  @on_registration&.call(newklass)
  i = @entries.index { |entry| entry.klass == newklass }
  new_entry = i.nil? ? make_entry(newklass, *) : @entries.delete_at(i)
  i = @entries.index { |entry| entry.klass == oldklass } || 0
  @entries.insert(i, new_entry)
end

#invoke(*args, &block) ⇒ Object

Walks the chain inside-out. Each middleware receives a block that advances to the next; the innermost block is the caller's &block, whose return value is propagated back out. Empty chain: yield.

Hot path — one job pays this per invocation, so instantiation is folded into an index walk over @entries: no instance array from retrieve, no traversal lambda, and no shift (which memmoves the array per entry). Cost is one allocation per entry (the middleware itself), zero to walk.

Raises:

  • (ArgumentError)


145
146
147
148
149
150
# File 'lib/wurk/middleware/chain.rb', line 145

def invoke(*args, &block)
  raise ArgumentError, 'middleware chain requires a block' unless block
  return yield if @entries.empty?

  traverse(@entries, 0, args, &block)
end

#prepend(klass) ⇒ Object



98
99
100
101
102
# File 'lib/wurk/middleware/chain.rb', line 98

def prepend(klass, *)
  @on_registration&.call(klass)
  remove(klass)
  @entries.unshift(make_entry(klass, *))
end

#remove(klass) ⇒ Object



53
54
55
# File 'lib/wurk/middleware/chain.rb', line 53

def remove(klass)
  @entries.delete_if { |entry| entry.klass == klass }
end

#retrieve ⇒ Object



129
130
131
# File 'lib/wurk/middleware/chain.rb', line 129

def retrieve
  map(&:make_new)
end