From e1e215467775c7475206711aaa48e5ab41a6c180 Mon Sep 17 00:00:00 2001 From: Yessiest Date: Tue, 7 May 2024 19:07:34 +0400 Subject: [PATCH] Version bump and, finally, thread safety and shared request context memory --- examples/rack_app.ru | 59 ++++++++++++++++------------------ examples/thread_safety_test.ru | 39 ++++++++++++++++++++++ examples/websockets.ru | 40 +++++++++++------------ landline.gemspec | 2 +- lib/landline.rb | 2 +- lib/landline/app.rb | 41 +++++++++++++++-------- lib/landline/path.rb | 31 ++---------------- lib/landline/probe.rb | 22 ------------- lib/landline/probe/handler.rb | 12 +++++-- lib/landline/request.rb | 14 ++++++-- lib/landline/sandbox.rb | 32 ++++++++++++++++++ lib/landline/server.rb | 2 +- lib/landline/util/lookup.rb | 2 ++ 13 files changed, 174 insertions(+), 124 deletions(-) create mode 100644 examples/thread_safety_test.ru create mode 100644 lib/landline/sandbox.rb diff --git a/examples/rack_app.ru b/examples/rack_app.ru index 4d502cf..cc8aaa3 100644 --- a/examples/rack_app.ru +++ b/examples/rack_app.ru @@ -19,31 +19,28 @@ end # Example Landline application as rack middleware class HelloServer < Landline::App - setup do - get "/test2" do - "Hello world from #{self}!" - end - handle do |status, backtrace: nil| - page = ([Landline::Util::HTTP_STATUS[status]] + - (backtrace || [""])).join("\n") - [ - { - "content-length": page.bytesize, - "content-type": "text/plain", - "x-cascade": true - }, - page - ] - end + get "/test2" do + "Hello world from #{self}!" + end + + handle do |status, backtrace: nil| + page = ([Landline::Util::HTTP_STATUS[status]] + + (backtrace || [""])).join("\n") + [ + { + "content-length": page.bytesize, + "content-type": "text/plain", + "x-cascade": true + }, + page + ] end end # Example Landline app as rack application class CrossCallServer < Landline::App - setup do - get "/inner_test" do - "Hello world, through crosscall!" - end + get "/inner_test" do + "Hello world, through crosscall!" end end @@ -52,21 +49,19 @@ class Server < Landline::App use TimerMiddleware use HelloServer - setup do - crosscall_server = CrossCallServer.new + crosscall_server = CrossCallServer.new - get "/test" do - "Hello from #{self}!" - end + get "/test" do + "Hello from #{self}!" + end - # Cross-callable application included as a subpath - link "/outer", crosscall_server + # Cross-callable application included as a subpath + link "/outer", crosscall_server - # Cross calling an application in a probe context - get "/crosscall" do - request.path = "/inner_test" - call(crosscall_server) - end + # Cross calling an application in a probe context + get "/crosscall" do + request.path = "/inner_test" + call(crosscall_server) end end diff --git a/examples/thread_safety_test.ru b/examples/thread_safety_test.ru new file mode 100644 index 0000000..14e1a84 --- /dev/null +++ b/examples/thread_safety_test.ru @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +require 'landline' + +class Application < Landline::App + def initialize(*args) + puts self + super(*args) + end + + preprocess do + puts Thread.current.inspect + end + + get "/long" do + before = request + sleep 10 + "Request didn't change mid-execution: #{before == request}" + end + + get "/long2" do + puts self + @instance_property = (rand * 1000).floor + response = "Your magic number is #{@instance_property}\n" + sleep 20 + response += "Your magic number after waiting 20 seconds is #{@instance_property}" + response + end + + get "/magic" do + @instance_property = (rand * 1000).floor + end + + postprocess do + puts "Last value of instance_property: #{@instance_property}" + end +end + +run Application.new diff --git a/examples/websockets.ru b/examples/websockets.ru index 012cd84..1475584 100644 --- a/examples/websockets.ru +++ b/examples/websockets.ru @@ -4,28 +4,26 @@ require 'landline' require 'landline/extensions/websocket' class Test < Landline::App - setup do - websocket "/test", version: 7 do |socket| - socket.on :message do |msg| - puts "Client wrote: #{msg}" - end - socket.on :error do |err| - puts "Error occured: #{err.inspect}" - puts err.backtrace - end - socket.on :close do - puts "Client closed read connection" - end - socket.ready - socket.write("Hi!") - response = socket.read - socket.write("You said: #{response}") - socket.write("Goodbye!") - socket.close - rescue Exception => e - puts e.inspect - puts e.backtrace + websocket "/test", version: 7 do |socket| + socket.on :message do |msg| + puts "Client wrote: #{msg}" end + socket.on :error do |err| + puts "Error occured: #{err.inspect}" + puts err.backtrace + end + socket.on :close do + puts "Client closed read connection" + end + socket.ready + socket.write("Hi!") + response = socket.read + socket.write("You said: #{response}") + socket.write("Goodbye!") + socket.close + rescue Exception => e + puts e.inspect + puts e.backtrace end end diff --git a/landline.gemspec b/landline.gemspec index c4eb1d4..3a28288 100644 --- a/landline.gemspec +++ b/landline.gemspec @@ -2,7 +2,7 @@ Gem::Specification.new do |spec| spec.name = "landline" - spec.version = "0.12.0" + spec.version = "0.12.1" spec.summary = "Elegant HTTP DSL" spec.description = <<~DESC Landline is a no-hard-dependencies HTTP routing DSL that was made entirely for fun. diff --git a/lib/landline.rb b/lib/landline.rb index 8b55a36..72f564e 100644 --- a/lib/landline.rb +++ b/lib/landline.rb @@ -12,7 +12,7 @@ require_relative 'landline/app' # Landline is a backend framework born as a by-product of experimentation module Landline # Landline version - VERSION = '0.12 "Concrete and Gold" (pre-alpha)' + VERSION = '0.12.1 "Concrete and Gold" (pre-alpha)' # Landline branding and version VLINE = "Landline/#{Landline::VERSION} (Ruby/#{RUBY_VERSION}/#{RUBY_RELEASE_DATE})\n".freeze diff --git a/lib/landline/app.rb b/lib/landline/app.rb index 341e479..afbdf20 100644 --- a/lib/landline/app.rb +++ b/lib/landline/app.rb @@ -3,18 +3,20 @@ module Landline # Rack application interface class App - # TODO: fix this mess somehow (probably impossible) - - # @!parse include Landline::DSL::PathMethods - # @!parse include Landline::DSL::PathConstructors - # @!parse include Landline::DSL::ProbeConstructors - # @!parse include Landline::DSL::ProbeMethods - # @!parse include Landline::DSL::CommonMethods class << self + # TODO: fix this mess somehow (probably impossible) + # @!parse include Landline::DSL::PathMethods + # @!parse include Landline::DSL::PathConstructors + # @!parse include Landline::DSL::ProbeConstructors + # @!parse include Landline::DSL::ProbeMethods + # @!parse include Landline::DSL::CommonMethods + # Duplicate used middleware for the subclassed app def inherited(subclass) super(subclass) + @setup_chain ||= [] subclass.middleware = @middleware.dup + subclass.setup_chain = @setup_chain.dup end # Include a middleware in application @@ -24,17 +26,30 @@ module Landline @middleware.append(middleware) end - # Setup block - # @param block [#call] - def setup(&block) - @setup_block = block + # Check if Server can respond to given symbol + def respond_to_missing?(symbol, _include_private) + Landline::ServerContext.instance_methods.include?(symbol) || super end - attr_accessor :middleware, :setup_block + # Store applied app manipulations + def method_missing(symbol, *args, **params, &callback) + if Landline::ServerContext.instance_methods.include? symbol + @setup_chain.append([symbol, args, params, callback]) + else + super(symbol, *args, **params, &callback) + end + end + + attr_accessor :middleware, :setup_chain end def initialize(*args, **opts) - @app = ::Landline::Server.new(*args, **opts, &self.class.setup_block) + setup_chain = self.class.setup_chain + @app = ::Landline::Server.new(*args, **opts) do + setup_chain.each do |symbol, cargs, cparams, callback| + send(symbol, *cargs, **cparams, &callback) + end + end self.class.middleware&.reverse_each do |cls| @app = cls.new(@app) end diff --git a/lib/landline/path.rb b/lib/landline/path.rb index 999e361..620ac2f 100644 --- a/lib/landline/path.rb +++ b/lib/landline/path.rb @@ -4,23 +4,9 @@ require_relative 'pattern_matching' require_relative 'node' require_relative 'dsl/constructors_path' require_relative 'dsl/methods_path' -require_relative 'dsl/methods_common' -require_relative 'dsl/methods_probe' -require_relative 'dsl/constructors_probe' require_relative 'util/lookup' module Landline - # Execution context for filters and preprocessors. - class ProcessorContext - include Landline::DSL::CommonMethods - include Landline::DSL::ProbeMethods - include Landline::DSL::ProbeConstructors - - def initialize(path) - @origin = path - end - end - # Execution context for path setup block. class PathContext include Landline::DSL::PathConstructors @@ -31,21 +17,8 @@ module Landline end end - # Ephemeral proxy class to which callback execution binds - class PathExecutionOrigin - def initialize(request, properties) - @request = request - @properties = Landline::Util::LookupROProxy.new(properties) - end - - attr_accessor :response - attr_reader :request, :properties - end - # Primary building block of request navigation. class Path < Landline::Node - ExecutionOrigin = Landline::PathExecutionOrigin - ProcContext = Landline::ProcessorContext Context = Landline::PathContext # @param path [Object] Object to generate {Landline::Pattern} from @@ -108,8 +81,8 @@ module Landline # Create an execution context for in-path processing blocks def get_context(request) - exec_origin = self.class::ExecutionOrigin.new(request, @properties) - self.class::ProcContext.new(exec_origin) + request.context.origin.properties.lookup = @properties + request.context end # Sequentially run through all filters and drop request if one is false diff --git a/lib/landline/probe.rb b/lib/landline/probe.rb index a5e1db1..d19ddb6 100644 --- a/lib/landline/probe.rb +++ b/lib/landline/probe.rb @@ -24,28 +24,6 @@ module Landline autoload :Link, "landline/probe/crosscall_handler" end - # Context that provides execution context for Probes. - class ProbeContext - include Landline::DSL::ProbeConstructors - include Landline::DSL::ProbeMethods - include Landline::DSL::CommonMethods - - def initialize(origin) - @origin = origin - end - end - - # Ephemeral proxy class to which callback execution binds - class ProbeExecutionOrigin - def initialize(request, properties) - @request = request - @properties = Landline::Util::LookupROProxy.new(properties) - end - - attr_accessor :response - attr_reader :request, :properties - end - # Test probe. Also base for all "reactive" nodes. class Probe < Landline::Node # @param path [Object] diff --git a/lib/landline/probe/handler.rb b/lib/landline/probe/handler.rb index e090d85..eb92089 100644 --- a/lib/landline/probe/handler.rb +++ b/lib/landline/probe/handler.rb @@ -29,8 +29,8 @@ module Landline # @return [Boolean] true if further navigation is possible # @raise [UncaughtThrowError] may raise if die() is called. def process(request) - origin = Landline::ProbeExecutionOrigin.new(request, @properties) - context = Landline::ProbeContext.new(origin) + origin, context = get_context(request) + return reject(request) unless request.path.match?(/^\/?$/) response = catch(:break) do @@ -47,6 +47,14 @@ module Landline end throw :finish, response end + + private + + # Create a context to run handler in + def get_context(request) + request.context.origin.properties.lookup = @properties + [request.context.origin, request.context] + end end end end diff --git a/lib/landline/request.rb b/lib/landline/request.rb index e93155d..357d821 100644 --- a/lib/landline/request.rb +++ b/lib/landline/request.rb @@ -3,6 +3,7 @@ require 'uri' require_relative 'util/query' require_relative 'util/cookie' +require_relative 'sandbox' module Landline # Request wrapper for Rack protocol @@ -30,13 +31,16 @@ module Landline @states = [] # Postprocessors for current request @postprocessors = [] + # Execution context + @context = init_context end # Run postprocessors # @param response [Landline::Response] def run_postprocessors(response) + @context.origin.properties.lookup = {} @postprocessors.reverse_each do |postproc| - postproc.call(self, response) + @context.instance_exec(self, response, &postproc) end @postprocessors = [] end @@ -90,11 +94,17 @@ module Landline attr_reader :request_method, :script_name, :path_info, :server_name, :server_port, :server_protocol, :headers, :param, :splat, - :postprocessors, :cookies, :rack + :postprocessors, :cookies, :rack, :context attr_accessor :path, :filepath, :query private + # Initialize execution context + def init_context + origin = Landline::ProcessorOrigin.new(self, {}) + Landline::ProcessorContext.new(origin) + end + # Initialize basic rack request parameters # @param env [Hash] def init_request_params(env) diff --git a/lib/landline/sandbox.rb b/lib/landline/sandbox.rb new file mode 100644 index 0000000..39b1495 --- /dev/null +++ b/lib/landline/sandbox.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require_relative 'dsl/methods_common' +require_relative 'dsl/methods_probe' +require_relative 'dsl/constructors_probe' +require_relative 'util/lookup' + +module Landline + # Execution context for filters and preprocessors. + class ProcessorContext + include Landline::DSL::CommonMethods + include Landline::DSL::ProbeMethods + include Landline::DSL::ProbeConstructors + + def initialize(path) + @origin = path + end + + attr_reader :origin + end + + # Ephemeral proxy class to which callback execution binds + class ProcessorOrigin + def initialize(request, properties) + @request = request + @properties = Landline::Util::LookupROProxy.new(properties) + end + + attr_accessor :response + attr_reader :request, :properties + end +end diff --git a/lib/landline/server.rb b/lib/landline/server.rb index 174e8c3..52b686e 100644 --- a/lib/landline/server.rb +++ b/lib/landline/server.rb @@ -14,7 +14,7 @@ module Landline # @param parent [Landline::Node, nil] Parent object to inherit properties to # @param setup [#call] Setup block def initialize(passthrough = nil, parent: nil, **opts, &setup) - super("", parent: nil, **opts, &setup) + super("", parent: parent, **opts, &setup) return if parent @passthrough = passthrough diff --git a/lib/landline/util/lookup.rb b/lib/landline/util/lookup.rb index d92f226..30b585a 100644 --- a/lib/landline/util/lookup.rb +++ b/lib/landline/util/lookup.rb @@ -46,6 +46,8 @@ module Landline def [](key) @lookup.[](key) end + + attr_accessor :lookup end end end