class Sqreen::Graft::Hook

Constants

DEFAULT_STRATEGY

Attributes

point[R]

Public Class Methods

[](hook_point, strategy = DEFAULT_STRATEGY) click to toggle source
# File lib/sqreen/graft/hook.rb, line 17
def self.[](hook_point, strategy = DEFAULT_STRATEGY)
  @hooks[hook_point] ||= new(hook_point, nil, strategy)
end
add(hook_point, strategy = DEFAULT_STRATEGY, &block) click to toggle source
# File lib/sqreen/graft/hook.rb, line 21
def self.add(hook_point, strategy = DEFAULT_STRATEGY, &block)
  self[hook_point, strategy].add(&block)
end
ignore() { || ... } click to toggle source
# File lib/sqreen/graft/hook.rb, line 25
def self.ignore
  Thread.current[:sqreen_hook_entered] = true
  yield
ensure
  Thread.current[:sqreen_hook_entered] = false
end
new(hook_point, dependency_test = nil, strategy = DEFAULT_STRATEGY) click to toggle source
# File lib/sqreen/graft/hook.rb, line 34
def initialize(hook_point, dependency_test = nil, strategy = DEFAULT_STRATEGY)
  @disabled = false
  @point = hook_point.is_a?(HookPoint) ? hook_point : HookPoint.new(hook_point, strategy)
  @before = []
  @after  = []
  @raised = []
  @ensured = []
  @dependency_test = dependency_test || Proc.new { point.exist? }
