Class: Axn::Configuration

Inherits:
Object
  • Object
show all
Extended by:
Axn::Configurable::Settings
Defined in:
lib/axn/configuration.rb

Constant Summary collapse

SIDEKIQ_JOB_TAG_SOURCES =

Which declared facet types surface as Sidekiq per-job tags at enqueue (PRO-2855). Sidekiq tags are ephemeral job-payload strings shown/searched in the web UI — they carry no metrics-billing cost, so high-cardinality tags are welcome here (unlike metrics). Default is both; set %i for bounded-only, or [] to disable the sink.

%i[tag dimension].freeze
TOOL_ROOTS_BLOCKLIST =

Root-ish tool_roots entries that must never be accepted: both normalize to the project root itself (an empty entry cleanpaths to ".", and "." is its own normalized form). Compared against the normalized entry (see .normalize_tool_root). The broad-DIRECTORY-NAME cases (actions, app, app/actions, and any absolute spelling of them) are caught by the leaf-segment rule below instead, since a blocklist of exact strings can't also catch e.g. File.expand_path("actions") or /srv/app/actions.

["", "."].freeze
BROAD_TOOL_ROOT_LEAVES =

Leaf (final path segment) names that make a tool_roots entry broad regardless of how much path precedes them — including an ABSOLUTE spelling (e.g. File.expand_path("actions"), "/srv/app/actions"), which normalizes to a path that no longer equals any exact blocklist string but still resolves to (and bulk-exposes) the same directory. An entry ending in actions is the business-actions dir; ending in app is the whole app dir. Checked against the LAST segment only, so a narrow subdir like actions/tools (leaf tools) is unaffected.

%w[actions app].freeze
ASYNC_EXCEPTION_REPORTING_OPTIONS =

Controls when on_exception is triggered in async context (Sidekiq/ActiveJob). Options:

:every_attempt - trigger on every retry attempt (includes retry context)
:first_and_exhausted - trigger on first attempt and when retries exhausted (default)
:only_exhausted - only trigger when retries exhausted (via death handler)
%i[every_attempt first_and_exhausted only_exhausted].freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Axn::Configurable::Settings

_declared_settings, extended, overridable_config_source, setting

Methods included from Axn::Configurable::PerClassOverrides

#_validate_override_setter!, #config_namespace, #overrides, #resolve_override_for

Instance Attribute Details

#ambient_context_providerObject

Optional callable returning a Hash of ambient context data (e.g. from request-local state). Consulted when no explicit ambient_context: kwarg is passed to an Axn call. Falls back to Axn::Core::AmbientContext.default_source when nil.



170
171
172
# File 'lib/axn/configuration.rb', line 170

def ambient_context_provider
  @ambient_context_provider
end

#loggerObject



283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
# File 'lib/axn/configuration.rb', line 283

def logger
  return @logger if @logger

  # Use sidekiq logger if in background
  resolved =
    begin
      if Axn::Util::ExecutionContext.background? && defined?(Sidekiq)
        Sidekiq.logger
      else
        Rails.logger
      end
    rescue NameError
      nil
    end

  # Memoize a real host logger, but not the stdout fallback below: `Rails.logger` is nil
  # until Rails runs its initialize_logger initializer, so `include Axn` at gem load (under
  # Bundler.require) resolves to nil here. Returning the transient fallback without caching it
  # keeps every `Axn.config.logger.<level>` call site working during boot and still picks up
  # `Rails.logger` on a later call once it exists (PRO-2891).
  return @logger = resolved if resolved

  @fallback_logger ||= Logger.new($stdout).tap { |l| l.level = Logger::INFO }
end

#on_exception(e, action:, context: {}) ⇒ Object



242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
# File 'lib/axn/configuration.rb', line 242

