Skip to content

Commit

Permalink
fix: implement an optional thread protection mechanism in REST::Helpers
Browse files Browse the repository at this point in the history
The protection is meant to avoid usage of core objects (plan,
interface), outside the Roby execution thread.

It is always disabled in tests because of interaction with equality
(assert_equal, flexmock, ...). It is also disabled by default because of
the performance impact.
  • Loading branch information
doudou committed Nov 19, 2024
1 parent 2ca982c commit 9065ae4
Showing 1 changed file with 82 additions and 5 deletions.
87 changes: 82 additions & 5 deletions lib/roby/interface/rest/helpers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,53 @@ module Helpers
#
# @return [Roby::Interface]
def interface
env.fetch("roby.interface")
return @interface if @interface

i = env.fetch("roby.interface")
@interface = roby_thread_protection(i) do
i.inside_control?
end
end

# The underlying Roby app
#
# @return [Roby::Application]
def roby_app
@roby_app ||= interface.app
return @roby_app if @roby_app

i = env.fetch("roby.interface")
@roby_app = roby_thread_protection(i.app) do
i.execution_engine.inside_control?
end
end

# @api private
#
# Wrap `object` within a proxy that validate that calls are made inside
# the Roby execution thread
#
# The wrapping is disabled by default. Need to set
# Conf.roby.rest_thread_protection to true to enable. It is always
# disabled in tests because of interference with comparisons (e.g.
# flexmock or assert_equal)
def roby_thread_protection(object, &block)
if !Roby.app.testing? && Conf.roby.rest_thread_protection?
ThreadProtectionProxy.new(i.app, &block)
else
object
end
end

# The underlying Roby plan
#
# @return [Roby::ExecutablePlan]
def roby_plan
roby_app.plan
return @roby_plan if @roby_plan

i = env.fetch("roby.interface")
@roby_plan = roby_thread_protection(i.plan) do
i.execution_engine.inside_control?
end
end

# A permanent storage hash
Expand All @@ -34,12 +66,18 @@ def roby_storage
#
# @return [Roby::ExecutablePlan]
def execution_engine
@execution_engine ||= interface.execution_engine
return @execution_engine if @execution_engine

i = env.fetch("roby.interface")
@roby_plan = roby_thread_protection(i.execution_engine) do
i.execution_engine.inside_control?
end
end

# Execute a block in a context synchronzied with the engine
def roby_execute(&block)
execution_engine.execute(&block)
i = env.fetch("roby.interface")
i.execution_engine.execute(&block)
end

# @deprecated use {#roby_execute} instead
Expand All @@ -50,6 +88,45 @@ def execute(&block)
"use #roby_execute instead"
roby_execute(&block)
end

# @api private
#
# (Mostly) transparent proxy that validates that all calls are
# within a given thread
#
# The class calls the block given to its constructor for each call,
# and will raise if the block returns false
#
# @see Helpers#roby_thread_protection
class ThreadProtectionProxy < BasicObject
def initialize(object, &in_thread)
@object = object
@in_thread = in_thread
end

def respond_to_missing?(name, *)
@object.respond_to?(name)
end

def inspect
"ThreadProtectionProxy(#{super})"
end

def to_s
"ThreadProtectionProxy(#{super})"
end

def method_missing(name, *args, **kw, &block)
unless @in_thread.call
$stderr.puts "wrong thread in call to #{@object}"
$stderr.puts ::Kernel.caller.join("\n ")
::Kernel.raise ::ThreadError,
"wrong thread in call to #{@object}"
end

@object.send(name, *args, **kw, &block)
end
end
end
end
end
Expand Down

0 comments on commit 9065ae4

Please sign in to comment.