[ts-gen] Downstream architecture (3rd)
Bill Pippin
pippin at owlriver.net
Tue Oct 6 19:46:59 EDT 2009
Today's release includes a new directory, rsc, for ruby sample client,
with example code for popen3 and select. The code is preliminary and
leaves much room for improvement, yet includes the minimum architectural
features of a call to Open3.popen3 to open the connection to a shim
process, and use of IO.select to read both the shim stdout and stderr.
Users may want to use the ruby class definitions in the rsc directory
as the starting point for downstream script development.
Since these sample client sources are also used by the exs/test.rb
regression script, they are a starting point for me in improving
test coverage, in particular to avoid problems such as the one noted
earlier this week by Ken, where execution report requests had silently
stoped working.
In an earlier post back in Aug,
http://www.trading-shim.org/pipermail/ts-general/2009-August/000570.html
I wrote:
> ... users should already be using popen3() or its equivalent
> in their favorite scripting language, and select() ditto ...
> It would be nice to provide example code via a ruby sample
> client, and that's just one of many things ... [not] completed
> yet.
Users attempting to use Open3.popen3 as suggested above have probably
had problems with livelock, and may well have fallen back on using
psuedo ttys. [Note that Pty.spawn(...) is nearly a drop in replacment
for Open3.popen3(...), with the difference that the ptty merges stdout
and stderr, so that the client must disambiguate them.]
The current release of the shim changes the buffering for the shim
stdin to unbuffered, which is the last step needed to overcome the
problems with livelock. Previous changes included the choice of
line buffering for the shim's stdout, with the cout option; and
advice to the list to eliminate script buffering to the pipes, e.g.,
via the assignments in the popen3 block below:
> Open3.popen3(text.cmd_line) do | cmd_send, msg_recv, err_text |
> cmd_send.sync =
> msg_recv.sync =
> err_text.sync = true
> test.prog(cmds, read, proc, cmd_send, msg_recv, err_text)
> end
The livelock would occur where commands to the shim were accumulating
in a stdlib buffer, the shim did not know of them and was loop waiting
using select for commands, and the client was loop waiting for events
that would not occur until the commands were processed.
The text of the new ruby sample client follows my sig, and, as noted
earlier, is also included in the rsc directory in the today's release.
Also, the problem with history queries for ECBOT:YM is new, known, and
I'll be getting to that next. It seems to be data (symbol) dependent.
Thanks,
Bill
::::::::::::::
test.rb
::::::::::::::
#!/usr/bin/ruby
# author: Bill Pippin, <pippin at trading-shim.com>, msgs may gate to the list
# copyright (c) 2008-2009 Trading-shim.com, LLC Columbus, OH
# GPL version 3 or later, see COPYING for details
=begin
Given the command line defined by CmdArgText; the predefined commands,
by CannedCmds; the select/read loop, by SelectRead; and the processing
filter, by ProcFilter: for the class Popen3Shim, exec the command as a
child using popen3, and use the SampleTest instance to send cmds to it,
read what it writes back, and dump results. See the ProcFilter class
for further details of processing.
=end
require "rsc/CmdArgText"
require "rsc/CannedCmds"
require "rsc/SelectRead"
require "rsc/ProcFilter"
require "rsc/SampleTest"
require "rsc/Popen3Shim"
text = CmdArgText.new
cmds = CannedCmds.new
read = SelectRead.new
proc = ProcFilter.new
test = SampleTest.new
Popen3Shim.new.prog(text, cmds, read, proc, test)
exit
::::::::::::::
SampleTest.rb
::::::::::::::
# author: Bill Pippin, <pippin at trading-shim.com>, msgs may gate to the list
# copyright (c) 2009 Trading-shim.com, LLC Columbus, OH
# GPL version 3 or later, see COPYING for details
=begin
Given both: objects to send commands, perform processing, and read
results; and files for output, input, and stderr for the related IO;
then glue the object processing together via a simple test harness.
=end
class SampleTest
def prog(cmds, read, proc, cmd_send, msg_recv, err_text)
cmds.test(cmd_send)
if read.loop(msg_recv, err_text, proc)
cmds.exit(cmd_send)
proc.dump
end
end
end
::::::::::::::
CmdArgText.rb
::::::::::::::
# author: Bill Pippin, <pippin at trading-shim.com>, msgs may gate to the list
# copyright (c) 2009 Trading-shim.com, LLC Columbus, OH
# GPL version 3 or later, see COPYING for details
=begin
Define a reasonable command line with which to start a data mode shim,
given the goal of regression testing; feel free to modify as needed.
=end
class CmdArgText
def cmd_line
"./shim --data cout file save join diff"
end
end
::::::::::::::
Popen3Shim.rb
::::::::::::::
# author: Bill Pippin, <pippin at trading-shim.com>, msgs may gate to the list
# copyright (c) 2009 Trading-shim.com, LLC Columbus, OH
# GPL version 3 or later, see COPYING for details
=begin
Open the command line Text using popen3, and apply the prog method of
the Test object to each of the object parameters Cmds, Read, and Proc
as well as the resulting pipes.
Note that pseudo ttys can be used as nearly a drop in replacement for
popen3. I.e. Pty.spawn(text.cmd_line) do | cmd_send, msg_recv, pid |
There's the drawback that the process filter must distinguish tws api
events from shim writes to the stderr, but given the log format, this
is not difficult.
=end
require 'open3'
class Popen3Shim
def prog(text, cmds, read, proc, test)
Open3.popen3(text.cmd_line) do | cmd_send, msg_recv, err_text |
cmd_send.sync =
msg_recv.sync =
err_text.sync = true
test.prog(cmds, read, proc, cmd_send, msg_recv, err_text)
end
end
end
::::::::::::::
CannedCmds.rb
::::::::::::::
# author: Bill Pippin, <pippin at trading-shim.com>, msgs may gate to the list
# copyright (c) 2009 Trading-shim.com, LLC Columbus, OH
# GPL version 3 or later, see COPYING for details
=begin
Define as text various commands in the trading-shim input language, and
provide operations to print those commands as part of regression tests.
Currently this class includes only non-transactional tests, those that do
not perform any trades. For now, see the test script exs/risk.rb for an
example of simple trades.
Although this script is meant in part to illustrate correct syntax for
the shim commands included herein, there are a number of cases where the
alternatives in the plain shim scripts --- executable text files with the
shim itself as the shebang interpreter, found in the directory ../exs ---
are more flexible and upto date.
In particular, the comments in the tick, info, and bars scripts may be of
interest to new users.
test_cmds(shim_cmd)
Once contructed, and given an IO object connected to the unbuffered
stdin of a trading-shim process, send session level, market data,
marked depth, and history commands to the shim. This top-level cmd
will trigger all the other methods of this class, and so is the only
method the client need use.
By the way, the contracts are meant to be for Apple and the current
front month of ECBOT:YM .
=end
class CannedCmds
def test shim
data_test(shim)
tick_test(shim)
book_test(shim)
past_test(shim)
tick_test(shim)
end
def data_test(shim); shim.write data_text(); end
def tick_test(shim); shim.write tick_text(); end
def book_test(shim); shim.write book_text(); end
def past_test(shim); shim.write past_text(); end
def exit (shim);
STDERR.print "exit \n"
shim.write "exit;\n"; end
def data_text()
return <<-"EOT"
set loglevel Detail;
select next;
select exec AAPL 9:00:00;
select news all;
cancel news;
select info ibc: 266093 at SMART new;
select info ibc:56578477 at ECBOT all;
EOT
end
def tick_text()
return <<-"EOT"
select tick ibc: 266093 at SMART 1; wait 2;
cancel tick ibc: 266093 at SMART; wait 0;
select tick ibc:56578477 at ECBOT 1; wait 2;
cancel tick ibc:56578477 at ECBOT; wait 0;
EOT
end
def book_text()
return <<-"EOT"
select book ibc:56578477 at ECBOT 5; wait 4;
cancel book ibc:56578477 at ECBOT; wait 0;
EOT
end
def past_text()
return <<-"EOT"
select past ibc:56578477 at ECBOT h1 11 1d now; wait 6;
EOT
end
end
::::::::::::::
SelectRead.rb
::::::::::::::
# author: Bill Pippin, <pippin at trading-shim.com>, msgs may gate to the list
# copyright (c) 2009 Trading-shim.com, LLC Columbus, OH
# GPL version 3 or later, see COPYING for details
=begin
Given two inputs, the first for a shim format event log, and the second
from the shim stderr, loop to multiplex reads for each via select, send
each log input line to proc.filter, each error text line to proc.stderr,
and terminate if:
eof or errors occur with either the event or error input files; or
twelve or more one-second timeouts occur during the call to select.
=end
class SelectRead
def loop(msg_recv, err_text, proc)
limit = 12
count = 0
child = true
input = [msg_recv, err_text]
while count < limit && child
result = IO.select(input, nil, input, 1)
if result != nil
if result[2] == []
result[0].each do |f|
case f
when msg_recv then child &= proc.filter msg_recv.gets
when err_text then child &= proc.stderr err_text.gets
end
end
else STDERR.print "IO error in select loop\n"; child = false end
else count += 1 end
end
return child
end
end
::::::::::::::
ProcFilter.rb
::::::::::::::
# author: Bill Pippin, <pippin at trading-shim.com>, msgs may gate to the list
# copyright (c) 2009 Trading-shim.com, LLC Columbus, OH
# GPL version 3 or later, see COPYING for details
=begin
Given shim format event lines or stderr text, count events for the former,
and echo the latter; and otherwise, if the input line is nil, again echo
it, and otherwise do nothing. The dump method lists event totals.
For debugging and other purposes there is also a commented-out statement
to list log format events. That statement will, given the split on bars,
slice the array to drop the first three fields, reconstitute via join,
and slice the resulting string to fit the terminal display.
The event counting is crude, and will in the future be modified to use
database information about the various event types.
=end
class ProcFilter
attr_reader :cmd_totals, :req_totals, :msg_totals
def initialize
@cmd_totals = []
@req_totals = []
@msg_totals = []
(0..100).each do |i|
@cmd_totals[i] = 0
@req_totals[i] = 0
@msg_totals[i] = 0
end
end
def filter line
if line != nil
fields = line.chop.split('|')
length = fields.size
if length >= 5 then
src = fields[3].to_i
tag = fields[4].to_i
ver = fields[5].to_i
if src == 3
# STDERR.print fields[3,length].join('|')[0..78], "|\n"
end
case src
when 1 then cmd_totals[tag] += 1
when 2 then req_totals[tag] += 1
when 3 then msg_totals[tag] += 1
end
end
end
return line
end
def stderr line
STDERR.print line
return line
end
def dump
1.upto(20) { |i| printf("%4u", cmd_totals[i]) }
1.upto(20) { |i| printf("%4u", req_totals[i]) }
1.upto(20) { |i| printf("%4u", msg_totals[i]) }
end
end
More information about the ts-general
mailing list