delayer-deferred 1.1.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +77 -3
- data/delayer-deferred.gemspec +1 -0
- data/lib/delayer/deferred.rb +11 -10
- data/lib/delayer/deferred/chain.rb +10 -0
- data/lib/delayer/deferred/chain/await.rb +43 -0
- data/lib/delayer/deferred/chain/base.rb +35 -0
- data/lib/delayer/deferred/chain/next.rb +16 -0
- data/lib/delayer/deferred/chain/trap.rb +16 -0
- data/lib/delayer/deferred/deferred.rb +4 -72
- data/lib/delayer/deferred/deferredable.rb +8 -158
- data/lib/delayer/deferred/deferredable/awaitable.rb +27 -0
- data/lib/delayer/deferred/deferredable/chainable.rb +155 -0
- data/lib/delayer/deferred/deferredable/graph.rb +118 -0
- data/lib/delayer/deferred/deferredable/node_sequence.rb +158 -0
- data/lib/delayer/deferred/deferredable/trigger.rb +34 -0
- data/lib/delayer/deferred/enumerator.rb +5 -7
- data/lib/delayer/deferred/error.rb +9 -0
- data/lib/delayer/deferred/promise.rb +78 -0
- data/lib/delayer/deferred/request.rb +61 -0
- data/lib/delayer/deferred/response.rb +26 -0
- data/lib/delayer/deferred/thread.rb +29 -14
- data/lib/delayer/deferred/tools.rb +20 -10
- data/lib/delayer/deferred/version.rb +1 -1
- data/lib/delayer/deferred/worker.rb +112 -0
- data/test/deferred_test.rb +14 -0
- data/test/enumerable_test.rb +1 -1
- data/test/graph_test.rb +52 -0
- data/test/helper.rb +8 -3
- data/test/promise_test.rb +91 -0
- data/test/thread_test.rb +9 -5
- metadata +35 -3
@@ -0,0 +1,27 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
|
3
|
+
module Delayer::Deferred::Deferredable
|
4
|
+
module Awaitable
|
5
|
+
|
6
|
+
# _self_ が終了して結果が出るまで呼び出し側のDeferredを停止し、 _self_ の結果を返す。
|
7
|
+
# 呼び出し側はDeferredブロック内でなければならないが、 _Deferred#next_ を使わずに
|
8
|
+
# 直接戻り値を得ることが出来る。
|
9
|
+
# _self_ が失敗した場合は、呼び出し側のDeferredの直近の _trap_ ブロックが呼ばれる。
|
10
|
+
def +@
|
11
|
+
response = Fiber.yield(Delayer::Deferred::Request::Await.new(self))
|
12
|
+
if response.ok?
|
13
|
+
response.value
|
14
|
+
else
|
15
|
+
Delayer::Deferred.fail(response.value)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def enter_await
|
20
|
+
change_sequence(:await)
|
21
|
+
end
|
22
|
+
|
23
|
+
def exit_await
|
24
|
+
change_sequence(:resume)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,155 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
require "delayer/deferred/deferredable/awaitable"
|
3
|
+
require "delayer/deferred/deferredable/graph"
|
4
|
+
require "delayer/deferred/deferredable/node_sequence"
|
5
|
+
|
6
|
+
module Delayer::Deferred::Deferredable
|
7
|
+
module Chainable
|
8
|
+
include Awaitable
|
9
|
+
include Graph
|
10
|
+
include NodeSequence
|
11
|
+
|
12
|
+
attr_reader :child
|
13
|
+
|
14
|
+
# このDeferredが成功した場合の処理を追加する。
|
15
|
+
# 新しいDeferredのインスタンスを返す。
|
16
|
+
# このメソッドはスレッドセーフです。
|
17
|
+
# TODO: procが空のとき例外を発生させる
|
18
|
+
def next(&proc)
|
19
|
+
add_child(Delayer::Deferred::Chain::Next.new(&proc))
|
20
|
+
end
|
21
|
+
alias deferred next
|
22
|
+
|
23
|
+
# このDeferredが失敗した場合の処理を追加する。
|
24
|
+
# 新しいDeferredのインスタンスを返す。
|
25
|
+
# このメソッドはスレッドセーフです。
|
26
|
+
# TODO: procが空のとき例外を発生させる
|
27
|
+
def trap(&proc)
|
28
|
+
add_child(Delayer::Deferred::Chain::Trap.new(&proc))
|
29
|
+
end
|
30
|
+
alias error trap
|
31
|
+
|
32
|
+
# この一連のDeferredをこれ以上実行しない。
|
33
|
+
# このメソッドはスレッドセーフです。
|
34
|
+
def cancel
|
35
|
+
change_sequence(:genocide) unless spoiled?
|
36
|
+
end
|
37
|
+
|
38
|
+
def has_child?
|
39
|
+
child ? true : false
|
40
|
+
end
|
41
|
+
|
42
|
+
# 子を追加する。
|
43
|
+
# _Delayer::Deferred::Chainable_ を直接指定できる。通常外部から呼ぶときは _next_ か _trap_ メソッドを使うこと。
|
44
|
+
# このメソッドはスレッドセーフです。
|
45
|
+
# ==== Args
|
46
|
+
# [chainable] 子となるDeferred
|
47
|
+
# ==== Return
|
48
|
+
# 必ず _chainable_ を返す
|
49
|
+
# ==== Raise
|
50
|
+
# [Delayer::Deferred::SequenceError]
|
51
|
+
# 既に子が存在している場合
|
52
|
+
def add_child(chainable)
|
53
|
+
change_sequence(:get_child) do
|
54
|
+
chainable.parent = self
|
55
|
+
@child = chainable
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# 子が追加された時に一度だけコールバックするオブジェクトを登録する。
|
60
|
+
# observerと言っているが、実際には _Delayer::Deferred::Worker_ を渡して利用している。
|
61
|
+
# このメソッドはスレッドセーフです。
|
62
|
+
# ==== Args
|
63
|
+
# [observer] pushメソッドを備えているもの。引数に _@child_ の値が渡される
|
64
|
+
# ==== Return
|
65
|
+
# self
|
66
|
+
def add_child_observer(observer)
|
67
|
+
change_sequence(:gaze) do
|
68
|
+
@child_observer = observer
|
69
|
+
end
|
70
|
+
self
|
71
|
+
end
|
72
|
+
|
73
|
+
def awaited
|
74
|
+
@awaited ||= [].freeze
|
75
|
+
end
|
76
|
+
|
77
|
+
def has_awaited?
|
78
|
+
not awaited.empty?
|
79
|
+
end
|
80
|
+
|
81
|
+
def add_awaited(awaitable)
|
82
|
+
@awaited = [*awaited, awaitable].freeze
|
83
|
+
self
|
84
|
+
end
|
85
|
+
|
86
|
+
# activateメソッドを呼ぶDelayerジョブを登録する寸前に呼ばれる。
|
87
|
+
def reserve_activate
|
88
|
+
change_sequence(:reserve)
|
89
|
+
end
|
90
|
+
|
91
|
+
def enter_pass
|
92
|
+
change_sequence(:pass)
|
93
|
+
end
|
94
|
+
|
95
|
+
def exit_pass
|
96
|
+
change_sequence(:resume)
|
97
|
+
end
|
98
|
+
|
99
|
+
protected
|
100
|
+
|
101
|
+
# 親を再帰的に辿り、一番最初のノードを返す。
|
102
|
+
# 親が複数見つかった場合は、それらを返す。
|
103
|
+
def ancestor
|
104
|
+
if @parent
|
105
|
+
@parent.ancestor
|
106
|
+
else
|
107
|
+
self
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# cancelとかデバッグ用のコールグラフを得るために親を登録しておく。
|
112
|
+
# add_childから呼ばれる。
|
113
|
+
def parent=(chainable)
|
114
|
+
@parent = chainable
|
115
|
+
end
|
116
|
+
|
117
|
+
private
|
118
|
+
|
119
|
+
def call_child_observer
|
120
|
+
if has_child? and defined?(@child_observer)
|
121
|
+
change_sequence(:called)
|
122
|
+
@child_observer.push(@child)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def on_sequence_changed(old_seq, flow, new_seq)
|
127
|
+
case new_seq
|
128
|
+
when NodeSequence::BURST_OUT
|
129
|
+
call_child_observer
|
130
|
+
when NodeSequence::GENOCIDE
|
131
|
+
@parent.cancel if defined?(@parent) and @parent
|
132
|
+
when NodeSequence::RESERVED_C, NodeSequence::RUN_C, NodeSequence::PASS_C, NodeSequence::AWAIT_C, NodeSequence::GRAFT_C
|
133
|
+
if !has_child?
|
134
|
+
notice "child: #{@child.inspect}"
|
135
|
+
raise Delayer::Deferred::SequenceError.new("Sequence changed `#{old_seq.name}' to `#{flow}', but it has no child")
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
# ノードの名前。サブクラスでオーバライドし、ノードが定義されたファイルの名前や行数などを入れておく。
|
141
|
+
def node_name
|
142
|
+
self.class.to_s
|
143
|
+
end
|
144
|
+
|
145
|
+
def graph_mynode
|
146
|
+
if defined?(@seq_logger)
|
147
|
+
label = "#{node_name}\n(#{@seq_logger.map(&:name).join('→')})"
|
148
|
+
else
|
149
|
+
label = "#{node_name}\n(#{sequence.name})"
|
150
|
+
end
|
151
|
+
"#{__id__} [shape=#{graph_shape},label=#{label.inspect}]"
|
152
|
+
end
|
153
|
+
|
154
|
+
end
|
155
|
+
end
|
@@ -0,0 +1,118 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
|
3
|
+
module Delayer::Deferred::Deferredable
|
4
|
+
=begin rdoc
|
5
|
+
graphvizによってChainableなDeferredをDOT言語形式でダンプする機能を追加するmix-in。
|
6
|
+
いずれかのノードに対して _graph_ メソッドを呼ぶと、再帰的に親子を全て辿り、digraphの断片の文字列を得ることが出来る。
|
7
|
+
|
8
|
+
== 出力例
|
9
|
+
|
10
|
+
20892180 [shape=egg,label="#<Class:0x000000027da288>.Promise\n(reserved)"]
|
11
|
+
20892480 [shape=box,label="test/thread_test.rb:53\n(connected)"]
|
12
|
+
20891440 [shape=diamond,label="test/thread_test.rb:56\n(fresh)"]
|
13
|
+
20892480 -> 20891440
|
14
|
+
20892180 -> 20892480
|
15
|
+
|
16
|
+
=end
|
17
|
+
module Graph
|
18
|
+
# この一連のDeferredチェインの様子を、DOT言語フォーマットで出力する
|
19
|
+
# ==== Args
|
20
|
+
# [child_only:]
|
21
|
+
# _true_ なら、このノードとその子孫のみを描画する。
|
22
|
+
# _false_ なら、再帰的に親を遡り、そこから描画を開始する。
|
23
|
+
# [output:]
|
24
|
+
# このオブジェクトに、 _<<_ メソッドで内容が書かれる。
|
25
|
+
# 省略した場合は、戻り値が _String_ になる。
|
26
|
+
# ==== Return
|
27
|
+
# [String] DOT言語によるグラフ
|
28
|
+
# [output:] 引数 output: に指定されたオブジェクト
|
29
|
+
def graph(child_only: false, output: String.new)
|
30
|
+
if child_only
|
31
|
+
output << "digraph Deferred {\n".freeze
|
32
|
+
Enumerator.new{ |yielder|
|
33
|
+
graph_child(output: yielder)
|
34
|
+
}.lazy.each{|l|
|
35
|
+
output << "\t#{l}\n"
|
36
|
+
}
|
37
|
+
output << '}'.freeze
|
38
|
+
else
|
39
|
+
ancestor.graph(child_only: true, output: output)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# Graph.graph の結果を内容とする一時ファイルを作成して返す。
|
44
|
+
# ただし、ブロックを渡された場合は、一時ファイルを引数にそのブロックを一度だけ実行し、ブロックの戻り値をこのメソッドの戻り値とする。
|
45
|
+
# ==== Args
|
46
|
+
# [&block] 一時ファイルを利用する処理
|
47
|
+
# ==== Return
|
48
|
+
# [Tempfile] ブロックを指定しなかった場合。作成された一時ファイルオブジェクト
|
49
|
+
# [Object] ブロックが指定された場合。ブロックの実行結果。
|
50
|
+
def graph_save(permanent: false, &block)
|
51
|
+
if block
|
52
|
+
Tempfile.open{|tmp|
|
53
|
+
graph(output: tmp)
|
54
|
+
tmp.seek(0)
|
55
|
+
block.(tmp)
|
56
|
+
}
|
57
|
+
else
|
58
|
+
tmp = Tempfile.open
|
59
|
+
graph(output: tmp).tap{|t|t.seek(0)}
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# 画像ファイルとしてグラフを書き出す。
|
64
|
+
# dotコマンドが使えないと失敗する。
|
65
|
+
# ==== Args
|
66
|
+
# [format:] 画像の拡張子
|
67
|
+
# ==== Return
|
68
|
+
# [String] 書き出したファイル名
|
69
|
+
def graph_draw(dir: '/tmp', format: 'svg'.freeze)
|
70
|
+
graph_save do |dotfile|
|
71
|
+
base = File.basename(dotfile.path)
|
72
|
+
dest = File.join(dir, "#{base}.#{format}")
|
73
|
+
system("dot -T#{format} #{dotfile.path} -o #{dest}")
|
74
|
+
dest
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# このノードとその子全てのDeferredチェインの様子を、DOT言語フォーマットで出力する。
|
79
|
+
# Delayer::Deferred::Deferredable::Graph#graph の内部で利用されるため、将来このメソッドのインターフェイスは変更される可能性がある。
|
80
|
+
def graph_child(output:)
|
81
|
+
output << graph_mynode
|
82
|
+
if has_child?
|
83
|
+
@child.graph_child(output: output)
|
84
|
+
output << "#{__id__} -> #{@child.__id__}"
|
85
|
+
end
|
86
|
+
if has_awaited?
|
87
|
+
awaited.each do |awaitable|
|
88
|
+
if awaitable.is_a?(Delayer::Deferred::Deferredable::Chainable)
|
89
|
+
awaitable.ancestor.graph_child(output: output)
|
90
|
+
else
|
91
|
+
label = "#{awaitable.class}"
|
92
|
+
output << "#{awaitable.__id__} [shape=oval,label=#{label.inspect}]"
|
93
|
+
end
|
94
|
+
output << "#{awaitable.__id__} -> #{__id__} [label = \"await\", style = \"dotted\"]"
|
95
|
+
end
|
96
|
+
end
|
97
|
+
nil
|
98
|
+
end
|
99
|
+
|
100
|
+
private
|
101
|
+
|
102
|
+
# このノードを描画する時の形の名前を文字列で返す。
|
103
|
+
# 以下のページにあるような、graphvizで取り扱える形の中から選ぶこと。
|
104
|
+
# http://www.graphviz.org/doc/info/shapes.html
|
105
|
+
def graph_shape
|
106
|
+
'oval'.freeze
|
107
|
+
end
|
108
|
+
|
109
|
+
# このノードの形などをDOT言語の断片で返す。
|
110
|
+
# このメソッドをオーバライドすることで、描画されるノードの見た目を自由に変更することが出来る。
|
111
|
+
# ただし、簡単な変更だけなら別のメソッドをオーバライドするだけで可能なので、このmix-inの他のメソッドも参照すること。
|
112
|
+
def graph_mynode
|
113
|
+
label = "#{node_name}\n(#{sequence.name})"
|
114
|
+
"#{__id__} [shape=#{graph_shape},label=#{label.inspect}]"
|
115
|
+
end
|
116
|
+
|
117
|
+
end
|
118
|
+
end
|
@@ -0,0 +1,158 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
require 'delayer/deferred/error'
|
3
|
+
|
4
|
+
require 'thread'
|
5
|
+
|
6
|
+
module Delayer::Deferred::Deferredable
|
7
|
+
module NodeSequence
|
8
|
+
class Sequence
|
9
|
+
attr_reader :name
|
10
|
+
|
11
|
+
def initialize(name)
|
12
|
+
@name = name.to_sym
|
13
|
+
@map = {}
|
14
|
+
@exceptions = Hash.new(Delayer::Deferred::SequenceError)
|
15
|
+
end
|
16
|
+
|
17
|
+
def add(seq, flow = seq.name)
|
18
|
+
@map[flow] = seq
|
19
|
+
self
|
20
|
+
end
|
21
|
+
|
22
|
+
def exception(exc, flow)
|
23
|
+
@exceptions[flow] = exc
|
24
|
+
self
|
25
|
+
end
|
26
|
+
|
27
|
+
def pull(flow)
|
28
|
+
if @map.has_key?(flow.to_sym)
|
29
|
+
@map[flow.to_sym]
|
30
|
+
else
|
31
|
+
raise @exceptions[flow.to_sym], "Invalid sequence flow `#{name}' to `#{flow}'."
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def inspect
|
36
|
+
"#<#{self.class}: #{name}>"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
FRESH = Sequence.new(:fresh)
|
41
|
+
CONNECTED = Sequence.new(:connected) # 子がいる、未実行
|
42
|
+
RESERVED = Sequence.new(:reserved) # 実行キュー待ち
|
43
|
+
RESERVED_C= Sequence.new(:reserved) # 実行キュー待ち(子がいる)
|
44
|
+
RUN = Sequence.new(:run) # 実行中
|
45
|
+
RUN_C = Sequence.new(:run) # 実行中(子がいる)
|
46
|
+
PASS = Sequence.new(:pass) # パス中
|
47
|
+
PASS_C = Sequence.new(:pass) # パス中
|
48
|
+
AWAIT = Sequence.new(:await) # Await中
|
49
|
+
AWAIT_C = Sequence.new(:await) # Await中(子がいる)
|
50
|
+
GRAFT = Sequence.new(:graft) # 戻り値がAwaitableの時
|
51
|
+
GRAFT_C = Sequence.new(:graft) # 戻り値がAwaitableの時(子がいる)
|
52
|
+
CALL_CHILD= Sequence.new(:call_child) # 完了、子がいる
|
53
|
+
STOP = Sequence.new(:stop) # 完了、子なし
|
54
|
+
WAIT = Sequence.new(:wait) # 完了、オブザーバ登録済み
|
55
|
+
BURST_OUT = Sequence.new(:burst_out) # 完了、オブザーバ登録済み、子追加済み
|
56
|
+
ROTTEN = Sequence.new(:rotten).freeze # 終了
|
57
|
+
GENOCIDE = Sequence.new(:genocide).freeze# この地ではかつて大量虐殺があったという。
|
58
|
+
|
59
|
+
FRESH
|
60
|
+
.add(CONNECTED, :get_child)
|
61
|
+
.add(RESERVED, :reserve)
|
62
|
+
.add(GENOCIDE).freeze
|
63
|
+
CONNECTED
|
64
|
+
.add(RESERVED_C, :reserve)
|
65
|
+
.exception(Delayer::Deferred::MultipleAssignmentError, :get_child)
|
66
|
+
.add(GENOCIDE).freeze
|
67
|
+
RESERVED
|
68
|
+
.add(RUN, :activate)
|
69
|
+
.add(RESERVED_C, :get_child)
|
70
|
+
.add(GENOCIDE).freeze
|
71
|
+
RESERVED_C
|
72
|
+
.add(RUN_C, :activate)
|
73
|
+
.exception(Delayer::Deferred::MultipleAssignmentError, :get_child)
|
74
|
+
.add(GENOCIDE).freeze
|
75
|
+
RUN
|
76
|
+
.add(RUN_C, :get_child)
|
77
|
+
.add(PASS)
|
78
|
+
.add(AWAIT, :await)
|
79
|
+
.add(STOP, :complete)
|
80
|
+
.add(GENOCIDE).freeze
|
81
|
+
RUN_C
|
82
|
+
.add(PASS_C)
|
83
|
+
.add(AWAIT_C, :await)
|
84
|
+
.add(CALL_CHILD, :complete)
|
85
|
+
.exception(Delayer::Deferred::MultipleAssignmentError, :get_child)
|
86
|
+
.add(GENOCIDE).freeze
|
87
|
+
PASS
|
88
|
+
.add(PASS_C, :get_child)
|
89
|
+
.add(RUN, :resume)
|
90
|
+
.add(GENOCIDE).freeze
|
91
|
+
PASS_C
|
92
|
+
.add(RUN_C, :resume)
|
93
|
+
.add(GENOCIDE).freeze
|
94
|
+
AWAIT
|
95
|
+
.add(RUN, :resume)
|
96
|
+
.add(AWAIT_C, :get_child)
|
97
|
+
.add(GENOCIDE).freeze
|
98
|
+
AWAIT_C
|
99
|
+
.add(RUN_C, :resume)
|
100
|
+
.exception(Delayer::Deferred::MultipleAssignmentError, :get_child)
|
101
|
+
.add(GENOCIDE).freeze
|
102
|
+
CALL_CHILD
|
103
|
+
.add(GRAFT_C, :await)
|
104
|
+
.add(ROTTEN, :called)
|
105
|
+
.add(GENOCIDE).freeze
|
106
|
+
GRAFT
|
107
|
+
.add(STOP, :resume)
|
108
|
+
.add(GRAFT_C, :get_child)
|
109
|
+
.add(GENOCIDE).freeze
|
110
|
+
GRAFT_C
|
111
|
+
.add(CALL_CHILD, :resume)
|
112
|
+
.add(GENOCIDE).freeze
|
113
|
+
STOP
|
114
|
+
.add(GRAFT, :await)
|
115
|
+
.add(WAIT, :gaze)
|
116
|
+
.add(GENOCIDE).freeze
|
117
|
+
WAIT
|
118
|
+
.add(BURST_OUT, :get_child)
|
119
|
+
.add(GENOCIDE).freeze
|
120
|
+
BURST_OUT
|
121
|
+
.add(ROTTEN, :called)
|
122
|
+
.add(GENOCIDE).freeze
|
123
|
+
|
124
|
+
SEQUENCE_LOCK = Monitor.new
|
125
|
+
|
126
|
+
def sequence
|
127
|
+
@sequence ||= FRESH
|
128
|
+
end
|
129
|
+
|
130
|
+
# このメソッドはスレッドセーフです
|
131
|
+
def change_sequence(flow, &block)
|
132
|
+
SEQUENCE_LOCK.synchronize do
|
133
|
+
old_seq = sequence
|
134
|
+
new_seq = @sequence = sequence.pull(flow)
|
135
|
+
(@seq_logger ||= [old_seq]) << new_seq
|
136
|
+
if block
|
137
|
+
result = block.()
|
138
|
+
on_sequence_changed(old_seq, flow, new_seq)
|
139
|
+
result
|
140
|
+
else
|
141
|
+
on_sequence_changed(old_seq, flow, new_seq)
|
142
|
+
nil
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
def on_sequence_changed(old_seq, flow, new_seq)
|
148
|
+
end
|
149
|
+
|
150
|
+
def activated?
|
151
|
+
![FRESH, CONNECTED, RUN, RUN_C].include?(sequence)
|
152
|
+
end
|
153
|
+
|
154
|
+
def spoiled?
|
155
|
+
sequence == ROTTEN || sequence == GENOCIDE
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|