end
wrapper(hook) click to toggle source
Calls superclass method
# File lib/sqreen/graft/hook.ruby_2.rb, line 11
def self.wrapper(hook)
  timed_hooks_proc = proc do |t|
    if (request = Thread.current[:sqreen_http_request])
      request[:timed_hooks] << t if request[:timed_level] >= 1
    end
  end
  timed_callbacks_proc = proc do |t|
    if (request = Thread.current[:sqreen_http_request])
      request[:timed_callbacks] << t if request[:timed_level] >= 1
    end
  end

  Proc.new do |*args, &block|
    request = Thread.current[:sqreen_http_request]

    if Thread.current[:sqreen_hook_entered]
      if hook.point.super?
        return super(*args, &block)
      else
        return hook.point.apply(self, 'sqreen_hook', *args, &block)
      end
    end

    if request && request[:time_budget_expended] && (hook.before + hook.after + hook.raised + hook.ensured).none?(&:mandatory)
      if request[:timed_level] >= 2
        begin
          request[:skipped_callbacks].concat(hook.before)

          if hook.point.super?
            return super(*args, &block)
          else
            return hook.point.apply(self, 'sqreen_hook', *args, &block)
          end
        rescue ::Exception # rubocop:disable Lint/RescueException
          request[:skipped_callbacks].concat(hook.raised)
          raise
        else
          request[:skipped_callbacks].concat(hook.after)
        ensure
          request[:skipped_callbacks].concat(hook.ensured)
        end
      else
        if hook.point.super? # rubocop:disable Style/IfInsideElse
          return super(*args, &block)
        else
          return hook.point.apply(self, 'sqreen_hook', *args, &block)
        end
      end
    end

    hook_point_super = hook.point.super?
    logger = Sqreen::Graft.logger
    logger_debug = Sqreen::Graft.logger.debug?

    Timer.new(hook.point, &timed_hooks_proc).measure do |chrono|
      # budget implies request
      # TODO: make budget depend on a generic context (currently "request")
      budget = request[:time_budget] if request
      if budget
        budget_threshold = request[:time_budget_threshold]
        budget_ratio = request[:time_budget_ratio]
        sqreen_timer = request[:sqreen_timer]
        request_timer = request[:request_timer]
      end

      hooked_call = HookedCall.new(self, args)

      begin
        begin
          sqreen_timer.start if budget
          Thread.current[:sqreen_hook_entered] = true

          # TODO: make Call have #ball to throw by cb
          # TODO: can Call be the ball? r = catch(Call.new, &c)
          # TODO: is catch return value a Call? a #dispatch?
          # TODO: make before/after/raised return a CallbackCollection << Array (or extend with module)
          # TODO: add CallbackCollection#each_with_call(instance, args) { |call| ... } ?
          # TODO: HookCall x CallbackCollection#each_with_call x Flow
          # TODO: TimedHookCall TimedCallbackCall
          # TODO: TimeBoundHookCall TimeBoundCallbackCall TimeBoundFlow?

          request_elapsed = request_timer.elapsed if budget

          hook.before.each do |c|
            next if c.ignore && c.ignore.call

            if budget && !c.mandatory
              sqreen_elapsed = sqreen_timer.elapsed
              if budget_ratio && !request[:time_budget_expended]
                fixed_budget = budget_threshold
                proportional_budget = (request_elapsed - sqreen_elapsed) * budget_ratio
                remaining = (fixed_budget > proportional_budget ? fixed_budget : proportional_budget) - sqreen_elapsed
              else
                remaining = budget_threshold - sqreen_elapsed
              end
              unless remaining > 0
                request[:skipped_callbacks] << c
                request[:time_budget_expended] = true
                next
              end
            end

            flow = catch(Ball.new) do |ball|
              Timer.new(c.name, &timed_callbacks_proc).measure(ignore: chrono) do
                c.call(CallbackCall.new(c, remaining, hooked_call.instance, hooked_call.args_passed), ball)
              end
            end

            next unless c.flow && flow.is_a?(Flow)
            hooked_call.raise = flow.raise and hooked_call.raising = true if flow.raise?
            hooked_call.args_pass = flow.args and hooked_call.args_passing = true if flow.args?
            hooked_call.return = flow.return and hooked_call.returning = true if flow.return?
            break if flow.break?
          end unless hook.disabled?
        rescue StandardError => e
          Sqreen::Weave.logger.debug { "exception:#{e.class} when:before message:'#{e.message}' location:\"#{e.backtrace[0]}\"" }
          Sqreen::RemoteException.record(e) if Sqreen.queue
        end

        raise hooked_call.raise if hooked_call.raising
        return hooked_call.return if hooked_call.returning
      ensure
        Thread.current[:sqreen_hook_entered] = false
        sqreen_timer.stop if budget
      end unless hook.before.empty?

      begin
        chrono.ignore do
          if hook_point_super
            hooked_call.returned = super(*(hooked_call.args_passing ? hooked_call.args_pass : hooked_call.args_passed), &block)
          else
            hooked_call.returned = hook.point.apply(hooked_call.instance, 'sqreen_hook', *(hooked_call.args_passing ? hooked_call.args_pass : hooked_call.args_passed), &block)
          end
        end
      rescue ::Exception => e # rubocop:disable Lint/RescueException
        begin
          sqreen_timer.start if budget
          Thread.current[:sqreen_hook_entered] = true
          hooked_call.raised = e

          logger.debug { "[#{Process.pid}] Hook #{hook.point} disabled:#{hook.disabled?} exception:#{e}" } if logger_debug

          # TODO: early escape causes raise too early then caught by ensure which mistakenly reports legit errors to sqreen
          # raise if hook.raised.empty?

          request_elapsed = request_timer.elapsed if budget

          hook.raised.each do |c|
            next if c.ignore && c.ignore.call

            if budget && !c.mandatory
              sqreen_elapsed = sqreen_timer.elapsed
              if budget_ratio && !request[:time_budget_expended]
                fixed_budget = budget_threshold
                proportional_budget = (request_elapsed - sqreen_elapsed) * budget_ratio
                remaining = (fixed_budget > proportional_budget ? fixed_budget : proportional_budget) - sqreen_elapsed
              else
                remaining = budget_threshold - sqreen_elapsed
              end
              unless remaining > 0
                request[:skipped_callbacks] << c
                request[:time_budget_expended] = true
                next
              end
            end

            flow = catch(Ball.new) do |ball|
              Timer.new(c.name, &timed_callbacks_proc).measure(ignore: chrono) do
                c.call(CallbackCall.new(c, remaining, hooked_call.instance, hooked_call.args_passing ? hooked_call.args_pass : hooked_call.args_passed, hooked_call.raised), ball)
              end
            end

            next unless c.flow && flow.is_a?(Flow)
            hooked_call.raise = flow.raise and hooked_call.raising = true if flow.raise?
            hooked_call.return = flow.return and hooked_call.returning = true if flow.return?
            hooked_call.retrying = true if flow.retry?
            break if flow.break?
          end unless hook.disabled?
        rescue StandardError => e
          Sqreen::Weave.logger.debug { "exception:#{e.class} when:raised message:'#{e.message}' location:\"#{e.backtrace[0]}\"" }
          Sqreen::RemoteException.record(e) if Sqreen.queue
        end

        retry if hooked_call.retrying
        raise hooked_call.raise if hooked_call.raising
        return hooked_call.return if hooked_call.returning
        raise
      else
        begin
          sqreen_timer.start if budget
          Thread.current[:sqreen_hook_entered] = true

          # TODO: hooked_call.returning should be always false here?
          return hooked_call.returning ? hooked_call.return : hooked_call.returned if hook.after.empty?

          request_elapsed = request_timer.elapsed if budget

          hook.after.each do |c|
            next if c.ignore && c.ignore.call

            if budget && !c.mandatory
              sqreen_elapsed = sqreen_timer.elapsed
              if budget_ratio && !request[:time_budget_expended]
                fixed_budget = budget_threshold
                proportional_budget = (request_elapsed - sqreen_elapsed) * budget_ratio
                remaining = (fixed_budget > proportional_budget ? fixed_budget : proportional_budget) - sqreen_elapsed
              else
                remaining = budget_threshold - sqreen_elapsed
              end
              unless remaining > 0
                request[:skipped_callbacks] << c
                request[:time_budget_expended] = true
                next
              end
            end

            flow = catch(Ball.new) do |ball|
              Timer.new(c.name, &timed_callbacks_proc).measure(ignore: chrono) do
                c.call(CallbackCall.new(c, remaining, hooked_call.instance, hooked_call.args_passing ? hooked_call.args_pass : hooked_call.args_passed, nil, hooked_call.returned), ball)
              end
            end

            next unless c.flow && flow.is_a?(Flow)
            hooked_call.raise = flow.raise and hooked_call.raising = true if flow.raise?
            hooked_call.return = flow.return and hooked_call.returning = true if flow.return?
            break if flow.break?
          end unless hook.disabled?
        rescue StandardError => e
          Sqreen::Weave.logger.debug { "exception:#{e.class} when:after message:'#{e.message}' location:\"#{e.backtrace[0]}\"" }
          Sqreen::RemoteException.record(e) if Sqreen.queue
        end

        raise hooked_call.raise if hooked_call.raising
        return hooked_call.returning ? hooked_call.return : hooked_call.returned
      ensure
        begin
          # TODO: sqreen_timer.start if someone has thrown?
          # TODO: sqreen_timer.stop at end of rescue+else?
          # TODO: Thread.current[:sqreen_hook_entered] = true if neither rescue nor else ie thrown?
          # TODO: Thread.current[:sqreen_hook_entered] = false at end of rescue+else? (risky?)

          # TODO: uniform early bail out? (but don't forget sqreen_hook_entered and sqreen_timer)
          # return hooked_call.returning ? hooked_call.return : hooked_call.returned if hook.ensured.empty?

          # done at either rescue or else
          # request_elapsed = request_timer.elapsed if budget # && !hook.ensured.empty?

          hook.ensured.each do |c|
            next if c.ignore && c.ignore.call

            if budget && !c.mandatory
              sqreen_elapsed = sqreen_timer.elapsed
              if budget_ratio && !request[:time_budget_expended]
                fixed_budget = budget_threshold
                proportional_budget = (request_elapsed - sqreen_elapsed) * budget_ratio
                remaining = (fixed_budget > proportional_budget ? fixed_budget : proportional_budget) - sqreen_elapsed
              else
                remaining = budget_threshold - sqreen_elapsed
              end
              unless remaining > 0
                request[:skipped_callbacks] << c
                request[:time_budget_expended] = true
                next
              end
            end

            flow = catch(Ball.new) do |ball|
              Timer.new(c.name, &timed_callbacks_proc).measure(ignore: chrono) do
                c.call(CallbackCall.new(c, remaining, hooked_call.instance, hooked_call.args_passing ? hooked_call.args_pass : hooked_call.args_passed, nil, hooked_call.returned), ball)
              end
            end

            next unless c.flow && flow.is_a?(Flow)
            hooked_call.raise = flow.raise and hooked_call.raising = true if flow.raise?
            hooked_call.return = flow.return and hooked_call.returning = true if flow.return?
            break if flow.break?
          end unless hook.ensured.empty? || hook.disabled?

          Thread.current[:sqreen_hook_entered] = false
          sqreen_timer.stop if budget
        rescue StandardError => e
          Sqreen::Weave.logger.debug { "exception:#{e.class} when:ensured message:'#{e.message}' location:\"#{e.backtrace[0]}\"" }
          Sqreen::RemoteException.record(e) if Sqreen.queue
        end

        # TODO: should we run the following?
        # raise hooked_call.raise if hooked_call.raising
        # return hooked_call.returning ? hooked_call.return : hooked_call.returned
      end
    end # chrono
  end
