A transaction processing system for CMS & TSO

KICKS KooKbooK recipe

The 251 project – Scripting 3270 apps with open source tools ( II )

Running multiple TSO sessions at the same time

This is a series of KooKbooK recipes detailing my successful effort to demonstrate over 250 KICKS users sharing VSAM files in the MVS Turnkey under Hercules, with sub-second response time.

This recipe continues the focus on scripting 3270 applications using free open source tools (C3270 and EXPECT) – i.e., how I manage to simulate over 250 users running KICKS

Last time we saw how to use a script to logon to TSO, “do something” and then logoff. This time we will extend the script to do the same on several terminals at once. While we are at it we’ll implement a simulation of user “think time”, and we will implement recording of “response time”. Without further ado, here’s this recipe’s script

#!/usr/bin/expect -f
# Using expect & c3270 to logon to tso, run an application, then logoff
# Note this script does not 'handle' all possible exception cases...
proc tso_task { who what where } { # userid, password, termid
# initialize constant for think time simulation
set think 5 ;# average user think time in seconds
# initialize variable for response time recording
set trannum 0 ;# total transactions so far
set trantim 0 ;# total trans time so far
set trantimmax -1 ;# longest trans response so far
set trantimmin 99999 ;# shortest trans response so far
# suppress terminal output to our screen(s)
log_user 0 ;# 0 to suppress, 1 to see it...
# spawn c3270 (uses special keymap to accomplish 'attn' function)
spawn c3270 -model 3279-2 -once 0$where@localhost:3270
# send a CLEAR (wakes up hung screen, put netsol on it)
sleep [expr (1+$think*2*rand())]; send "\001"; send "c"
# see if we got a netsol screen
set timeout 30
expect {
"Logon ===>" { set which netsol }
"INPUT NOT RECOGNIZED" { set which netsol }
timeout { set which timeout }
# if timeout die
if { $which == "timeout" } {
send_user "$where not at netsol\n"
# at netsol so logon
sleep [expr (1+$think*2*rand())]; send "logon $who"; send "\015"
while { $which != "ready" } {
# see what happened to the logon rqst
expect {
"ENTER CURRENT PASSWORD" { set which needpass }
"READY" { set which ready }
"KEY TO CONTINUE" { set which fsibanner }
timeout { set which timeout }
# if timeout die
if { $which == "timeout" } {
send_user "$who $where logon failed\n"
# tso wants password so give it
if { $which == "needpass"} {
sleep [expr (1+$think*2*rand())]; send "$what"; send "\015"
# press enter if fsi banner
if { $which == "fsibanner"} {
sleep [expr (1+$think*2*rand())]; send "\015"
} ;# end of 'while'
# at tso ready prompt, so go to rpf
sleep [expr (1+$think*2*rand())]; send "rpf"; send "\015"
set d1 [clock clicks -milliseconds]; set t1 $d1
# at rpf screen so quit rpf
expect "===>" {
set t2 [clock clicks -milliseconds]; set t2 [expr ($t2 - $t1)]
if { $t2 < $trantimmin } { set trantimmin $t2 }
if { $t2 > $trantimmax } { set trantimmax $t2 }
set trantim [expr ($trantim + $t2)]
incr trannum
sleep [expr (1+$think*2*rand())]
send "x"; send "\015"
set t1 [clock clicks -milliseconds]
# at tso ready again, now logoff
expect "READY" {
set t2 [clock clicks -milliseconds]; set t2 [expr ($t2 - $t1)]
if { $t2 < $trantimmin } { set trantimmin $t2 }
if { $t2 > $trantimmax } { set trantimmax $t2 }
set trantim [expr ($trantim + $t2)]
incr trannum
set d2 [clock clicks -milliseconds]; set d2 [expr ($d2 - $d1)]
sleep [expr (1+$think*2*rand())]
send "logoff"; send "\015"
# back at netsol screen so do ^] to get to c3270 menu
expect "===>" { sleep [expr (1+$think*2*rand())]; send "\035" }
# at c3270 menu so quit
expect "c3270>" { sleep [expr (1+$think*2*rand())]; send "quit\n" }
# record response time
set logfile [ open "$where-response.txt" "w" ]
puts -nonewline $logfile [format "%s\t" $where]
puts -nonewline $logfile [format "%.3f\t" [expr {$d2 / 1e3}]]
puts -nonewline $logfile [format "%d\t" $trannum]
puts -nonewline $logfile [format "%.3f\t" [expr {$trantim / 1e3}]]
puts -nonewline $logfile [format "%.3f\t" [expr {$trantimmax / 1e3}]]
puts $logfile [format "%.3f" [expr {$trantimmin / 1e3}]]
close $logfile
# ---------- end of subroutine ----- beginning of mainline ----------
set thewho { "HERC01" "HERC02" "HERC03" "HERC04" }
set thewhat { "" "CUL8TR" "" "PASS4U" }
set thewhere { "0C0" "0C1" "0C2" "0C3"}
set imax [llength $thewhere]
set imax 3 ;# only 3 tso sessions on base tk3upd...
for { set ii 0 } { $ii < $imax } { incr ii } {
set who [lindex $thewho $ii]
set what [lindex $thewhat $ii]
set where [lindex $thewhere $ii]
if { [fork] } { sleep 2 } else {
tso_task $who $what $where
exit 0
exit 0

No surprise, it looks a lot like last recipe’s! So what’s different? A quick way to see is to use MELD to compare them, as below. On the left, last recipe’s. On the right, this recipe’s…

The first change is right after the introductory comments, where instead of setting the who, what, where variables you see a “proc” statement (tso_task) with who, what, where arguments. You guessed it – we’re turning last recipe’s script into a subroutine! In fact most of this recipe’s script is just a slightly modified version of the last recipe. The biggest difference is the mainline, ie, the last dozen or so lines of the script; the bottom of the preceding page. Let’s review that short mainline before we see what changed in the subroutine (last recipe’s part).

First there’s a set of array initializations for thewho, thewhat, and thewhere. These arrays hold the who, what, and where variables we will pass in to the subroutine.

Next you see the imax variable initialized to the number of elements in the thewhere array.

Then you see the imax variable re-initialized to 3! What’s happening is that usually you would want imax to represent all the elements of the table. But since the (unmodified) TK3UPD MVS only has 3 3270’s (0C0, 0C1, 0C2) in its .conf file we already know the c3270 spawn for 00C3 would fail, so we just arrange for the script to quit before it gets there. I left the first “set” for imax in the code simply as documentation. BTW, this is a technique we will use as we ramp up to a large number of users. For convenience we will put all the 251 TSO userids, 251 passwords, and 251 terminal CUU’s in the tables, but we will use a similar limiting value of imax to control the number of sessions we try to start as we tune our way up.

After that there is a FOR loop that sets values of who, what, where based on table index. At first glance this looks like just marching thru the table calling the subroutine – which would of course run all the tasks – but one and a time, not all at once. However a closer examination shows it’s doing a FORK, then calling the subroutine! FORK is the usual *NIX way to create a new task that is a copy of the current task. FORK returns “true” if it is the original task (the one that did the FORK), or “false” if it is the new task. Our FORK is tested by an IF.

If FORK returns true (meaning it is in the original task) the program pauses (SLEEP) for a couple seconds to avoid “flooding” MVS with a bunch of TSO logons at once, then the FOR loop continues marching thru the table FORKing off new tasks and ultimately falling out the bottom and doing the EXIT that is the last line of the script. From this you can probably see that the original task actually ends before most (all?) of its FORKed copies…

If FORK returns “false” (meaning it is in the new task) the program makes a subroutine call to tso_task, passing the who, what, and where variables. When the subroutine returns the following EXIT terminates the new task.

No more to tell, that’s the whole “mainline” of the program! Back to the tso task subroutine starting near the top of the listing.

We already noted the set’s for who, what, and where in last recipe’s script have been replaced by the who, what, and where arguments in the PROC tso_task statement.

Next a set statement is added initializing a constant we will use to simulate think time (the time that passes between the user getting the last system output and his finishing his next data entry and pressing enter again). I’m saying “constant” but it’s really a variable whose value I won’t change. EXPECT does not support true constants.

Next four variables (representing total number of transactions, total transaction response time, maximum transaction response time, and minimum transaction response time) are initialized for response time recording. Response time is the time that passes between the user pressing enter and the system output being available at his screen.

After that there is a change to suppress terminal logging. Last recipe’s script showed the C3270 screen while EXPECT was driving it, and that was OK since there was only one screen being driven. But this recipe, since the script is driving multiple screens, we’d see all the screens’ output intermixed, probably not useful!

A few lines down from that we find the next change; instead of a “sleep 1” to briefly delay after some key strokes are sent to MVS we have a “sleep [expr (1+$think*2*rand())]”. Indeed this change repeats every time there was a “sleep 1”. This is the simulation of user “Think Time”. The intent is the simulated think time will be a random number between 1 and 1 plus twice the $think constant value. The average of these values should (as the number of them becomes large) be the same as the $think constant.

The next change is after the comment “# at tso ready prompt, so go to rpf”. It sets variable d1 to clock milliseconds, ant then sets variable t1 to d1. I’m using d1 and d2 to get the total duration of all transactions (including think time) and using t1 and t2 to get the response time for individual transactions. So these two statements get the start time for all transactions and the start time for the next transaction.

A few lines down, in the expect statement under the comment “# at rpf screen so quit rpf”, are 5 more new lines that make up the bulk of the response time recording. The 1st of these 5 obtains the transaction end time in milliseconds, then subtracts the start time to get the response time. Then in the 2nd and 3rd lines that is compared to the max and min response seen so far, and if better the statistic is updated. The 4th line adds the transaction response time to the total response time, and the 5th line increments the transaction count. The reason to keep these last two statistics is to enable calculation of average response time.

These 5 lines repeat in the next expect statement under the comment “# at tso ready again, now logoff”.

The last set of changes is a group of 8 lines under the comment “# record response time” at the bottom of the subroutine. An output file named for the terminal CUU is opened, the response time statistics are written as tab delineated values in a single line, and the file is closed. For convenience the times are converted from milliseconds to seconds.

The result of writing the statistics like this is that after running the script three new files will be present: 0C0-response.txt, 0C1-response.txt, and 0C2-response.txt. Their contents as I recorded were

0C0 1.719 2 0.341 0.210 0.131

0C1 5.073 2 0.335 0.201 0.134

0C2 6.051 2 0.290 0.199 0.097

Having the statistics in three files is a minor inconvenience, but you might imagine having them in 250 would be more problematic. However combining the three (or the 250) into one file is as simple as

cat 0*-response.txt >resp.xls

At which point resp.xls can be opened and spruced up using Open Office Calc or Microsoft Excel,

where I’ve added the formatting, including the titles, and calculated average response times.

We aren’t quite done with the scripts, but next time we’ll shift focus for awhile to look at the initial resource definition effort. This will include defining all the users and terminals (in all the different places they need to be defined!) as well as updating other system parameters we know (or can guess!) we will have to change to get to 250.

Copyright © Mike Noel, 2008-2016; last updated 1/16/2015 for KICKS 1.5.0