stampeded 0.3.1-aarch64-linux-gnu

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.
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stampeded
4
+ module ContinuableExt
5
+ PROGRESS_EVERY = 1.0
6
+
7
+ def checkpoint!
8
+ stampeded_save_progress(force: queue_adapter.stopping?) if Current.context
9
+ super
10
+ end
11
+
12
+ private
13
+
14
+ def resume_job(exception)
15
+ return super unless Current.context
16
+
17
+ stampeded_save_progress(force: true)
18
+ raise exception unless exception.is_a?(ActiveJob::Continuation::Interrupt)
19
+
20
+ if max_resumptions && resumptions >= max_resumptions
21
+ raise ActiveJob::Continuation::ResumeLimitError, "Job was resumed a maximum of #{max_resumptions} times"
22
+ end
23
+
24
+ raise Requeue, exception.message
25
+ end
26
+
27
+ def stampeded_save_progress(force:)
28
+ progress = continuation.to_h
29
+ now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
30
+ fresh = progress["completed"] == @stampeded_saved_steps && now - (@stampeded_saved_at || -PROGRESS_EVERY) < PROGRESS_EVERY
31
+ return if fresh && !force
32
+
33
+ Current.context.checkpoint!("activejob" => { "continuation" => progress, "resumptions" => resumptions })
34
+ @stampeded_saved_steps = progress["completed"]
35
+ @stampeded_saved_at = now
36
+ end
37
+ end
38
+ end
39
+
40
+ ActiveSupport.on_load(:active_job_continuable) { prepend Stampeded::ContinuableExt }
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stampeded
4
+ module Current
5
+ KEY = :__stampeded_job_context
6
+
7
+ module_function
8
+
9
+ def context = Thread.current[KEY]
10
+
11
+ def with(context)
12
+ previous = Thread.current[KEY]
13
+ Thread.current[KEY] = context
14
+ yield
15
+ ensure
16
+ Thread.current[KEY] = previous
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stampeded
4
+ module Daemon
5
+ BINARY = "stampeded"
6
+
7
+ module_function
8
+
9
+ def platform
10
+ %i[cpu os].map { |m| Gem::Platform.local.public_send(m) }.join("-")
11
+ end
12
+
13
+ def executable
14
+ if (dir = ENV.fetch("STAMPEDED_INSTALL_DIR", nil))
15
+ path = File.expand_path(File.join(dir, BINARY))
16
+ raise DaemonNotFound, "STAMPEDED_INSTALL_DIR is set to #{dir}, but #{path} is not executable" \
17
+ unless File.executable?(path)
18
+
19
+ return path
20
+ end
21
+
22
+ path = bundled_path
23
+ return path if path && File.executable?(path)
24
+
25
+ raise DaemonNotFound, <<~MSG
26
+ Could not find the stampeded daemon: there is no build for #{platform}.
27
+ Build it and point at it:
28
+
29
+ cargo build --release --bin stampeded
30
+ export STAMPEDED_INSTALL_DIR=/path/to/stampeded/target/release
31
+
32
+ The daemon is OPTIONAL: without it jobs still run, with no scheduler,
33
+ reaper, pruner or dashboard.
34
+ MSG
35
+ end
36
+
37
+ def bundled_path
38
+ File.expand_path(File.join("../..", "libexec", BINARY), __dir__)
39
+ end
40
+
41
+ def available?
42
+ !executable.nil?
43
+ rescue DaemonNotFound
44
+ false
45
+ end
46
+
47
+ def exec!(*args)
48
+ Kernel.exec(executable, *args)
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stampeded
4
+ module Envelope
5
+ module_function
6
+
7
+ def tasks
8
+ @tasks ||= {}
9
+ end
10
+
11
+ def register(task, job_class)
12
+ name = task.to_s
13
+ raise ArgumentError, "a task name cannot be blank" if name.strip.empty?
14
+
15
+ owner = tasks[name]
16
+ if owner && owner != job_class.name
17
+ raise ArgumentError, "task #{name.inspect} is already #{owner}'s; #{job_class.name} cannot claim it too"
18
+ end
19
+
20
+ tasks[name] = job_class.name
21
+ end
22
+
23
+ def from_active_job(job)
24
+ task = job.class.try(:stampeded_task)
25
+ context = job.serialize.except("arguments", "provider_job_id")
26
+ kwargs = standard_arguments(job.arguments)
27
+
28
+ if kwargs
29
+ [task || job.class.name, kwargs, { "activejob" => context }]
30
+ elsif task
31
+ raise PortableArgumentsError.new(job.class.name, task)
32
+ else
33
+ context["arguments"] = ActiveJob::Arguments.serialize(job.arguments)
34
+ [job.class.name, {}, { "activejob" => context }]
35
+ end
36
+ end
37
+
38
+ def to_active_job_payload(id:, queue:, task:, args:, meta:, checkpoint: nil)
39
+ context = meta.fetch("activejob", {})
40
+ job_class = context["job_class"] || tasks[task] || task
41
+
42
+ arguments =
43
+ context.fetch("arguments") do
44
+ kwargs = args.transform_keys(&:to_sym)
45
+ kwargs.empty? ? [] : ActiveJob::Arguments.serialize([Hash.ruby2_keywords_hash(kwargs)])
46
+ end
47
+
48
+ context.merge(
49
+ "job_class" => job_class,
50
+ "job_id" => context["job_id"] || id,
51
+ "queue_name" => queue,
52
+ "arguments" => arguments,
53
+ "executions" => context["executions"] || 0,
54
+ "exception_executions" => context["exception_executions"] || {},
55
+ "enqueued_at" => context["enqueued_at"] || Time.now.utc.iso8601
56
+ ).merge(continuation(checkpoint))
57
+ end
58
+
59
+ def continuation(checkpoint)
60
+ return {} unless checkpoint.is_a?(Hash) && checkpoint["activejob"].is_a?(Hash)
61
+
62
+ checkpoint["activejob"].slice("continuation", "resumptions")
63
+ end
64
+
65
+ def standard_arguments(arguments)
66
+ return {} if arguments.empty?
67
+ return nil unless arguments.size == 1
68
+
69
+ kwargs = arguments.first
70
+ return nil unless kwargs.is_a?(Hash) && Hash.ruby2_keywords_hash?(kwargs)
71
+ return nil unless plain?(kwargs)
72
+
73
+ JSON.parse(JSON.generate(kwargs))
74
+ end
75
+
76
+ def plain?(value)
77
+ case value
78
+ when String, Integer, true, false, nil then true
79
+ when Float then value.finite?
80
+ when Array then value.all? { plain?(it) }
81
+ when Hash then value.all? { |k, v| (k.is_a?(String) || k.is_a?(Symbol)) && plain?(v) }
82
+ else false
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stampeded
4
+ Error = Class.new(StandardError)
5
+
6
+ class RetryConflict < Error
7
+ def initialize(job_class)
8
+ super(<<~MSG)
9
+ #{job_class} declares `retry_on`; Stampeded owns retries:
10
+
11
+ class #{job_class} < ApplicationJob
12
+ stampeded max_attempts: 5, backoff_base_ms: 1_000, backoff_max_ms: 300_000
13
+ end
14
+
15
+ For a specific delay: raise Stampeded::RetryIn.new(seconds). `discard_on` is fine.
16
+ MSG
17
+ end
18
+ end
19
+
20
+ class PortableArgumentsError < Error
21
+ def initialize(job_class, task)
22
+ super(<<~MSG)
23
+ #{job_class} is task #{task.inspect}: pass keyword arguments with
24
+ plain JSON values (strings, numbers, booleans, nil, arrays, hashes),
25
+ ids rather than records, ISO 8601 strings rather than times:
26
+
27
+ #{job_class}.perform_later(document_id: document.id)
28
+ MSG
29
+ end
30
+ end
31
+
32
+ class DaemonNotFound < Error; end
33
+ class LibraryNotFound < Error; end
34
+ class SchemaMissing < Error
35
+ def initialize(cause = nil)
36
+ super(<<~MSG)
37
+ The Stampeded schema is not installed in this database.
38
+
39
+ In Rails:
40
+
41
+ bin/rails stampeded:install
42
+
43
+ Elsewhere:
44
+
45
+ Stampeded::Schema.install!
46
+
47
+ Postgres 18 or newer is required.
48
+ #{"\nUnderlying error: #{cause.message.lines.first&.strip}" if cause}
49
+ MSG
50
+ end
51
+ end
52
+
53
+ class UnknownTask < Error; end
54
+
55
+ class Requeue < Exception; end
56
+ class RetryIn < Error
57
+ attr_reader :seconds
58
+
59
+ def initialize(seconds, message = nil)
60
+ @seconds = Float(seconds)
61
+ raise ArgumentError, "RetryIn needs a non-negative delay, got #{seconds.inspect}" if @seconds.negative?
62
+
63
+ super(message || "retry in #{@seconds}s")
64
+ end
65
+
66
+ def delay_ms = (@seconds * 1000).round
67
+ end
68
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "time"
5
+
6
+ module Stampeded
7
+ class Job
8
+ ATTRIBUTES = %w[
9
+ id queue task args meta state priority attempt max_attempts
10
+ interruptions max_interruptions deferrals max_deferrals
11
+ tenant_id destination dead_letter_queue expire_seconds
12
+ scheduled_at last_error failure_kind
13
+ ].freeze
14
+
15
+ attr_reader :row
16
+
17
+ def initialize(row, decoded: false)
18
+ @row = row
19
+ @decoded = decoded
20
+ end
21
+
22
+ def id = row["id"]
23
+ def queue = row["queue"]
24
+ def task = row["task"]
25
+ def state = row["state"]
26
+ def terminal? = Lifecycle.terminal?(state)
27
+ def claimable? = Lifecycle.claimable?(state)
28
+
29
+ def available_events = Lifecycle.events_from(state)
30
+
31
+ def respond_to_missing?(name, include_private = false) = state_predicate?(name) || super
32
+
33
+ def method_missing(name, *args)
34
+ return super unless args.empty? && state_predicate?(name)
35
+
36
+ state.to_s == name.to_s.delete_suffix("?")
37
+ end
38
+ def attempt = row["attempt"].to_i
39
+ def max_attempts = row["max_attempts"].to_i
40
+ def interruptions = row["interruptions"].to_i
41
+ def deferrals = row["deferrals"].to_i
42
+ def tenant_id = row["tenant_id"]
43
+ def destination = row["destination"]
44
+
45
+ def args
46
+ @args ||= decode(row["args"])
47
+ end
48
+
49
+ def meta
50
+ @meta ||= decode(row["meta"])
51
+ end
52
+
53
+ def checkpoint
54
+ return @checkpoint if defined?(@checkpoint)
55
+
56
+ raw = row["checkpoint"]
57
+ @checkpoint = raw.nil? ? nil : decode(raw)
58
+ end
59
+
60
+ def resumed? = !checkpoint.nil?
61
+
62
+ def checkpoint_seq = row["checkpoint_seq"].to_i
63
+ def attempts_remaining = [max_attempts - attempt, 0].max
64
+
65
+ def last_attempt? = attempts_remaining.zero?
66
+
67
+ def to_h = ATTRIBUTES.to_h { |a| [a, row[a]] }
68
+
69
+ def inspect
70
+ "#<Stampeded::Job #{id} #{queue}/#{task} attempt=#{attempt}/#{max_attempts}>"
71
+ end
72
+
73
+ private
74
+
75
+ def state_predicate?(name) = name.end_with?("?") && Lifecycle.states.include?(name.to_s.delete_suffix("?").to_sym)
76
+
77
+ def decode(value)
78
+ case value
79
+ when nil then {}
80
+ when String then @decoded ? value : JSON.parse(value)
81
+ else value
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Stampeded
6
+ class JobContext
7
+ extend Forwardable
8
+ def_delegators :@job, :id, :queue, :task, :args, :meta, :attempt, :max_attempts,
9
+ :attempts_remaining, :last_attempt?, :tenant_id, :destination,
10
+ :checkpoint, :resumed?, :checkpoint_seq, :row
11
+
12
+ attr_reader :job, :worker_id
13
+
14
+ def initialize(job, worker_id)
15
+ @job = job
16
+ @worker_id = worker_id
17
+ end
18
+
19
+ def checkpoint!(state = nil, conn: nil, **rest)
20
+ state ||= rest
21
+ raise ArgumentError, "checkpoint! needs some state" if state.nil? || state.empty?
22
+
23
+ sql = "SELECT stampeded_checkpoint($1, $2, $3, $4, $5)"
24
+ params = [job.queue, job.id, worker_id, job.attempt, JSON.generate(state)]
25
+
26
+ seq =
27
+ if conn
28
+ conn.exec_params(sql, params)[0]["stampeded_checkpoint"]
29
+ else
30
+ Connection.with { |c| c.exec_params(sql, params)[0]["stampeded_checkpoint"] }
31
+ end
32
+
33
+ !seq.nil?
34
+ end
35
+
36
+ def steps
37
+ @steps ||= Steps.new(self)
38
+ end
39
+
40
+ def step(name, cursor: nil, &) = steps.step(name, cursor: cursor, &)
41
+ def touch!
42
+ ok = Connection.own do |c|
43
+ c.exec_params("SELECT stampeded_touch($1, $2, $3, $4)",
44
+ [job.queue, job.id, worker_id, job.attempt])[0]["stampeded_touch"]
45
+ end
46
+ Stampeded.truthy?(ok)
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Stampeded
4
+ module Lifecycle
5
+ module_function
6
+
7
+ def states = @states ||= native.lifecycle_states
8
+
9
+ def events = @events ||= native.lifecycle_events
10
+
11
+ def apply(from, event) = native.lifecycle_apply(from, event)
12
+
13
+ def legal?(from, event)
14
+ !apply(from, event).nil?
15
+ rescue Error
16
+ false
17
+ end
18
+
19
+ def events_from(state) = events.select { legal?(state, it) }
20
+
21
+ def terminal?(state) = events_from(state).empty?
22
+
23
+ def claimable?(state) = legal?(state, :claim)
24
+
25
+ def native
26
+ require "stampeded/native"
27
+ Native
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,233 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ffi"
4
+ require "json"
5
+
6
+ module Stampeded
7
+ module Native
8
+ extend FFI::Library
9
+
10
+ OK = 0
11
+ NONE = 1
12
+ ERROR = -1
13
+
14
+ LIBRARY = FFI.map_library_name("stampeded_ffi")
15
+
16
+ def self.library_path
17
+ if (dir = ENV.fetch("STAMPEDED_INSTALL_DIR", nil))
18
+ path = File.expand_path(File.join(dir, LIBRARY))
19
+ raise LibraryNotFound, "STAMPEDED_INSTALL_DIR is set to #{dir}, but #{path} does not exist" \
20
+ unless File.file?(path)
21
+
22
+ return path
23
+ end
24
+
25
+ path = File.expand_path(File.join("../..", "libexec", LIBRARY), __dir__)
26
+ return path if File.file?(path)
27
+
28
+ raise LibraryNotFound, <<~MSG
29
+ Could not find #{LIBRARY} for #{Daemon.platform}. Build it and point at it:
30
+
31
+ cargo build --release -p stampeded-ffi
32
+ export STAMPEDED_INSTALL_DIR=/path/to/stampeded/target/release
33
+ MSG
34
+ end
35
+
36
+ ffi_lib library_path
37
+
38
+ class PullerOptions < FFI::Struct
39
+ layout :queues, :pointer,
40
+ :except_queues, :pointer,
41
+ :max_leases, :uint32,
42
+ :batch_size, :uint32,
43
+ :poll_interval_ms, :uint32,
44
+ :heartbeat_interval_ms, :uint32
45
+ end
46
+
47
+ attach_function :stampeded_version, [], :string
48
+ attach_function :stampeded_last_error, [], :string
49
+ attach_function :stampeded_log_init, [:string], :int32
50
+
51
+ attach_function :stampeded_connect, [:string], :pointer, blocking: true
52
+ attach_function :stampeded_client_free, [:pointer], :void, blocking: true
53
+
54
+ attach_function :stampeded_puller_start, [:pointer, PullerOptions.by_ref], :pointer, blocking: true
55
+ attach_function :stampeded_puller_worker_id, [:pointer], :string
56
+ attach_function :stampeded_puller_next, %i[pointer uint32 pointer], :int32, blocking: true
57
+ attach_function :stampeded_puller_stop, %i[pointer uint32], :int32, blocking: true
58
+ attach_function :stampeded_puller_free, [:pointer], :void, blocking: true
59
+
60
+ attach_function :stampeded_lease_job, [:pointer], :string
61
+ attach_function :stampeded_lease_complete, [:pointer], :int32, blocking: true
62
+ attach_function :stampeded_lease_fail, %i[pointer string int64], :int32, blocking: true
63
+ attach_function :stampeded_lease_timed_out, %i[pointer string], :int32, blocking: true
64
+ attach_function :stampeded_lease_requeue, [:pointer], :int32, blocking: true
65
+ attach_function :stampeded_lease_discard, %i[pointer string], :int32, blocking: true
66
+ attach_function :stampeded_lease_free, [:pointer], :void, blocking: true
67
+
68
+ attach_function :stampeded_lifecycle_states, [], :string
69
+ attach_function :stampeded_lifecycle_events, [], :string
70
+ attach_function :stampeded_lifecycle_apply, %i[string string pointer], :int32
71
+ attach_function :stampeded_cron_check, %i[string string pointer], :int32
72
+
73
+ attach_function :stampeded_steps_load, [:string], :pointer
74
+ attach_function :stampeded_steps_free, [:pointer], :void
75
+ attach_function :stampeded_steps_completed, %i[pointer string], :int32
76
+ attach_function :stampeded_steps_value, %i[pointer string], :string
77
+ attach_function :stampeded_steps_cursor, %i[pointer string], :string
78
+ attach_function :stampeded_steps_complete, %i[pointer string string], :int32
79
+ attach_function :stampeded_steps_set_cursor, %i[pointer string string], :int32
80
+ attach_function :stampeded_steps_json, [:pointer], :string
81
+
82
+ module_function
83
+
84
+ def version = stampeded_version
85
+ def log_init(filter)
86
+ status = stampeded_log_init(filter)
87
+ raise Error, last_error if status == ERROR
88
+
89
+ status == OK
90
+ end
91
+
92
+ def last_error = stampeded_last_error || "unknown error"
93
+
94
+ def ms(seconds) = (seconds.to_f * 1000).round.clamp(0, 0xFFFF_FFFF)
95
+ def text(value) = value.to_s.encode("UTF-8", invalid: :replace, undef: :replace).delete("\0")
96
+
97
+ def lifecycle_states = stampeded_lifecycle_states.split(",").map(&:to_sym)
98
+
99
+ def lifecycle_events = stampeded_lifecycle_events.split(",").map(&:to_sym)
100
+
101
+ def lifecycle_apply(state, event)
102
+ out = FFI::MemoryPointer.new(:pointer)
103
+ case stampeded_lifecycle_apply(state.to_s, event.to_s, out)
104
+ when OK then out.read_pointer.read_string.to_sym
105
+ when NONE then nil
106
+ else raise Error, last_error
107
+ end
108
+ end
109
+
110
+ def cron_check(expr, timezone)
111
+ out = FFI::MemoryPointer.new(:pointer)
112
+ raise Error, last_error if stampeded_cron_check(text(expr), text(timezone), out) == ERROR
113
+
114
+ out.read_pointer.read_string
115
+ end
116
+
117
+ class Steps
118
+ def initialize(checkpoint)
119
+ pointer = Native.stampeded_steps_load(checkpoint.nil? ? nil : JSON.generate(checkpoint))
120
+ raise Error, Native.last_error if pointer.null?
121
+
122
+ @pointer = FFI::AutoPointer.new(pointer, Native.method(:stampeded_steps_free))
123
+ end
124
+
125
+ def completed?(name) = check(Native.stampeded_steps_completed(@pointer, name.to_s)) == OK
126
+
127
+ def value(name) = decode(Native.stampeded_steps_value(@pointer, name.to_s))
128
+
129
+ def cursor(name) = decode(Native.stampeded_steps_cursor(@pointer, name.to_s))
130
+
131
+ def complete(name, json) = check(Native.stampeded_steps_complete(@pointer, name.to_s, json))
132
+
133
+ def set_cursor(name, json) = check(Native.stampeded_steps_set_cursor(@pointer, name.to_s, json))
134
+
135
+ def to_h = JSON.parse(Native.stampeded_steps_json(@pointer))
136
+
137
+ private
138
+
139
+ def check(status)
140
+ raise Error, Native.last_error if status == ERROR
141
+
142
+ status
143
+ end
144
+
145
+ def decode(json) = json && JSON.parse(json)
146
+ end
147
+ class Puller
148
+ attr_reader :worker_id
149
+
150
+ def initialize(url:, queues:, except:, max_leases:, batch_size:, poll_interval:, heartbeat_interval:)
151
+ @client = Native.stampeded_connect(url)
152
+ raise Error, Native.last_error if @client.null?
153
+
154
+ options = PullerOptions.new
155
+ queue_spec = FFI::MemoryPointer.from_string(Native.text(queues))
156
+ except_spec = except && FFI::MemoryPointer.from_string(Native.text(except))
157
+ options[:queues] = queue_spec
158
+ options[:except_queues] = except_spec || FFI::Pointer::NULL
159
+ options[:max_leases] = max_leases
160
+ options[:batch_size] = batch_size
161
+ options[:poll_interval_ms] = Native.ms(poll_interval)
162
+ options[:heartbeat_interval_ms] = Native.ms(heartbeat_interval)
163
+
164
+ @puller = Native.stampeded_puller_start(@client, options)
165
+ if @puller.null?
166
+ message = Native.last_error
167
+ close
168
+ raise Error, "could not start the worker: #{message}"
169
+ end
170
+ @worker_id = Native.stampeded_puller_worker_id(@puller)
171
+ end
172
+
173
+ def next(timeout)
174
+ out = FFI::MemoryPointer.new(:pointer)
175
+ case Native.stampeded_puller_next(@puller, Native.ms(timeout), out)
176
+ when OK then Lease.new(out.read_pointer)
177
+ when NONE then nil
178
+ else raise Error, Native.last_error
179
+ end
180
+ end
181
+
182
+ def stop(drain)
183
+ raise Error, Native.last_error if Native.stampeded_puller_stop(@puller, Native.ms(drain)) == ERROR
184
+ end
185
+
186
+ def close
187
+ Native.stampeded_puller_free(@puller) if @puller && !@puller.null?
188
+ Native.stampeded_client_free(@client) if @client && !@client.null?
189
+ @puller = @client = nil
190
+ end
191
+ end
192
+
193
+ class Lease
194
+ attr_reader :row
195
+
196
+ def initialize(pointer)
197
+ @pointer = pointer
198
+ @row = JSON.parse(Native.stampeded_lease_job(pointer))
199
+ end
200
+
201
+ def settled? = @pointer.nil?
202
+
203
+ def complete! = settle { Native.stampeded_lease_complete(it) }
204
+ def fail!(message, retry_in_ms: nil)
205
+ settle { Native.stampeded_lease_fail(it, Native.text(message), retry_in_ms || -1) }
206
+ end
207
+
208
+ def requeue! = settle { Native.stampeded_lease_requeue(it) }
209
+
210
+ def timed_out!(message) = settle { Native.stampeded_lease_timed_out(it, Native.text(message)) }
211
+
212
+ def discard!(message) = settle { Native.stampeded_lease_discard(it, Native.text(message)) }
213
+
214
+ def release!
215
+ return if settled?
216
+
217
+ pointer = @pointer
218
+ @pointer = nil
219
+ Native.stampeded_lease_free(pointer)
220
+ end
221
+
222
+ private
223
+
224
+ def settle
225
+ raise Error, "lease already settled" if settled?
226
+
227
+ pointer = @pointer
228
+ @pointer = nil
229
+ raise Error, Native.last_error if yield(pointer) == ERROR
230
+ end
231
+ end
232
+ end
233
+ end