def on_exception(e, action:, context: {})
  if action.respond_to?(:result) && action.result.respond_to?(:error)
    resolved_error = action.result.error
    # Compare with the default fallback message instead of calling default_error
    # to avoid triggering error message resolution multiple times
    # Each branch picks WHICH detail to report; none of them renders it. Rendering happens once, at the
    # join below, so the composition does not depend on every branch here having remembered to.
    detail = if resolved_error == Axn::Core::Flow::Handlers::Resolvers::MessageResolver::DEFAULT_ERROR
               e
             else
               resolved_error
             end
  else
    detail = e
  end

  # Both operands normalized at the join. `detail` is the caller's own object whenever they handed one to
  # `fail!` (or returned one from a declared `error` handler), so a rendered UTF-8 class name beside a raw
  # Latin-1 detail raised `Encoding::CompatibilityError` — and since this whole handler runs inside
  # `best_effort`, that lost BOTH this log line and the configured `on_exception` callback below. An
  # exception detail reads through the guarded message reader; anything else through the value renderer.
  msg = "Handled exception (#{Axn::Internal::Rendering.class_name(e)}): #{_rendered_detail(detail)}"
  msg = ("#" * 10) + " #{msg} " + ("#" * 10) unless Axn.config.env.production?
  action.log(msg)

  return unless @on_exception

  # Only pass the args and kwargs that the given block expects
  Axn::Internal::Callable.call_with_desired_shape(@on_exception, args: [e], kwargs: { action:, context: })
end

#railsObject



240
# File 'lib/axn/configuration.rb', line 240

def rails = @rails ||= RailsConfiguration.new

Class Method Details

.broad_tool_root?(entry) ⇒ Boolean

Single source-of-truth predicate for "is this tool_roots entry too broad to allow" — shared by Axn::Tools::AdapterRoots.validate! (which raises on a bad tool_roots entry) and Tools::Registry (which skips + warns at resolve time). Validation alone can't fail-safe an in-place mutation of the live array (adapter.config.tool_roots << "actions" never calls the validated writer), so the registry re-checks every entry against this same predicate before resolving it to a directory.

Returns:

  • (Boolean)


88
# File 'lib/axn/configuration.rb', line 88

def broad_tool_root?(entry) = !_broad_tool_root_reason(entry).nil?

.normalize_tool_root(entry) ⇒ Object