end

Public Instance Methods

add(&block) click to toggle source
# File lib/sqreen/graft/hook.rb, line 48
def add(&block)
  tap { instance_eval(&block) }
end
after(tag = nil, opts = {}, &block) click to toggle source
# File lib/sqreen/graft/hook.rb, line 63
def after(tag = nil, opts = {}, &block)
  return @after if block.nil?

  @after << Callback.new(callback_name(:after, tag), opts, &block)
  @after.sort_by!(&:rank)
end
before(tag = nil, opts = {}, &block) click to toggle source
# File lib/sqreen/graft/hook.rb, line 56
def before(tag = nil, opts = {}, &block)
  return @before if block.nil?

  @before << Callback.new(callback_name(:before, tag), opts, &block)
  @before.sort_by!(&:rank)
end
callback_name(whence, tag = nil) click to toggle source
# File lib/sqreen/graft/hook.rb, line 52
def callback_name(whence, tag = nil)
  "#{point}@#{whence}" << (tag ? ":#{tag}" : "")
end
clear() click to toggle source
# File lib/sqreen/graft/hook.rb, line 120
def clear
  @before = []
  @after = []
  @raised = []
  @ensured = []
end
dependency?() click to toggle source
# File lib/sqreen/graft/hook.rb, line 44
def dependency?
  @dependency_test.call if @dependency_test
