delayer-deferred-2.0.0/0000755000004100000410000000000013124154450014743 5ustar www-datawww-datadelayer-deferred-2.0.0/Rakefile0000644000004100000410000000047213124154450016413 0ustar www-datawww-datarequire "bundler/gem_tasks" require "rake/testtask" Rake::TestTask.new do |t| t.test_files = FileList['test/*_test.rb'] t.warning = true t.verbose = true end task :benchmark do FileList['test/*_benchmark.rb'].each{|f| load f } end task :profile do FileList['test/*_profiler.rb'].each{|f| load f } end delayer-deferred-2.0.0/delayer-deferred.gemspec0000644000004100000410000000240613124154450021515 0ustar www-datawww-data# coding: utf-8 lib = File.expand_path('../lib', __FILE__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require 'delayer/deferred/version' Gem::Specification.new do |spec| spec.name = "delayer-deferred" spec.version = Delayer::Deferred::VERSION spec.authors = ["Toshiaki Asai"] spec.email = ["toshi.alternative@gmail.com"] spec.summary = %q{Deferred for Delayer} spec.description = %q{Deferred for Delayer.} spec.homepage = "https://github.com/toshia/delayer-deferred" spec.license = "MIT" spec.files = `git ls-files -z`.split("\x0") spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) spec.require_paths = ["lib"] spec.required_ruby_version = '>= 2.0.0' spec.add_dependency "delayer", ">= 0.0.2", "< 0.1" spec.add_development_dependency "bundler", "~> 1.7" spec.add_development_dependency "rake", "~> 10.0" spec.add_development_dependency "minitest", "~> 5.7" spec.add_development_dependency "simplecov" spec.add_development_dependency "ruby-prof" spec.add_development_dependency 'guard' spec.add_development_dependency 'guard-shell' spec.add_development_dependency 'guard-rake' end delayer-deferred-2.0.0/Gemfile0000644000004100000410000000014513124154450016236 0ustar www-datawww-datasource 'https://rubygems.org' # Specify your gem's dependencies in delayer-deferred.gemspec gemspec delayer-deferred-2.0.0/LICENSE.txt0000644000004100000410000000205613124154450016571 0ustar www-datawww-dataCopyright (c) 2015 Toshiaki Asai MIT License Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. delayer-deferred-2.0.0/lib/0000755000004100000410000000000013124154450015511 5ustar www-datawww-datadelayer-deferred-2.0.0/lib/delayer/0000755000004100000410000000000013124154450017136 5ustar www-datawww-datadelayer-deferred-2.0.0/lib/delayer/deferred.rb0000644000004100000410000000164213124154450021246 0ustar www-datawww-data# coding: utf-8 require "delayer" require "delayer/deferred/deferred" require "delayer/deferred/deferredable" require "delayer/deferred/enumerable" require "delayer/deferred/enumerator" require "delayer/deferred/thread" require "delayer/deferred/tools" require "delayer/deferred/version" module Delayer module Deferred class << self #真ならデバッグ情報を集める attr_accessor :debug def method_missing(*rest, &block) Delayer::Deferred::Deferred.__send__(*rest, &block) end end end module Extend def Promise @promise ||= begin the_delayer = self Class.new(::Delayer::Deferred::Promise) { define_singleton_method(:delayer) { the_delayer } } end end alias :Deferred :Promise #deprecate :Deferred, "Promise", 2018, 03 end end Delayer::Deferred.debug = false delayer-deferred-2.0.0/lib/delayer/deferred/0000755000004100000410000000000013124154450020716 5ustar www-datawww-datadelayer-deferred-2.0.0/lib/delayer/deferred/promise.rb0000644000004100000410000000257113124154450022726 0ustar www-datawww-data# -*- coding: utf-8 -*- require "delayer/deferred/tools" require "delayer/deferred/deferredable/trigger" module Delayer::Deferred class Promise extend Delayer::Deferred::Tools include Deferredable::Trigger class << self def new(stop=false, name: caller_locations(1,1).first.to_s, &block) result = promise = super(name: name) result = promise.next(&block) if block_given? promise.call(true) unless stop result end def Thread @thread_class ||= gen_thread_class end def Promise self end def delayer ::Delayer end def to_s "#{self.delayer}.Promise" end private def gen_thread_class the_delayer = delayer Class.new(Thread) do define_singleton_method(:delayer) do the_delayer end end end end def initialize(name:) super() @name = name end def activate(response) change_sequence(:activate) change_sequence(:complete) response end def inspect "#<#{self.class} seq:#{sequence.name}>" end def ancestor self end def parent=(chainable) fail Error, "#{self.class} can't has parent." end private def graph_shape 'egg'.freeze end def node_name @name.to_s end end end delayer-deferred-2.0.0/lib/delayer/deferred/chain/0000755000004100000410000000000013124154450022000 5ustar www-datawww-datadelayer-deferred-2.0.0/lib/delayer/deferred/chain/trap.rb0000644000004100000410000000036513124154450023277 0ustar www-datawww-data# -*- coding: utf-8 -*- require "delayer/deferred/chain/base" module Delayer::Deferred::Chain class Trap < Base def evaluate?(response) response.ng? end private def graph_shape 'diamond'.freeze end end end delayer-deferred-2.0.0/lib/delayer/deferred/chain/next.rb0000644000004100000410000000036113124154450023303 0ustar www-datawww-data# -*- coding: utf-8 -*- require "delayer/deferred/chain/base" module Delayer::Deferred::Chain class Next < Base def evaluate?(response) response.ok? end private def graph_shape 'box'.freeze end end end delayer-deferred-2.0.0/lib/delayer/deferred/chain/await.rb0000644000004100000410000000172613124154450023440 0ustar www-datawww-data# -*- coding: utf-8 -*- require "delayer/deferred/chain/base" module Delayer::Deferred::Chain class Await < Base def initialize(worker:, deferred:) super() @worker, @awaiting_deferred = worker, deferred deferred.add_awaited(self) end def activate(response) change_sequence(:activate) @worker.give_response(response, @awaiting_deferred) # TODO: 即座にspoilさせてよさそう ensure change_sequence(:complete) end def graph_child(output:) output << graph_mynode if has_child? @child.graph_child(output: output) @awaiting_deferred.graph_child(output: output) output << "#{__id__} -> #{@child.__id__}" end nil end def node_name "Await" end def graph_shape 'circle'.freeze end def graph_mynode label = "#{node_name}\n(#{sequence.name})" "#{__id__} [shape=#{graph_shape},label=#{label.inspect}]" end end end delayer-deferred-2.0.0/lib/delayer/deferred/chain/base.rb0000644000004100000410000000151213124154450023236 0ustar www-datawww-data# -*- coding: utf-8 -*- require "delayer/deferred/deferredable/chainable" require "delayer/deferred/deferredable/node_sequence" module Delayer::Deferred::Chain class Base include Delayer::Deferred::Deferredable::NodeSequence include Delayer::Deferred::Deferredable::Chainable def initialize(&proc) fail Error, "Delayer::Deferred::Chain can't create instance." if self.class == Delayer::Deferred::Chain::Base @proc = proc end def activate(response) change_sequence(:activate) if evaluate?(response) @proc.(response.value) else response end ensure change_sequence(:complete) end def inspect "#<#{self.class} seq:#{sequence.name} child:#{has_child?}>" end def node_name @proc.source_location.join(':'.freeze) end end end delayer-deferred-2.0.0/lib/delayer/deferred/deferredable.rb0000644000004100000410000000055513124154450023654 0ustar www-datawww-data# -*- coding: utf-8 -*- require "delayer/deferred/version" module Delayer::Deferred module Deferredable; end end require "delayer/deferred/deferredable/awaitable" require "delayer/deferred/deferredable/chainable" require "delayer/deferred/deferredable/graph" require "delayer/deferred/deferredable/node_sequence" require "delayer/deferred/deferredable/trigger" delayer-deferred-2.0.0/lib/delayer/deferred/response.rb0000644000004100000410000000046213124154450023103 0ustar www-datawww-data# -*- coding: utf-8 -*- module Delayer::Deferred::Response class Base attr_reader :value def initialize(value) @value = value end def ng? !ok? end end class Ok < Base def ok? true end end class Ng < Base def ok? false end end end delayer-deferred-2.0.0/lib/delayer/deferred/deferred.rb0000644000004100000410000000037413124154450023027 0ustar www-datawww-data# -*- coding: utf-8 -*- require "delayer/deferred/promise" require "delayer/deferred/chain" require "delayer/deferred/deferredable" require "delayer/deferred/worker" require "delayer/deferred/version" module Delayer::Deferred Deferred = Promise end delayer-deferred-2.0.0/lib/delayer/deferred/result_container.rb0000644000004100000410000000025313124154450024623 0ustar www-datawww-data# -*- coding: utf-8 -*- Delayer::Deferred::ResultContainer = Struct.new(:success_flag, :value) do def ok? success_flag end def ng? !success_flag end end delayer-deferred-2.0.0/lib/delayer/deferred/deferredable/0000755000004100000410000000000013124154450023322 5ustar www-datawww-datadelayer-deferred-2.0.0/lib/delayer/deferred/deferredable/node_sequence.rb0000644000004100000410000001076313124154450026473 0ustar www-datawww-data# -*- coding: utf-8 -*- require 'delayer/deferred/error' require 'thread' module Delayer::Deferred::Deferredable module NodeSequence class Sequence attr_reader :name def initialize(name) @name = name.to_sym @map = {} @exceptions = Hash.new(Delayer::Deferred::SequenceError) end def add(seq, flow = seq.name) @map[flow] = seq self end def exception(exc, flow) @exceptions[flow] = exc self end def pull(flow) if @map.has_key?(flow.to_sym) @map[flow.to_sym] else raise @exceptions[flow.to_sym], "Invalid sequence flow `#{name}' to `#{flow}'." end end def inspect "#<#{self.class}: #{name}>" end end FRESH = Sequence.new(:fresh) CONNECTED = Sequence.new(:connected) # 子がいる、未実行 RESERVED = Sequence.new(:reserved) # 実行キュー待ち RESERVED_C= Sequence.new(:reserved) # 実行キュー待ち(子がいる) RUN = Sequence.new(:run) # 実行中 RUN_C = Sequence.new(:run) # 実行中(子がいる) PASS = Sequence.new(:pass) # パス中 PASS_C = Sequence.new(:pass) # パス中 AWAIT = Sequence.new(:await) # Await中 AWAIT_C = Sequence.new(:await) # Await中(子がいる) GRAFT = Sequence.new(:graft) # 戻り値がAwaitableの時 GRAFT_C = Sequence.new(:graft) # 戻り値がAwaitableの時(子がいる) CALL_CHILD= Sequence.new(:call_child) # 完了、子がいる STOP = Sequence.new(:stop) # 完了、子なし WAIT = Sequence.new(:wait) # 完了、オブザーバ登録済み BURST_OUT = Sequence.new(:burst_out) # 完了、オブザーバ登録済み、子追加済み ROTTEN = Sequence.new(:rotten).freeze # 終了 GENOCIDE = Sequence.new(:genocide).freeze# この地ではかつて大量虐殺があったという。 FRESH .add(CONNECTED, :get_child) .add(RESERVED, :reserve) .add(GENOCIDE).freeze CONNECTED .add(RESERVED_C, :reserve) .exception(Delayer::Deferred::MultipleAssignmentError, :get_child) .add(GENOCIDE).freeze RESERVED .add(RUN, :activate) .add(RESERVED_C, :get_child) .add(GENOCIDE).freeze RESERVED_C .add(RUN_C, :activate) .exception(Delayer::Deferred::MultipleAssignmentError, :get_child) .add(GENOCIDE).freeze RUN .add(RUN_C, :get_child) .add(PASS) .add(AWAIT, :await) .add(STOP, :complete) .add(GENOCIDE).freeze RUN_C .add(PASS_C) .add(AWAIT_C, :await) .add(CALL_CHILD, :complete) .exception(Delayer::Deferred::MultipleAssignmentError, :get_child) .add(GENOCIDE).freeze PASS .add(PASS_C, :get_child) .add(RUN, :resume) .add(GENOCIDE).freeze PASS_C .add(RUN_C, :resume) .add(GENOCIDE).freeze AWAIT .add(RUN, :resume) .add(AWAIT_C, :get_child) .add(GENOCIDE).freeze AWAIT_C .add(RUN_C, :resume) .exception(Delayer::Deferred::MultipleAssignmentError, :get_child) .add(GENOCIDE).freeze CALL_CHILD .add(GRAFT_C, :await) .add(ROTTEN, :called) .add(GENOCIDE).freeze GRAFT .add(STOP, :resume) .add(GRAFT_C, :get_child) .add(GENOCIDE).freeze GRAFT_C .add(CALL_CHILD, :resume) .add(GENOCIDE).freeze STOP .add(GRAFT, :await) .add(WAIT, :gaze) .add(GENOCIDE).freeze WAIT .add(BURST_OUT, :get_child) .add(GENOCIDE).freeze BURST_OUT .add(ROTTEN, :called) .add(GENOCIDE).freeze SEQUENCE_LOCK = Monitor.new def sequence @sequence ||= FRESH end # このメソッドはスレッドセーフです def change_sequence(flow, &block) SEQUENCE_LOCK.synchronize do old_seq = sequence new_seq = @sequence = sequence.pull(flow) (@seq_logger ||= [old_seq]) << new_seq if block result = block.() on_sequence_changed(old_seq, flow, new_seq) result else on_sequence_changed(old_seq, flow, new_seq) nil end end end def on_sequence_changed(old_seq, flow, new_seq) end def activated? ![FRESH, CONNECTED, RUN, RUN_C].include?(sequence) end def spoiled? sequence == ROTTEN || sequence == GENOCIDE end end end delayer-deferred-2.0.0/lib/delayer/deferred/deferredable/awaitable.rb0000644000004100000410000000146413124154450025605 0ustar www-datawww-data# -*- coding: utf-8 -*- module Delayer::Deferred::Deferredable module Awaitable # _self_ が終了して結果が出るまで呼び出し側のDeferredを停止し、 _self_ の結果を返す。 # 呼び出し側はDeferredブロック内でなければならないが、 _Deferred#next_ を使わずに # 直接戻り値を得ることが出来る。 # _self_ が失敗した場合は、呼び出し側のDeferredの直近の _trap_ ブロックが呼ばれる。 def +@ response = Fiber.yield(Delayer::Deferred::Request::Await.new(self)) if response.ok? response.value else Delayer::Deferred.fail(response.value) end end def enter_await change_sequence(:await) end def exit_await change_sequence(:resume) end end end delayer-deferred-2.0.0/lib/delayer/deferred/deferredable/graph.rb0000644000004100000410000001123313124154450024750 0ustar www-datawww-data# -*- coding: utf-8 -*- module Delayer::Deferred::Deferredable =begin rdoc graphvizによってChainableなDeferredをDOT言語形式でダンプする機能を追加するmix-in。 いずれかのノードに対して _graph_ メソッドを呼ぶと、再帰的に親子を全て辿り、digraphの断片の文字列を得ることが出来る。 == 出力例 20892180 [shape=egg,label="#.Promise\n(reserved)"] 20892480 [shape=box,label="test/thread_test.rb:53\n(connected)"] 20891440 [shape=diamond,label="test/thread_test.rb:56\n(fresh)"] 20892480 -> 20891440 20892180 -> 20892480 =end module Graph # この一連のDeferredチェインの様子を、DOT言語フォーマットで出力する # ==== Args # [child_only:] # _true_ なら、このノードとその子孫のみを描画する。 # _false_ なら、再帰的に親を遡り、そこから描画を開始する。 # [output:] # このオブジェクトに、 _<<_ メソッドで内容が書かれる。 # 省略した場合は、戻り値が _String_ になる。 # ==== Return # [String] DOT言語によるグラフ # [output:] 引数 output: に指定されたオブジェクト def graph(child_only: false, output: String.new) if child_only output << "digraph Deferred {\n".freeze Enumerator.new{ |yielder| graph_child(output: yielder) }.lazy.each{|l| output << "\t#{l}\n" } output << '}'.freeze else ancestor.graph(child_only: true, output: output) end end # Graph.graph の結果を内容とする一時ファイルを作成して返す。 # ただし、ブロックを渡された場合は、一時ファイルを引数にそのブロックを一度だけ実行し、ブロックの戻り値をこのメソッドの戻り値とする。 # ==== Args # [&block] 一時ファイルを利用する処理 # ==== Return # [Tempfile] ブロックを指定しなかった場合。作成された一時ファイルオブジェクト # [Object] ブロックが指定された場合。ブロックの実行結果。 def graph_save(permanent: false, &block) if block Tempfile.open{|tmp| graph(output: tmp) tmp.seek(0) block.(tmp) } else tmp = Tempfile.open graph(output: tmp).tap{|t|t.seek(0)} end end # 画像ファイルとしてグラフを書き出す。 # dotコマンドが使えないと失敗する。 # ==== Args # [format:] 画像の拡張子 # ==== Return # [String] 書き出したファイル名 def graph_draw(dir: '/tmp', format: 'svg'.freeze) graph_save do |dotfile| base = File.basename(dotfile.path) dest = File.join(dir, "#{base}.#{format}") system("dot -T#{format} #{dotfile.path} -o #{dest}") dest end end # このノードとその子全てのDeferredチェインの様子を、DOT言語フォーマットで出力する。 # Delayer::Deferred::Deferredable::Graph#graph の内部で利用されるため、将来このメソッドのインターフェイスは変更される可能性がある。 def graph_child(output:) output << graph_mynode if has_child? @child.graph_child(output: output) output << "#{__id__} -> #{@child.__id__}" end if has_awaited? awaited.each do |awaitable| if awaitable.is_a?(Delayer::Deferred::Deferredable::Chainable) awaitable.ancestor.graph_child(output: output) else label = "#{awaitable.class}" output << "#{awaitable.__id__} [shape=oval,label=#{label.inspect}]" end output << "#{awaitable.__id__} -> #{__id__} [label = \"await\", style = \"dotted\"]" end end nil end private # このノードを描画する時の形の名前を文字列で返す。 # 以下のページにあるような、graphvizで取り扱える形の中から選ぶこと。 # http://www.graphviz.org/doc/info/shapes.html def graph_shape 'oval'.freeze end # このノードの形などをDOT言語の断片で返す。 # このメソッドをオーバライドすることで、描画されるノードの見た目を自由に変更することが出来る。 # ただし、簡単な変更だけなら別のメソッドをオーバライドするだけで可能なので、このmix-inの他のメソッドも参照すること。 def graph_mynode label = "#{node_name}\n(#{sequence.name})" "#{__id__} [shape=#{graph_shape},label=#{label.inspect}]" end end end delayer-deferred-2.0.0/lib/delayer/deferred/deferredable/chainable.rb0000644000004100000410000001075013124154450025560 0ustar www-datawww-data# -*- coding: utf-8 -*- require "delayer/deferred/deferredable/awaitable" require "delayer/deferred/deferredable/graph" require "delayer/deferred/deferredable/node_sequence" module Delayer::Deferred::Deferredable module Chainable include Awaitable include Graph include NodeSequence attr_reader :child # このDeferredが成功した場合の処理を追加する。 # 新しいDeferredのインスタンスを返す。 # このメソッドはスレッドセーフです。 # TODO: procが空のとき例外を発生させる def next(&proc) add_child(Delayer::Deferred::Chain::Next.new(&proc)) end alias deferred next # このDeferredが失敗した場合の処理を追加する。 # 新しいDeferredのインスタンスを返す。 # このメソッドはスレッドセーフです。 # TODO: procが空のとき例外を発生させる def trap(&proc) add_child(Delayer::Deferred::Chain::Trap.new(&proc)) end alias error trap # この一連のDeferredをこれ以上実行しない。 # このメソッドはスレッドセーフです。 def cancel change_sequence(:genocide) unless spoiled? end def has_child? child ? true : false end # 子を追加する。 # _Delayer::Deferred::Chainable_ を直接指定できる。通常外部から呼ぶときは _next_ か _trap_ メソッドを使うこと。 # このメソッドはスレッドセーフです。 # ==== Args # [chainable] 子となるDeferred # ==== Return # 必ず _chainable_ を返す # ==== Raise # [Delayer::Deferred::SequenceError] # 既に子が存在している場合 def add_child(chainable) change_sequence(:get_child) do chainable.parent = self @child = chainable end end # 子が追加された時に一度だけコールバックするオブジェクトを登録する。 # observerと言っているが、実際には _Delayer::Deferred::Worker_ を渡して利用している。 # このメソッドはスレッドセーフです。 # ==== Args # [observer] pushメソッドを備えているもの。引数に _@child_ の値が渡される # ==== Return # self def add_child_observer(observer) change_sequence(:gaze) do @child_observer = observer end self end def awaited @awaited ||= [].freeze end def has_awaited? not awaited.empty? end def add_awaited(awaitable) @awaited = [*awaited, awaitable].freeze self end # activateメソッドを呼ぶDelayerジョブを登録する寸前に呼ばれる。 def reserve_activate change_sequence(:reserve) end def enter_pass change_sequence(:pass) end def exit_pass change_sequence(:resume) end protected # 親を再帰的に辿り、一番最初のノードを返す。 # 親が複数見つかった場合は、それらを返す。 def ancestor if @parent @parent.ancestor else self end end # cancelとかデバッグ用のコールグラフを得るために親を登録しておく。 # add_childから呼ばれる。 def parent=(chainable) @parent = chainable end private def call_child_observer if has_child? and defined?(@child_observer) change_sequence(:called) @child_observer.push(@child) end end def on_sequence_changed(old_seq, flow, new_seq) case new_seq when NodeSequence::BURST_OUT call_child_observer when NodeSequence::GENOCIDE @parent.cancel if defined?(@parent) and @parent when NodeSequence::RESERVED_C, NodeSequence::RUN_C, NodeSequence::PASS_C, NodeSequence::AWAIT_C, NodeSequence::GRAFT_C if !has_child? notice "child: #{@child.inspect}" raise Delayer::Deferred::SequenceError.new("Sequence changed `#{old_seq.name}' to `#{flow}', but it has no child") end end end # ノードの名前。サブクラスでオーバライドし、ノードが定義されたファイルの名前や行数などを入れておく。 def node_name self.class.to_s end def graph_mynode if defined?(@seq_logger) label = "#{node_name}\n(#{@seq_logger.map(&:name).join('→')})" else label = "#{node_name}\n(#{sequence.name})" end "#{__id__} [shape=#{graph_shape},label=#{label.inspect}]" end end end delayer-deferred-2.0.0/lib/delayer/deferred/deferredable/trigger.rb0000644000004100000410000000171513124154450025316 0ustar www-datawww-data# -*- coding: utf-8 -*- require "delayer/deferred/deferredable/chainable" require "delayer/deferred/deferredable/node_sequence" require "delayer/deferred/response" module Delayer::Deferred::Deferredable =begin rdoc Promiseなど、親を持たず、自身がWorkerを作成できるもの。 =end module Trigger include NodeSequence include Chainable # Deferredを直ちに実行する。 # このメソッドはスレッドセーフです。 def call(value = nil) execute(Delayer::Deferred::Response::Ok.new(value)) end # Deferredを直ちに失敗させる。 # このメソッドはスレッドセーフです。 def fail(exception = nil) execute(Delayer::Deferred::Response::Ng.new(exception)) end private def execute(value) worker = Delayer::Deferred::Worker.new(delayer: self.class.delayer, initial: value) worker.push(self) end end end delayer-deferred-2.0.0/lib/delayer/deferred/thread.rb0000644000004100000410000000301013124154450022504 0ustar www-datawww-data# -*- coding: utf-8 -*- require "delayer" require "delayer/deferred/deferredable/awaitable" class Thread include ::Delayer::Deferred::Deferredable::Awaitable def self.delayer Delayer end # このDeferredが成功した場合の処理を追加する。 # 新しいDeferredのインスタンスを返す。 # このメソッドはスレッドセーフです。 # TODO: procが空のとき例外を発生させる def next(name: caller_locations(1,1).first.to_s, &proc) add_child(Delayer::Deferred::Chain::Next.new(&proc), name: name) end alias deferred next # このDeferredが失敗した場合の処理を追加する。 # 新しいDeferredのインスタンスを返す。 # このメソッドはスレッドセーフです。 # TODO: procが空のとき例外を発生させる def trap(name: caller_locations(1,1).first.to_s, &proc) add_child(Delayer::Deferred::Chain::Trap.new(&proc), name: name) end alias error trap def add_child(chainable, name: caller_locations(1,1).first.to_s) __gen_promise(name).add_child(chainable) end private def __gen_promise(name) promise = self.class.delayer.Promise.new(true, name: name) Thread.new(self) do |tt| __promise_callback(tt, promise) end promise end def __promise_callback(tt, promise) begin result = tt.value self.class.delayer.new do promise.call(result) end rescue Exception => err self.class.delayer.new do promise.fail(err) end end end end delayer-deferred-2.0.0/lib/delayer/deferred/request.rb0000644000004100000410000000350213124154450022733 0ustar www-datawww-data# -*- coding: utf-8 -*- # -*- coding: utf-8 -*- module Delayer::Deferred::Request class Base attr_reader :value def initialize(value) @value = value end end =begin rdoc Fiberが次のWorkerを要求している時に返す値。 新たなインスタンスは作らず、 _NEXT_WORKER_ にあるインスタンスを使うこと。 =end class NextWorker < Base # _deferred_ に渡された次のChainableに、 _deferred_ の戻り値を渡す要求を出す。 # ==== Args # [deferred] 実行が完了したDeferred 。次のDeferredとして _deferred.child_ を呼び出す # [worker] このDeferredチェインを実行しているWorker def accept_request(worker:, deferred:) if deferred.has_child? worker.push(deferred.child) else deferred.add_child_observer(worker) end end end =begin rdoc Chainable#+@ が呼ばれた時に、一旦そこで処理を止めるためのリクエスト。 _value_ には、実行完了を待つDeferredが入っている。 ==== わかりやすい! accept_requestメソッドの引数のdeferred { +value } =end class Await < Base alias_method :foreign_deferred, :value def accept_request(worker:, deferred:) deferred.enter_await foreign_deferred.add_child(Delayer::Deferred::Chain::Await.new(worker: worker, deferred: deferred)) end end =begin rdoc 一旦処理を中断して、Delayerキューに並び直すためのリクエスト。 Tools#pass から利用される。 新たなインスタンスは作らず、 _PASS_ にあるインスタンスを使うこと。 =end class Pass < Base def accept_request(worker:, deferred:) deferred.enter_pass worker.resume_pass(deferred) end end NEXT_WORKER = NextWorker.new(nil).freeze PASS = Pass.new(nil).freeze end delayer-deferred-2.0.0/lib/delayer/deferred/enumerator.rb0000644000004100000410000000041513124154450023424 0ustar www-datawww-data# -*- coding: utf-8 -*- require "delayer" require "delayer/deferred/deferred" class Enumerator def deach(delayer=Delayer, &proc) delayer.Deferred.new.next do self.each do |node| delayer.Deferred.pass proc.(node) end end end end delayer-deferred-2.0.0/lib/delayer/deferred/chain.rb0000644000004100000410000000034413124154450022326 0ustar www-datawww-data# -*- coding: utf-8 -*- module Delayer::Deferred module Chain; end end require "delayer/deferred/chain/await" require "delayer/deferred/chain/base" require "delayer/deferred/chain/next" require "delayer/deferred/chain/trap" delayer-deferred-2.0.0/lib/delayer/deferred/version.rb0000644000004100000410000000010113124154450022720 0ustar www-datawww-datamodule Delayer module Deferred VERSION = "2.0.0" end end delayer-deferred-2.0.0/lib/delayer/deferred/worker.rb0000644000004100000410000000661413124154450022563 0ustar www-datawww-data# -*- coding: utf-8 -*- require "delayer/deferred/request" require "delayer/deferred/response" module Delayer::Deferred =begin rdoc Deferredを実行するためのWorker。Deferredチェインを実行するFiberを 管理する。 == pushに渡すオブジェクトについて Worker#push に渡す引数は、activateメソッドを実装している必要がある。 === activate(response) ==== Args response :: Delayer::Deferred::Response::Base Deferredに渡す値 ==== Returns [Delayer::Deferred::Response::Base] これを返すと、値の自動変換が行われないため、意図的に失敗させたり、Deferredを次のブロックに伝搬させることができる。 [Delayer::Deferred::Chainable] 戻り値のDeferredが終わるまでWorkerの処理を停止する。 再開された時、結果は戻り値のDeferredの結果に置き換えられる。 [else] _Delayer::Deferred::Response::Ok.new_ の引数に渡され、その結果が利用される =end class Worker def initialize(delayer:, initial:) @delayer, @initial = delayer, initial end def push(deferred) deferred.reserve_activate @delayer.new do next if deferred.spoiled? begin fiber.resume(deferred).accept_request(worker: self, deferred: deferred) rescue Delayer::Deferred::SequenceError => err err.deferred = deferred raise end end nil end # Awaitから復帰した時に呼ばれる。 # ==== Args # [response] Awaitの結果(Delayer::Deferred::Response::Base) # [deferred] 現在実行中のDeferred def give_response(response, deferred) @delayer.new do next if deferred.spoiled? deferred.exit_await fiber.resume(response).accept_request(worker: self, deferred: deferred) end nil end # Tools#pass から復帰した時に呼ばれる。 # ==== Args # [deferred] 現在実行中のDeferred def resume_pass(deferred) deferred.exit_pass @delayer.new do next if deferred.spoiled? fiber.resume(nil).accept_request(worker: self, deferred: deferred) end end private def fiber @fiber ||= Fiber.new{|response| loop do response = wait_and_activate(response) case response.value when Delayer::Deferred::SequenceError raise response.value end end }.tap{|f| f.resume(@initial); @initial = nil } end def wait_and_activate(argument) response = catch(:success) do failed = catch(:__deferredable_fail) do begin if argument.value.is_a? Deferredable::Awaitable throw :success, +argument.value else defer = Fiber.yield(Request::NEXT_WORKER) res = defer.activate(argument) if res.is_a? Delayer::Deferred::Deferredable::Awaitable defer.add_awaited(res) end end throw :success, res rescue Exception => err throw :__deferredable_fail, err end end Response::Ng.new(failed) end if response.is_a?(Response::Base) response else Response::Ok.new(response) end end end end delayer-deferred-2.0.0/lib/delayer/deferred/tools.rb0000644000004100000410000000477313124154450022416 0ustar www-datawww-data# -*- coding: utf-8 -*- require 'delayer/deferred/error' module Delayer::Deferred module Tools def next(&proc) new.next(&proc) end def trap(&proc) new.trap(&proc) end # 実行中のDeferredを失敗させる。raiseと違って、Exception以外のオブジェクトをtrap()に渡すことができる。 # Deferredのnextとtrapの中でだけ呼び出すことができる。 # ==== Args # [value] trap()に渡す値 # ==== Throw # :__deferredable_fail をthrowする def fail(value) throw(:__deferredable_fail, value) end # 実行中のDeferredを、Delayerのタイムリミットが来ている場合に限り一旦中断する。 # 長期に渡る可能性のある処理で、必要に応じて他のタスクを先に実行してもよい場合に呼び出す。 def pass Fiber.yield(Request::PASS) if delayer.expire? end # 複数のdeferredを引数に取って、それら全ての実行が終了したら、 # その結果を引数の順番通りに格納したArrayを引数に呼ばれるDeferredを返す。 # 引数のDeferredが一つでも失敗するとこのメソッドの返すDeferredも失敗する。 # ==== Args # [*args] 終了を待つDeferredオブジェクト # ==== Return # Deferred def when(*args) return self.next{[]} if args.empty? args = args.flatten args.each_with_index{|d, index| unless d.is_a?(Deferredable::Chainable) || d.is_a?(Deferredable::Awaitable) raise TypeError, "Argument #{index} of Deferred.when must be #{Deferredable::Chainable}, but given #{d.class}" end if d.respond_to?(:has_child?) && d.has_child? raise "Already assigned child for argument #{index}" end } defer, *follow = *args defer.next{|res| [res, *follow.map{|d| +d }] } end # Kernel#systemを呼び出して、コマンドが成功たら成功するDeferredを返す。 # 失敗した場合、trap{}ブロックには $? の値(Process::Status)か、例外が発生した場合それが渡される # ==== Args # [*args] Kernel#spawn の引数 # ==== Return # Deferred def system(*args) delayer.Deferred.Thread.new { Process.waitpid2(Kernel.spawn(*args)) }.next{|_pid, status| if status && status.success? status else raise ForeignCommandAborted.new("command aborted: #{args.join(' ')}", process: status) end } end end end delayer-deferred-2.0.0/lib/delayer/deferred/enumerable.rb0000644000004100000410000000046713124154450023371 0ustar www-datawww-data# -*- coding: utf-8 -*- require "delayer" require "delayer/deferred/enumerator" module Enumerable # 遅延each。あとで実行されるし、あんまりループに時間がかかるようなら一旦ループを終了する def deach(delayer=Delayer, &proc) to_enum.deach(delayer, &proc) end end delayer-deferred-2.0.0/lib/delayer/deferred/error.rb0000644000004100000410000000073413124154450022400 0ustar www-datawww-data# -*- coding: utf-8 -*- module Delayer::Deferred Error = Class.new(StandardError) class ForeignCommandAborted < Error attr_reader :process def initialize(message, process:) super(message) @process = process end end SequenceError = Class.new(Error) do attr_accessor :deferred def initialize(message, deferred: nil) super(message) @deferred = deferred end end MultipleAssignmentError = Class.new(SequenceError) end delayer-deferred-2.0.0/test/0000755000004100000410000000000013124154450015722 5ustar www-datawww-datadelayer-deferred-2.0.0/test/helper.rb0000644000004100000410000000034613124154450017531 0ustar www-datawww-datarequire 'bundler/setup' require 'securerandom' require 'set' require 'timeout' require_relative 'testutils' require 'simplecov' SimpleCov.start do add_filter "/test/" end require 'delayer/deferred' require 'minitest/autorun' delayer-deferred-2.0.0/test/deferred_profiler.rb0000644000004100000410000000113313124154450021727 0ustar www-datawww-data# -*- coding: utf-8 -*- require 'bundler/setup' require 'delayer/deferred' require 'ruby-prof' require_relative 'testutils.rb' extend TestUtils n = 1000 RubyProf.start delayer = Delayer.generate_class eval_all_events(delayer) do n.times do delayer.Deferred.new.next{|x| x }.trap{|x| x } end end result = RubyProf.stop printer = RubyProf::CallTreePrinter.new(result) path = File.expand_path(File.join(__dir__, '..', 'profile', Time.new.strftime('%Y-%m-%d-%H%M%S'))) FileUtils.mkdir_p(path) puts "profile: writing to #{path}" printer.print(path: path) puts "profile: done." delayer-deferred-2.0.0/test/enumerable_test.rb0000644000004100000410000000265613124154450021436 0ustar www-datawww-data# -*- coding: utf-8 -*- require_relative 'helper' describe(Enumerable) do include TestUtils before do @delayer = Delayer.generate_class end describe "deach" do it "iterate Array" do sum = 0 eval_all_events(@delayer) do (1..10000).to_a.deach(@delayer) do |digit| sum += digit end end assert_equal 50005000, sum end it "iterate infinite Enumerator" do log = [] finish = failure = nil @delayer = Delayer.generate_class(expire: 0.1) fib = Enumerator.new do |yielder| a = 1 b = 1 loop do c = a + b yielder << a a, b = b, c end end Timeout.timeout(1) { fib.deach(@delayer) {|digit| log << digit }.next{ finish = true }.trap {|exception| failure = exception } @delayer.run } refute failure refute finish, "Enumerable#deach won't call next block" refute log.empty?, "Executed deach block" log_size = log.size sample_size = [156, log_size].min assert_equal fib.take(sample_size), log.take(sample_size), "deach block takes collect arguments" @delayer.run refute failure refute finish, "Enumerable#deach won't call next block" assert log.size > log_size, "Restart iteration if call Delayer#run (first #{log_size} iterations, second #{log.size})" end end end delayer-deferred-2.0.0/test/promise_test.rb0000644000004100000410000000433313124154450020767 0ustar www-datawww-data# -*- coding: utf-8 -*- require_relative 'helper' describe(Delayer::Deferred::Promise) do include TestUtils before do Delayer.default = Delayer.generate_class @delayer = Delayer.generate_class end describe 'get instance' do it 'default delayer' do assert_instance_of Delayer::Deferred::Promise, Delayer::Deferred::Promise.new end it 'another delayer' do promise = @delayer.Promise.new assert_instance_of @delayer.Promise, promise end describe "with block" do before do @promise = @delayer.Promise.new{ ; } end it 'was generated' do assert_kind_of Delayer::Deferred::Chain::Next, @promise end it "doesn't have child" do refute @promise.has_child? end end end describe 'chain' do before do @promise = @delayer.Promise.new(true) end describe 'next' do before do @record = nil @chain = @promise.next{|x| @record = x + 1 } end it 'should execute next block if called promise#call' do val = rand(1000) eval_all_events(@delayer) do @promise.call(val) end assert_equal val + 1, @record, ->{ "next block did not executed.\n[[#{@chain.graph_draw}]]" } end it "shouldn't execute next block if called promise#fail" do val = rand(1000) eval_all_events(@delayer) do @promise.fail(val) end refute_equal val + 1, @record, ->{ "next block did executed.\n[[#{@chain.graph_draw}]]" } end end describe 'trap' do before do @record = nil @chain = @promise.trap{|x| @record = x + 1 } end it 'should execute trap block if called promise#fail' do val = rand(1000) eval_all_events(@delayer) do @promise.fail(val) end assert_equal val + 1, @record, ->{ "trap block did not executed.\n[[#{@chain.graph_draw}]]" } end it "shouldn't execute trap block if called promise#call" do val = rand(1000) eval_all_events(@delayer) do @promise.call(val) end refute_equal val + 1, @record, ->{ "trap block did executed.\n[[#{@chain.graph_draw}]]" } end end end end delayer-deferred-2.0.0/test/testutils.rb0000644000004100000410000000036513124154450020313 0ustar www-datawww-datamodule TestUtils def eval_all_events(delayer=Delayer) native = Thread.list result = yield if block_given? while not(delayer.empty? and (Thread.list - native).empty?) delayer.run Thread.pass end result end end delayer-deferred-2.0.0/test/graph_test.rb0000644000004100000410000000254013124154450020410 0ustar www-datawww-data# -*- coding: utf-8 -*- require_relative 'helper' describe(Delayer::Deferred) do include TestUtils before do Delayer.default = Delayer.generate_class end describe 'auto execution Promise' do it 'should include Promise result of Promise.graph' do promise = Delayer::Deferred::Promise.new assert_includes promise.graph, 'egg', ->{"[[#{promise.graph_draw}]]"} assert_includes promise.graph, 'reserved', ->{"[[#{promise.graph_draw}]]"} end end describe 'Promise' do it 'should include Promise result of Promise.graph' do promise = Delayer::Deferred::Promise.new(true) assert_includes promise.graph, 'egg', ->{"[[#{promise.graph_draw}]]"} assert_includes promise.graph, 'fresh', ->{"[[#{promise.graph_draw}]]"} end end describe 'Chain' do it 'should include ' do promise = Delayer::Deferred::Promise.new.next{ ; } assert_includes promise.graph, 'graph_test.rb', ->{"[[#{promise.graph_draw}]]"} end end describe 'Awaiting' do it 'await' do promise_a = Delayer::Deferred::Promise.new(true).next{ |buf| buf << :a }.next{ |buf| buf << :b }.trap{ |buf| buf << :c } promise_b = Delayer::Deferred::Promise.new.next{ +promise_a << :e }.trap{ |buf| buf << :f } eval_all_events end end end delayer-deferred-2.0.0/test/deferred_benchmark.rb0000644000004100000410000000215513124154450022044 0ustar www-datawww-data# -*- coding: utf-8 -*- require 'benchmark' require 'bundler/setup' require 'delayer/deferred' require_relative 'testutils.rb' Benchmark.bmbm do |r| extend TestUtils n = 10000 r.report "construct" do delayer = Delayer.generate_class n.times do delayer.Deferred.new end end r.report "register next block" do delayer = Delayer.generate_class n.times do delayer.Deferred.new.next{|x| x } end end r.report "execute next block" do delayer = Delayer.generate_class eval_all_events(delayer) do n.times do delayer.Deferred.new.next{|x| x } end end end r.report "double next block" do delayer = Delayer.generate_class eval_all_events(delayer) do n.times do delayer.Deferred.new.next{|x| x }.next{|x| x } end end end r.report "trap block" do delayer = Delayer.generate_class eval_all_events(delayer) do n.times do delayer.Deferred.new.next{|x| x }.trap{|x| x } end end end end delayer-deferred-2.0.0/test/thread_test.rb0000644000004100000410000001167313124154450020565 0ustar www-datawww-data# -*- coding: utf-8 -*- require_relative 'helper' describe(Thread) do include TestUtils before do Delayer.default = Delayer.generate_class end describe 'Generic thread operation' do describe 'Exception in thread' do before do @error = Class.new(RuntimeError) @thread = Thread.new { raise @error } end it 'should takes the exception' do assert_raises(@error) do @thread.value end end end end it "defer with Deferred#next" do thread = succeed = result = false uuid = SecureRandom.uuid eval_all_events do Thread.new { thread = true uuid }.next do |param| succeed = true result = param end end assert thread, "Thread did not executed." assert succeed, "next block did not executed." assert_equal uuid, result end it "defer with another Delayer" do thread = succeed = failure = result = false uuid = SecureRandom.uuid delayer = Delayer.generate_class assert_equal delayer, delayer.Deferred.Thread.delayer eval_all_events(delayer) do delayer.Deferred.Thread.new { thread = true uuid }.next{ |param| succeed = true result = param }.trap{ |exception| failure = exception } end assert_equal false, failure, 'Unexpected failed.' assert thread, "Thread did not executed." assert succeed, "next block did not executed." assert_equal uuid, result end it "error handling" do delayer = Delayer.generate_class succeed = failure = recover = false uuid = SecureRandom.uuid eval_all_events(delayer) do delayer.Deferred.Thread.new { raise uuid }.next { succeed = true }.trap { |value| failure = value }.next { recover = true } end refute succeed, "Raised exception but it was executed successed route." assert_instance_of RuntimeError, failure, "trap block takes incorrect value" assert_equal uuid, failure.message, "trap block takes incorrect value" assert recover, "next block did not executed when after trap" end it "exception handling" do succeed = failure = recover = false delayer = Delayer.generate_class eval_all_events(delayer) do delayer.Deferred.Thread.new { raise 'error test' }.next { succeed = true }.trap { failure = true }.next { recover = true } end refute succeed, "Raised exception but it was executed successed route." assert failure, "trap block did not executed" assert recover, "next block did not executed when after trap" end it "wait end of Deferredable if Deferredable block returns Thread" do result = failure = false delayer = Delayer.generate_class uuid = SecureRandom.uuid node = eval_all_events(delayer) do delayer.Deferred.new.next{ delayer.Deferred.new.next{ delayer.Deferred.Thread.new{ uuid } } }.next{ |value| result = value }.trap{ |exception| failure = exception } end assert_equal uuid, result, ->{ "[[#{node.graph_draw}]]" } assert_equal false, failure end describe 'Race conditions' do it "calls Thread#next for running thread" do thread = succeed = result = false uuid = SecureRandom.uuid eval_all_events do lock = true th = Thread.new { while lock; Thread.pass end thread = true uuid } th.next do |param| succeed = true result = param end lock = false end assert thread, "Thread did not executed." assert succeed, "next block did not executed." assert_equal uuid, result end it "calls Thread#next for stopped thread" do thread = succeed = result = false uuid = SecureRandom.uuid eval_all_events do th = Thread.new { thread = true uuid } while th.alive?; Thread.pass end th.next do |param| succeed = true result = param end end assert thread, "Thread did not executed." assert succeed, "next block did not executed." assert_equal uuid, result end end it "wait ended Thread for +thread" do result = failure = false delayer = Delayer.generate_class uuid1, uuid2, uuid3 = SecureRandom.uuid, SecureRandom.uuid, SecureRandom.uuid eval_all_events(delayer) do delayer.Deferred.new.next{ [ +delayer.Deferred.Thread.new{ uuid1 }, +delayer.Deferred.Thread.new{ uuid2 }, +delayer.Deferred.Thread.new{ uuid3 } ] }.next{ |value| result = value }.trap{ |exception| failure = exception } end assert_equal false, failure assert_instance_of Array, result assert_equal uuid1, result[0] assert_equal uuid2, result[1] assert_equal uuid3, result[2] end end delayer-deferred-2.0.0/test/deferred_test.rb0000644000004100000410000002277413124154450021102 0ustar www-datawww-data# -*- coding: utf-8 -*- require_relative 'helper' describe(Delayer::Deferred) do include TestUtils before do Delayer.default = Delayer.generate_class end it "defer with Deferred#next" do succeed = failure = false eval_all_events do Delayer::Deferred::Deferred.new.next{ succeed = true }.trap{ |exception| failure = exception } end assert_equal false, failure assert succeed, "Deferred did not executed." end it "defer with another Delayer" do succeed = failure = false delayer = Delayer.generate_class eval_all_events(delayer) do delayer.Deferred.new.next { succeed = true }.trap{ |exception| failure = exception } end assert_equal false, failure assert succeed, "Deferred did not executed." end it "error handling" do succeed = failure = recover = false uuid = SecureRandom.uuid eval_all_events do Delayer::Deferred::Deferred.new.next { Delayer::Deferred.fail(uuid) }.next { succeed = true }.trap { |value| failure = value }.next { recover = true } end refute succeed, "Raised exception but it was executed successed route." assert_equal uuid, failure, "trap block takes incorrect value" assert recover, "next block did not executed when after trap" end it "exception handling" do succeed = failure = recover = false eval_all_events do Delayer::Deferred::Deferred.new.next { raise 'error test' }.next { succeed = true }.trap { failure = true }.next { recover = true } end refute succeed, "Raised exception but it was executed successed route." assert failure, "trap block did not executed" assert recover, "next block did not executed when after trap" end it "wait end of Deferredable if Deferredable block returns Deferredable" do result = failure = false delayer = Delayer.generate_class uuid = SecureRandom.uuid eval_all_events(delayer) do delayer.Deferred.new.next{ delayer.Deferred.new.next{ uuid } }.next{ |value| result = value }.trap{ |exception| failure = exception } end assert_equal false, failure assert_equal uuid, result end it "join Deferredable#next after end of previous Deferredable" do succeed = failure = false delayer = Delayer.generate_class deferredable = eval_all_events(delayer) do delayer.Deferred.new.next { true } end eval_all_events(delayer) do deferredable.next{ |value| succeed = value }.trap{ |exception| failure = exception } end assert_equal false, failure assert succeed, "Deferred did not executed." end it "assign twice" do succeed = false delayer = Delayer.generate_class assert_raises(Delayer::Deferred::MultipleAssignmentError) do eval_all_events(delayer) do defer = delayer.Deferred.new.next { succeed = 0 } defer.next{ succeed = 1 } defer.next{ succeed = 2 } end end end describe "Deferred.when" do it "give 3 deferred" do result = failure = false delayer = Delayer.generate_class eval_all_events(delayer) do delayer.Deferred.when( delayer.Deferred.new.next{ 1 }, delayer.Deferred.new.next{ 2 }, delayer.Deferred.new.next{ 3 } ).next{ |values| result = values }.trap{ |exception| failure = exception } end assert_equal false, failure assert_equal [1,2,3], result end it "default deferred" do result = failure = false eval_all_events do Delayer::Deferred::Deferred.when( Delayer::Deferred::Deferred.new.next{ 1 }, Delayer::Deferred::Deferred.new.next{ 2 }, Delayer::Deferred::Deferred.new.next{ 3 } ).next{ |values| result = values }.trap{ |exception| failure = exception } end assert_equal false, failure assert_equal [1,2,3], result end it "give that is not Deferredable" do result = failure = false delayer = Delayer.generate_class assert_raises(TypeError) do eval_all_events(delayer) do delayer.Deferred.when( delayer.Deferred.new.next{ 1 }, 2, delayer.Deferred.new.next{ 3 } ).next{ |values| result = values }.trap{ |exception| failure = exception } end end assert_equal false, failure assert_equal false, result end it "execute trap block if failed" do result = failure = false delayer = Delayer.generate_class eval_all_events(delayer) do delayer.Deferred.when( delayer.Deferred.new.next{ 1 }, delayer.Deferred.new.next{ raise }, delayer.Deferred.new.next{ 3 } ).next{ |values| result = values }.trap{ |exception| failure = exception } end assert_kind_of RuntimeError, failure assert_equal false, result end it "no deferred given" do result = failure = false delayer = Delayer.generate_class eval_all_events(delayer) do delayer.Deferred.when().next{ |values| result = values }.trap{ |exception| failure = exception } end assert_equal false, failure assert_empty result end it "no deferred given for default delayer" do result = failure = false eval_all_events do Delayer::Deferred::Deferred.when().next{ |values| result = values }.trap{ |exception| failure = exception } end assert_equal false, failure assert_empty result end it "no deferred given for delayer module" do result = failure = false eval_all_events do Delayer::Deferred.when().next{ |values| result = values }.trap{ |exception| failure = exception } end assert_equal false, failure assert_empty result end end describe "cancel" do it "stops deferred chain" do succeed = failure = false delayer = Delayer.generate_class eval_all_events(delayer) do delayer.Deferred.new.next { succeed = true }.trap{ |exception| failure = exception }.cancel end assert_equal false, failure assert_equal false, succeed, "Deferred executed." end end describe 'recursive delayer' do it 'Deferred#next call in Deferred.next' do delayer = Delayer.generate_class buf = [] erra = errb = errc = nil eval_all_events(delayer) do delayer.Deferred.next{ buf << 'begin A' delayer.run buf << 'end A' }.trap{|err| erra = err } delayer.Deferred.next{ buf << 'begin B' delayer.run buf << 'end B' }.trap{|err| errb = err } delayer.Deferred.next{ buf << 'begin C' delayer.run buf << 'end C' }.trap{|err| errc = err } end refute erra refute errb refute errc assert_includes buf, 'begin A' assert_includes buf, 'end A' assert_includes buf, 'begin B' assert_includes buf, 'end B' assert_includes buf, 'begin C' assert_includes buf, 'end C' end end describe "Deferredable#system" do it "command successed" do succeed = failure = false delayer = Delayer.generate_class eval_all_events(delayer) do delayer.Deferred.system("/bin/sh", "-c", "exit 0").next{ |value| succeed = value }.trap{ |exception| failure = exception } end assert_equal false, failure assert succeed, "next block called" end it "command failed" do succeed = failure = false delayer = Delayer.generate_class eval_all_events(delayer) do delayer.Deferred.system("/bin/sh", "-c", "exit 1").next{ |value| succeed = value }.trap{ |exception| failure = exception } end refute succeed, "next block did not called" assert_instance_of Delayer::Deferred::ForeignCommandAborted, failure assert failure.process.exited?, "command exited" assert_equal 1, failure.process.exitstatus, "command exit status is 1" end end describe 'Deferredable#+@' do it 'stops +@ deferred chain, then it returns result after receiver completed' do delayer = Delayer.generate_class log = Array.new eval_all_events(delayer) do delayer.Deferred.new.next{ log << :a1 b = delayer.Deferred.new.next{ log << :b1 << +delayer.Deferred.new.next{ log << :c1 << +delayer.Deferred.new.next{ log << :d1 :d2 } :c2 } :b2 } log << :a2 << +b << :a3 } end assert_equal [:a1, :a2, :b1, :c1, :d1, :d2, :c2, :b2, :a3], log, 'incorrect call order' end it 'fails receiver of +@, then fails callee Deferred' do delayer = Delayer.generate_class log = Array.new eval_all_events(delayer) do delayer.Deferred.new.next{ log << :a1 b = delayer.Deferred.new.next{ log << :b1 delayer.Deferred.fail(:be) :b2 } log << :a2 << +b << :a3 }.trap do |err| log << :ae << err end end assert_equal [:a1, :a2, :b1, :ae, :be], log, 'incorrect call order' end end end delayer-deferred-2.0.0/.gitignore0000644000004100000410000000021213124154450016726 0ustar www-datawww-data/.bundle/ /.yardoc /Gemfile.lock /_yardoc/ /coverage/ /doc/ /pkg/ /spec/reports/ /tmp/ *.bundle *.so *.o *.a mkmf.log /vendor/ /Guardfile delayer-deferred-2.0.0/README.md0000644000004100000410000001605013124154450016224 0ustar www-datawww-data# Delayer::Deferred Delayerの、ブロックを実行キューにキューイングする機能を利用し、エラー処理やasync/awaitのような機能をサポートするライブラリです。 ## Installation Add this line to your application's Gemfile: ```ruby gem 'delayer-deferred' ``` And then execute: $ bundle Or install it yourself as: $ gem install delayer-deferred ## Usage ### The first step rubygemでインストールしたあと、requireします。 ```ruby require "delayer/deferred" ``` `Delayer::Deferred.new` が使えるようになります。ブロックを渡すと、Delayerのように後から(Delayer.runが呼ばれた時に)実行されます。 ```ruby Delayer.default = Delayer.generate_class # Delayerの準備 Delayer::Deferred.new { p "defer" } Delayer.run ``` ``` defer ``` `.next` メソッドで、前のブロックの実行が終わったら、その結果を受け取って次を実行することができます。 ```ruby Delayer.default = Delayer.generate_class # Delayerの準備 Delayer::Deferred.new { 1 + 1 }.next{ |sum| p sum } Delayer.run ``` ``` 2 ``` ### Error handling nextブロックの中で例外が発生した場合、次のtrapブロックまで処理が飛ばされます。 trapブロックは、その例外オブジェクトを引数として受け取ります。 ```ruby Delayer.default = Delayer.generate_class # Delayerの準備 Delayer::Deferred.new { 1 / 0 }.next{ |sum| p sum }.trap{ |exception| puts "Error occured!" p exception } Delayer.run ``` ``` Error occured! \# ``` 例外が発生すると、以降のnextブロックは無視され、例外が起こったブロック以降の最初のtrapブロックが実行されます。trapブロックの後にnextブロックがあればそれが実行されます。 `Delayer::Deferred.fail()` を使えば、例外以外のオブジェクトをtrapの引数に渡すこともできます。 ```ruby Delayer.default = Delayer.generate_class # Delayerの準備 Delayer::Deferred.new { Delayer::Deferred.fail("test error message") }.trap{ |exception| puts "Error occured!" p exception } Delayer.run ``` ``` Error occured! "test error message" ``` ### Thread Threadには、nextやtrapメソッドが実装されているので、Deferredのように扱うことができます。 ```ruby Delayer.default = Delayer.generate_class # Delayerの準備 Thread.new { 1 + 1 }.next{ |sum| p sum } Delayer.run ``` ``` 2 ``` この場合、nextやtrapのブロックは、全て `Delayer.run` メソッドが実行された側のThreadで実行されます。 ### Automatically Divide a Long Loop `Enumerable#deach`, `Enumerator#deach`はeachの変種で、Delayerのexpireの値よりループに時間がかかったら一旦処理を中断して、続きを実行するDeferredを新たに作ります。 ```ruby complete = false Delayer.default = Delayer.generate_class(expire: 0.1) # Delayerの準備 (1..100000).deach{ |digit| p digit }.next{ puts "complete" complete = true }.trap{ |exception| p exception complete = true } while !complete Delayer.run puts "divided" end ``` ``` 1 2 3 (中略) 25398 divided 25399 (中略) 100000 complete divided ``` 開発している環境では、25398までループした後、0.1秒経過したので一度処理が分断され、Delayer.runから処理が帰ってきています。 また、このメソッドはDeferredを返すので、ループが終わった後に処理をしたり、エラーを受け取ったりできます。 ### Pass to another Delayer Deferredのコンテキストの中で `Deferred.pass` を呼ぶと、そこで一旦処理が中断し、キューの最後に並び直します。 他のDelayerが処理され終わると `Deferred.pass` から処理が戻ってきて、再度そこから実行が再開されます。 `Deferred.pass` は常に処理を中断するわけではなく、Delayerの時間制限を過ぎている場合にのみ処理をブレークします。 用途としては `Enumerator#deach` が使えないようなループの中で毎回呼び出して、長時間処理をブロックしないようにするといった用途が考えられます。 `Enumerator#deach` は `Deferred.pass` を用いて作られています。 ### Combine Deferred `Deferred.when` は、引数に2つ以上のDeferredを受け取り、新たなDeferredを一つ返します。 引数のDeferredすべてが正常に終了したら、戻り値のDeferredのnextブロックが呼ばれ、whenの引数の順番通りに戻り値が渡されます。 複数のDeferredがあって、それらすべてが終了するのを待ち合わせる時に使うと良いでしょう。 ```ruby web_a = Thread.new{ open("http://example.com/a.html") } web_b = Thread.new{ open("http://example.com/b.html") } web_c = Thread.new{ open("http://example.com/c.html") } # 引数の順番は対応している Deferred.when(web_a, web_b, web_c).next do |a, b, c| ... end # 配列を渡すことも出来る Deferred.when([web_a, web_b, web_c]).next do |a, b, c| ... end ``` 引数のDeferredのうち、どれか一つでも失敗すると、直ちに `Deferred.when` の戻り値のtrapブロックが呼ばれます。trapの引数は、失敗したDeferredのそれです。 どれか一つでも失敗すると、他のDeferredが成功していたとしてもその結果は破棄されるということに気をつけてください。より細かく制御したい場合は、Async/Awaitを利用しましょう。 ```ruby divzero = Delayer::Deferred.new { 1 / 0 } web_a = Thread.new{ open("http://example.com/a.html") } Deferred.when(divzero, web_a).next{ puts 'success' }.trap{|err| p err } ``` ``` \# ``` ### Async/Await Deferred#next や Deferred#trap のブロック内では、Deferredable#+@ が使えます。非同期な処理を同期処理のように書くことができます。 +@を呼び出すと、呼び出し元のDeferredの処理が一時停止し、+@のレシーバになっているDeferredableが完了した後に処理が再開されます。また、戻り値はレシーバのDeferredableのそれになります。 ``` request = Thread.new{ open("http://mikutter.hachune.net/download/unstable.json").read } Deferred.next{ puts "最新の不安定版mikutterのバージョンは" response = JSON.parse(+request) puts response.first["version_string"] puts "です" } ``` `+request` が呼ばれた時、リクエスト完了まで処理は一時止まりますが、他にDelayerキューにジョブが溜まっていたら、そちらが実行されます。この機能を使わない場合は、HTTPレスポンスを受け取るまでDelayerの他のジョブは停止してしまいます。 ## Contributing 1. Fork it ( https://github.com/toshia/delayer-deferred/fork ) 2. Create your feature branch (`git checkout -b my-new-feature`) 3. Commit your changes (`git commit -am 'Add some feature'`) 4. Push to the branch (`git push origin my-new-feature`) 5. Create a new Pull Request