Normalizes a tool_roots entry for BOTH the broad-entry blocklist check and directory resolution (Tools::Registry#_resolve_tool_dir): strips surrounding whitespace and any leading/trailing slashes, so " /actions/ " and "actions" compare equal, then collapses ./.. segments via Pathname#cleanpath so alternate spellings like "./actions", "actions/.", and "actions/../actions" normalize to the same "actions" the blocklist already rejects (Pathname#cleanpath is a lexical collapse, no filesystem access). An empty string cleanpaths to ".", which the blocklist covers. Exposed as public API (rather than kept private) so the registry's resolver can share this exact normalization — validate! and the resolver must never disagree on what a given entry means.



99
100
101
102
# File 'lib/axn/configuration.rb', line 99

def normalize_tool_root(entry)
  stripped = entry.to_s.strip.gsub(%r{\A/+|/+\z}, "")
  Pathname(stripped).cleanpath.to_s
end

Instance Method Details

#_default_async_adapterObject



199
# File 'lib/axn/configuration.rb', line 199

def _default_async_adapter = @default_async_adapter ||= false

#_default_async_configObject



200
# File 'lib/axn/configuration.rb', line 200

def _default_async_config = @default_async_config ||= {}

#_default_async_config_blockObject



201
# File 'lib/axn/configuration.rb', line 201

def _default_async_config_block = @default_async_config_block

#async_exception_reportingObject



179
180
181
# File 'lib/axn/configuration.rb', line 179

def async_exception_reporting
  @async_exception_reporting ||= :first_and_exhausted
end

#async_exception_reporting=(value) ⇒ Object



183
184
185
186
187
188
189
190
191
192
# File 'lib/axn/configuration.rb', line 183

def async_exception_reporting=(value)
  unless ASYNC_EXCEPTION_REPORTING_OPTIONS.include?(value)
    raise ArgumentError, "async_exception_reporting must be one of: #{ASYNC_EXCEPTION_REPORTING_OPTIONS.join(', ')}"
  end

  @async_exception_reporting = value

  # Auto-register Sidekiq middleware/death handler if needed and Sidekiq is available
  _auto_configure_sidekiq_for_async_exception_reporting(value)
end

#default_async?Boolean

Whether a default async adapter is configured — the only thing a gem needs to know about the _default_async_* trio below, which stays underscored because core reads all three of them across files. present? rather than !!, matching how Axn::Async itself tests the adapter.

Returns:

  • (Boolean)


197
# File 'lib/axn/configuration.rb', line 197

def default_async? = _default_async_adapter.present?

#envObject



335
336
337
338
# File 'lib/axn/configuration.rb', line 335

def env
  @env ||= ENV["RACK_ENV"].presence || ENV["RAILS_ENV"].presence || "development"
  ActiveSupport::StringInquirer.new(@env)
end

#env=(value) ⇒ Object

Validated at ASSIGNMENT because the reader below wraps the stored value in ActiveSupport::StringInquirer, which takes a String and nothing else — so anything it refuses has to be refused HERE. Accepted silently and left to the reader, a Symbol raises TypeError: no implicit conversion of Symbol into String from every LATER read instead — six sites inside the gem plus every Axn.config.env.production? in user code — which puts the failure nowhere near the line that caused it, and makes c.env = :production in an initializer detonate on the first action to run.

A Symbol is COERCED rather than refused: it is a reasonable thing to write and its meaning is unambiguous. nil is accepted as the way to clear an override, since the reader's @env ||= ENV[…] fallback is what "auto-detect the environment" means. A String subclass is a String (Rails.env is a StringInquirer already, and c.env = Rails.env is the documented Rails wiring), so it is stored as it stands.

Anything else is a declaration error, named on the same terms as every other one: case/when decides the type through Module#===, a C-level check running none of the value's own code, and the offender is named by CLASS through the undispatched renderer — a value that raises from its own inspect must not replace the verdict being reached.



324
325
326
327
328
329
330
331
332
333
# File 'lib/axn/configuration.rb', line 324

def env=(value)
  @env = case value
         when nil, ::String then value
         when ::Symbol then value.to_s
         else
           raise ArgumentError,
                 "env must be a String or Symbol naming the environment, or nil to auto-detect it from " \
                 "RACK_ENV/RAILS_ENV (got a value of class #{Axn::Internal::Rendering.class_name(value)})"
         end
end

#set_default_async(adapter = false, **config, &block) ⇒ Object

rubocop:disable Style/OptionalBooleanParameter

Raises:

  • (ArgumentError)


203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
# File 'lib/axn/configuration.rb', line 203

def set_default_async(adapter = false, **config, &block) # rubocop:disable Style/OptionalBooleanParameter
  raise ArgumentError, "Cannot set default async adapter to nil as it would cause infinite recursion" if adapter.nil?

  @default_async_adapter = adapter unless adapter.nil?
  @default_async_config = config.any? ? config : {}
  @default_async_config_block = block_given? ? block : nil

  _ensure_async_exception_reporting_registered_for_adapter(adapter)
  _apply_async_to_enqueue_all_orchestrator

  # Build the dedicated Sidekiq default worker now (at boot, in every process) so it exists
  # and carries the default's config/block when a globally-defaulted action is enqueued or run.
  return unless @default_async_adapter == :sidekiq && defined?(Axn::Async::Adapters::Sidekiq)

  Axn::Async::Adapters::Sidekiq.configure_default_worker!(config: @default_async_config, block: @default_async_config_block)
end

#set_enqueue_all_async(adapter, **config, &block) ⇒ Object



231
232
233
234
235
236
237
238
# File 'lib/axn/configuration.rb', line 231

def set_enqueue_all_async(adapter, **config, &block)
  @enqueue_all_async_adapter = adapter
  @enqueue_all_async_config = config.any? ? config : {}
  @enqueue_all_async_config_block = block_given? ? block : nil

  _ensure_async_exception_reporting_registered_for_adapter(adapter)
  _apply_async_to_enqueue_all_orchestrator
end