end
depends_on(&block) click to toggle source
# File lib/sqreen/graft/hook.rb, line 84
def depends_on(&block)
  @dependency_test = block
end
disable() click to toggle source
# File lib/sqreen/graft/hook.rb, line 92
def disable
  @disabled = true
end
disabled?() click to toggle source
# File lib/sqreen/graft/hook.rb, line 96
def disabled?
  @disabled
end
enable() click to toggle source
# File lib/sqreen/graft/hook.rb, line 88
def enable
  @disabled = false
end
ensured(tag = nil, opts = {}, &block) click to toggle source
# File lib/sqreen/graft/hook.rb, line 77
def ensured(tag = nil, opts = {}, &block)
  return @ensured if block.nil?

  @ensured << Callback.new(callback_name(:ensured, tag), opts, &block)
  @ensured.sort_by!(&:rank)
end
install() click to toggle source
# File lib/sqreen/graft/hook.rb, line 100
def install
  unless point.exist?
    Sqreen::Graft.logger.debug { "[#{Process.pid}] #{point} not found" }
    return
  end
  Sqreen::Graft.logger.debug { "[#{Process.pid}] Hook #{point}: installing" }

  point.install('sqreen_hook', &Sqreen::Graft::Hook.wrapper(self))
end
raised(tag = nil, opts = {}, &block) click to toggle source
# File lib/sqreen/graft/hook.rb, line 70
def raised(tag = nil, opts = {}, &block)
  return @raised if block.nil?

  @raised << Callback.new(callback_name(:raised, tag), opts, &block)
  @raised.sort_by!(&:rank)
end
uninstall() click to toggle source
# File lib/sqreen/graft/hook.rb, line 110
def uninstall
  unless point.exist?
    Sqreen::Graft.logger.debug { "[#{Process.pid}] #{point} not found" }
    return
  end
  Sqreen::Graft.logger.debug { "[#{Process.pid}] Hook #{point}: uninstalling" }

  point.uninstall('sqreen_hook', &Sqreen::Graft::Hook.wrapper(self))
end