From 0a7242713f93d15be11677c36dfd906068e2272d Mon Sep 17 00:00:00 2001 From: Morten Kromberg Date: Fri, 19 Aug 2016 10:53:05 +0200 Subject: [PATCH] Server source structure in place, first version of TestVecdbSrv written. Now to make it work! --- APLProcess.dyalog | 420 ++++++++++++++++++++++++++++++++++++++++++++ BootServers.dyalog | 38 ++++ MakeBoot.dyalog | 10 ++ MakeBoot.dyapp | 2 + TestVecdbSrv.dyalog | 107 +++++++++++ dev.dyapp | 10 +- vecdbboot.dws | Bin 0 -> 41528 bytes 7 files changed, 585 insertions(+), 2 deletions(-) create mode 100644 APLProcess.dyalog create mode 100644 BootServers.dyalog create mode 100644 MakeBoot.dyalog create mode 100644 MakeBoot.dyapp create mode 100644 TestVecdbSrv.dyalog create mode 100644 vecdbboot.dws diff --git a/APLProcess.dyalog b/APLProcess.dyalog new file mode 100644 index 0000000..50f9539 --- /dev/null +++ b/APLProcess.dyalog @@ -0,0 +1,420 @@ +:Class APLProcess + ⍝ Start (and eventually dispose of) a Process + + (⎕IO ⎕ML)←1 1 + + :Field Public Args←'' + :Field Public Ws←'' + :Field Public Exe←'' + :Field Public Proc←⎕NS '' + :Field Public onExit←'' + :Field Public RunTime←0 ⍝ Boolean or name of runtime executable + + :Field Public RIDE_INIT←'' ⍝ RIDE parameters if remote debugging is to be allowed + + endswith←{w←,⍵ ⋄ a←,⍺ ⋄ w≡(-(⍴a)⌊⍴w)↑a} + tonum←{⊃⊃(//)⎕VFI ⍵} + eis←{2>|≡⍵:,⊂⍵ ⋄ ⍵} ⍝ enclose if simple + + ∇ make + :Access Public Instance + :Implements Constructor + ∇ + + ∇ make1 args;rt;cmd;ws + :Access Public Instance + :Implements Constructor + ⍝ args is: + ⍝ [1] the workspace to load + ⍝ [2] any command line arguments + ⍝ {[3]} if present, a Boolean indicating whether to use the runtime version, OR a character vector of the executable name to run + args←{2>|≡⍵:,⊂⍵ ⋄ ⍵}args + args←3↑args,(⍴args)↓'' '' 0 + (ws cmd rt)←args + Start(ws cmd rt) + ∇ + + ∇ Run + :Access Public Instance + Start(Ws Args RunTime) + ∇ + + ∇ Start(ws args rt);psi;pid + (Ws Args)←ws args + :If 0≠⍴RIDE_INIT + args←args,' RIDE_INIT=',RIDE_INIT + :EndIf + + :If ~0 2∊⍨10|⎕DR rt ⍝ if rt is character, it's the executable name + Exe←(RunTimeName⍣rt)GetCurrentExecutable + :Else + Exe←rt + rt←0 + :EndIf + ⍝ ws,←rt/' salt' ⍝ if runtime, load the salt workspace first, which will subsequently load the target workspace + :If IsWin + ⎕USING←'System,System.dll' + psi←⎕NEW Diagnostics.ProcessStartInfo,⊂Exe(ws,' ',args) + psi.WindowStyle←Diagnostics.ProcessWindowStyle.Minimized + Proc←Diagnostics.Process.Start psi + :Else ⍝ Unix + pid←_SH'{ ',args,' ',Exe,' +s ',ws,' -c APLppid=',(⍕GetCurrentProcessId),' /dev/null 2>&1 & } ; echo $!' + Proc.Id←pid + Proc.HasExited←HasExited + Proc.StartTime←⎕NEW Time ⎕TS + :EndIf + ∇ + + ∇ Close;count;limit + :Implements Destructor + WaitForKill&200 0.1 ⍝ Start a new thread to do the dirty work + ∇ + + ∇ WaitForKill(limit interval);count + :If (0≠⍴onExit)∧~HasExited ⍝ If the process is still alive + :Trap 0 ⋄ ⍎onExit ⋄ :EndTrap ⍝ Try this + + count←0 + :While ~HasExited + {}⎕DL interval + count←count+1 + :Until count>limit + :EndIf ⍝ OK, have it your own way + + {}Kill Proc + ∇ + + ∇ r←IsWin + :Access Public Shared + r←'Win'≡3↑⎕IO⊃#.⎕WG'APLVersion' + ∇ + + ∇ r←GetCurrentProcessId;t + :Access Public Shared + :If IsWin + r←⍎'t'⎕NA'U4 kernel32|GetCurrentProcessId' + :Else + r←tonum⊃_SH'echo $PPID' + :EndIf + ∇ + + ∇ r←GetCurrentExecutable;⎕USING;t;gmfn + :Access Public Shared + :If IsWin + r←'' + :Trap 0 + 'gmfn'⎕NA'U4 kernel32|GetModuleFileName* P =T[] U4' + r←⊃⍴/gmfn 0(1024⍴' ')1024 + :EndTrap + :If 0∊⍴r + ⎕USING←'System,system.dll' + r←2 ⎕NQ'.' 'GetEnvironment' 'DYALOG' + r←r,(~(¯1↑r)∊'\/')/'/' ⍝ Add separator if necessary + r←r,(Diagnostics.Process.GetCurrentProcess.ProcessName),'.exe' + :EndIf + :Else + t←⊃_PS'-o args -p ',⍕GetCurrentProcessId ⍝ AWS + :If '"'''∊⍨⊃t ⍝ if command begins with ' or " + r←{⍵/⍨{∧\⍵∨≠\⍵}⍵=⊃⍵}t + :Else + r←{⍵↑⍨¯1+1⍳⍨(¯1↓0,⍵='\')<⍵=' '}t ⍝ otherwise find first non-escaped space (this will fail on files that end with '\\') + :EndIf + :EndIf + ∇ + + ∇ r←RunTimeName exe + ⍝ Assumes that: + ⍝ Windows runtime ends in "rt.exe" + ⍝ *NIX runtime ends in ".rt" + r←exe + :If IsWin + :If 'rt.exe'≢¯6↑{('rt.ex',⍵)[⍵⍳⍨'RT.EX',⍵]}exe ⍝ deal with case insensitivity + r←'rt.exe',⍨{(~∨\⌽<\⌽'.'=⍵)/⍵}exe + :EndIf + :Else + r←exe,('.rt'≢¯3↑exe)/'.rt' + :EndIf + ∇ + + + ∇ r←KillChildren Exe;kids;⎕USING;p;m;i;mask + :Access Public Shared + ⍝ returns [;1] pid [;2] process name of any processes that were not killed + r←0 2⍴0 '' + :If ~0∊⍴kids←ListProcesses Exe ⍝ All child processes using the exe + :If IsWin + ⎕USING←'System,system.dll' + p←Diagnostics.Process.GetProcessById¨kids[;1] + p.Kill + ⎕DL 1 + :If 0≠⍴p←(~p.HasExited)/p + ⎕DL 1 + p.Kill + ⎕DL 1 + :If ∨/m←~p.HasExited + r←(kids[;1]∊m/p.Id)⌿kids + :EndIf + :EndIf + :Else + mask←(⍬⍴⍴kids)⍴0 + :For i :In ⍳⍴mask + mask[i]←Shoot kids[i;1] + :EndFor + r←(~mask)⌿kids + :EndIf + :EndIf + ∇ + + ∇ r←{all}ListProcesses procName;me;⎕USING;procs;unames;names;name;i;pn;kid;parent;mask;n + :Access public shared + ⍝ returns either my child processes or all processes + ⍝ procName is either '' for all children, or the name of a process + ⍝ r[;1] - child process number (Id) + ⍝ r[;2] - child process name + me←GetCurrentProcessId + r←0 2⍴0 '' + procName←,procName + all←{6::⍵ ⋄ all}0 ⍝ default to just my childen + + :If IsWin + ⎕USING←'System,system.dll' + + :If 0∊⍴procName ⋄ procs←Diagnostics.Process.GetProcesses'' + :Else ⋄ procs←Diagnostics.Process.GetProcessesByName⊂procName ⋄ :EndIf + :If all + r←↑procs.(Id ProcessName) + r⌿⍨←r[;1]≠me + :Else + :If 0<⍴procs + unames←∪names←procs.ProcessName + :For name :In unames + :For i :In ⍳n←1+.=(,⊂name)⍳names + pn←name,(n≠1)/'#',⍕i + :Trap 0 ⍝ trap here just in case a process disappeared before we get to it + parent←⎕NEW Diagnostics.PerformanceCounter('Process' 'Creating Process Id'pn) + :If me=parent.NextValue + kid←⎕NEW Diagnostics.PerformanceCounter('Process' 'Id Process'pn) + r⍪←(kid.NextValue)name + :EndIf + :EndTrap + :EndFor + :EndFor + :EndIf + :EndIf + :Else ⍝ Linux + ⍝ unfortunately, Ubuntu (and perhaps others) report the PPID of tasks started via ⎕SH as 1 + ⍝ so, the best we can do at this point is identify processes that we tagged with ppid= + mask←' '∧.=procs←' ',↑_PS'-eo pid,cmd',((~all)/' | grep APLppid=',(⍕GetCurrentProcessId)),(0<⍴procName)/' | grep ',procName,' | grep -v grep' ⍝ AWS + mask∧←2≥+\mask + procs←↓¨mask⊂procs + mask←me≠tonum¨1⊃procs ⍝ remove my task + procs←mask∘/¨procs[1 2] + mask←1 + :If 0<⍴procName + mask←∨/¨(procName,' ')∘⍷¨(2⊃procs),¨' ' + :EndIf + mask>←∨/¨'grep '∘⍷¨2⊃procs ⍝ remove procs that are for the searches + procs←mask∘/¨procs + r←↑[0.1]procs + :EndIf + ∇ + + ∇ r←Kill;delay + :Access Public Instance + r←0 ⋄ delay←0.1 + :Trap 0 + :If IsWin + Proc.Kill + :Repeat + ⎕DL delay + delay+←delay + :Until (delay>10)∨Proc.HasExited + :Else + {}UNIXIssueKill 3 Proc.Id ⍝ issue strong interrupt + {}⎕DL 2 ⍝ wait a couple seconds for it to react + :If ~Proc.HasExited←~UNIXIsRunning Proc.Id + {}UNIXIssueKill 9 Proc.Id ⍝ issue strong interrupt + {}⎕DL 2 ⍝ wait a couple seconds for it to react + :AndIf ~Proc.HasExited←~UNIXIsRunning Proc.Id + :Repeat + ⎕DL delay + delay+←delay + :Until (delay>10)∨Proc.HasExited~UNIXIsRunning Proc.Id + :EndIf + :EndIf + r←Proc.HasExited + :EndTrap + ∇ + + ∇ r←Shoot Proc;MAX;res + MAX←100 + r←0 + :If 0≠⎕NC⊂'Proc.HasExited' + :Repeat + :If ~Proc.HasExited + :If IsWin + Proc.Kill + ⎕DL 0.2 + :Else + {}UNIXIssueKill 3 Proc.Id ⍝ issue strong interrupt AWS + {}⎕DL 2 ⍝ wait a couple seconds for it to react + :If ~Proc.HasExited←0∊⍴res←UNIXGetShortCmd Proc.Id ⍝ AWS + Proc.HasExited∨←∨/''⍷⊃,/res + :EndIf + :EndIf + :EndIf + MAX-←1 + :Until Proc.HasExited∨MAX≤0 + r←Proc.HasExited + :ElseIf 2=⎕NC'Proc' ⍝ just a process id? + {}UNIXIssueKill 9 Proc.Id + {}⎕DL 2 + r←~UNIXIsRunning Proc.Id ⍝ AWS + :EndIf + ∇ + + ∇ r←HasExited + :Access public instance + :If IsWin + r←{0::⍵ ⋄ Proc.HasExited}1 + :Else + r←~UNIXIsRunning Proc.Id ⍝ AWS + :EndIf + ∇ + + ∇ r←IsRunning args;⎕USING;start;exe;pid;proc;diff;res + :Access public shared + ⍝ args - pid {exe} {startTS} + r←0 + args←eis args + (pid exe start)←3↑args,(⍴args)↓0 ''⍬ + :If IsWin + ⎕USING←'System,system.dll' + :Trap 0 + proc←Diagnostics.Process.GetProcessById pid + r←1 + :Else + :Return + :EndTrap + :If ''≢exe + r∧←exe≡proc.ProcessName + :EndIf + :If ⍬≢start + :Trap 90 + diff←|-/#.DFSUtils.DateToIDN¨start(proc.StartTime.(Year Month Day Hour Minute Second Millisecond)) + r∧←diff≤24 60 60 1000⊥0 1 0 0÷×/24 60 60 1000 ⍝ consider it a match within a 1 minute window + :Else + r←0 + :EndTrap + :EndIf + :Else + r←UNIXIsRunning pid + :EndIf + ∇ + + ∇ r←Stop pid;proc + :Access public shared + ⍝ attempts to stop the process with processID pid + :If IsWin + ⎕USING←'System,system.dll' + :Trap 0 + proc←Diagnostics.Process.GetProcessById pid + :Else + r←1 + :Return + :EndTrap + proc.Kill + {}⎕DL 0.5 + r←~##.APLProcess.IsRunning pid + :Else + {}UNIXIssueKill 3 pid ⍝ issue strong interrupt + :EndIf + ∇ + + ∇ r←UNIXIsRunning pid;txt + ⍝ Return 1 if the process is in the process table and is not a defunct + r←0 + →(r←' '∨.≠txt←UNIXGetShortCmd pid)↓0 + r←~∨/''⍷txt + ∇ + + ∇ {r}←UNIXIssueKill(signal pid) + signal pid←⍕¨signal pid + r←⎕SH'kill -',signal,' ',pid,' >/dev/null 2>&1 ; echo $?' + ∇ + + ∇ r←UNIXGetShortCmd pid + ⍝ Retrieve sort form of cmd used to start process + r←⊃1↓⎕SH'ps -o cmd -p ',(⍕pid),' 2>/dev/null ; exit 0' + ∇ + + ∇ r←_PS cmd;ps + ps←'ps ',⍨('AIX'≡3↑⊃'.'⎕WG'APLVersion')/'/usr/sysv/bin/' ⍝ Must use this ps on AIX + r←1↓⎕SH ps,cmd,' 2>/dev/null; exit 0' ⍝ Remove header line + ∇ + + ∇ r←{quietly}_SH cmd + :Access public shared + quietly←{6::⍵ ⋄ quietly}0 + :If quietly + cmd←cmd,' &1' + :EndIf + r←{0::'' ⋄ ⎕SH ⍵}cmd + ∇ + + :class Time + :field public Year + :field public Month + :field public Day + :field public Hour + :field public Minute + :field public Second + :field public Millisecond + + ∇ make ts + :Implements constructor + :Access public + (Year Month Day Hour Minute Second Millisecond)←7↑ts + ⎕DF(⍕¯2↑'00',⍕Day),'-',((12 3⍴'JanFebMarAprMayJunJulAugSepOctNovDec')[⍬⍴Month;]),'-',(⍕100|Year),' ',1↓⊃,/{':',¯2↑'00',⍕⍵}¨Hour Minute Second + ∇ + :endclass + + ∇ r←ProcessUsingPort port;t + ⍝ return the process ID of the process (if any) using a port + :Access public shared + r←⍬ + :If IsWin + :If ~0∊⍴t←_SH'netstat -a -n -o' + :AndIf ~0∊⍴t/⍨←∨/¨'LISTENING'∘⍷¨t + :AndIf ~0∊⍴t/⍨←∨/¨((':',⍕port),' ')∘⍷¨t + r←∪∊¯1↑¨(//)∘⎕VFI¨t + :EndIf + :Else + :If ~0∊⍴t←_SH'netstat -l -n -p 2>/dev/null | grep '':',(⍕port),' ''' + r←∪∊{⊃(//)⎕VFI{(∧\⍵∊⎕D)/⍵}⊃¯1↑{⎕ML←3 ⋄ (' '≠⍵)⊂⍵}⍵}¨t + :EndIf + :EndIf + ∇ + + ∇ r←MyDNSName;GCN + :Access Public Shared + + :If IsWin + 'GCN'⎕NA'I4 Kernel32|GetComputerNameEx* U4 >0T =U4' + r←2⊃GCN 7 255 255 + :Return +⍝ ComputerNameNetBIOS = 0 +⍝ ComputerNameDnsHostname = 1 +⍝ ComputerNameDnsDomain = 2 +⍝ ComputerNameDnsFullyQualified = 3 +⍝ ComputerNamePhysicalNetBIOS = 4 +⍝ ComputerNamePhysicalDnsHostname = 5 +⍝ ComputerNamePhysicalDnsDomain = 6 +⍝ ComputerNamePhysicalDnsFullyQualified = 7 <<< +⍝ ComputerNameMax = 8 + :Else + r←⊃_SH'hostname' + :EndIf + ∇ + +:EndClass diff --git a/BootServers.dyalog b/BootServers.dyalog new file mode 100644 index 0000000..a6422bc --- /dev/null +++ b/BootServers.dyalog @@ -0,0 +1,38 @@ +BootServers dummy;port;getenv;getnum;path +⍝ Start a vecdb server process if VECDBSRV=config.xml PORT=nnnn +⍝ vecdb slave process if VECDBSLAVE="file,[VECDBSHARD=n]" PORT=nnnn + + getenv←{0=≢2 ⎕NQ'.' 'GetEnvironment'⍵:⍺} + getnum←{⊃2⊃⎕VFI ⍵} + path←'file://',⊃⎕NPARTS ⎕WSID + + VECDBSRV←0≠⍴CONFIG←''getenv'VECDBSRV' + VECDBSLAVE←0≠VECDB←''getenv'VECDBSLAVE' + port←getnum''getenv'PORT' + + 2 ⎕FIX path,'APLProcess.dyalog' + 2 ⎕FIX path,'vecdb.dyalog' + 2 ⎕FIX path,'vecdbsrv.dyalog' + + :If 0=⎕NC'DRC' ⍝ Get conga if necessary + 'DRC'⎕CY'conga'getenv'CONGAWS' + :EndIf + + :If 0=port + ⎕←'See:' + ' ',2 ⎕FIX path,'TestVecdb.dyalog' + ' ',2 ⎕FIX path,'TestVecdbSrv.dyalog' + :Else + + {}1 ##.DRC.Init'' + + :If VECDBSRV ⋄ vecdbsrv.Start CONFIG port + :ElseIf VECDBSLAVE ⋄ vecdbslave.Start VECDB port + + port←getnum port + ⎕←'Starting slave, listening on port ',⍕port + RPCServer.Run'RPCSRV'port + :Else + ⎕←'See TestRPCServer' + :EndIf + :EndIf \ No newline at end of file diff --git a/MakeBoot.dyalog b/MakeBoot.dyalog new file mode 100644 index 0000000..028c819 --- /dev/null +++ b/MakeBoot.dyalog @@ -0,0 +1,10 @@ + MakeBoot;Path +⍝ Built the "vecdbboot" workspace + + Path←{(1-⌊/'/\'⍳⍨⌽⍵)↓⍵}4↓,¯1↑⎕CR⊃⎕SI + ⎕SE.SALT.Load Path,'BootServers.dyalog' + ⎕LX←'BootServers ''''' + ⎕←'Now please:' + ⎕←' ⎕EX ''MakeBoot''' + ⎕←' )WSID ',Path,'vecdbboot.dws' + ⎕←' )SAVE' diff --git a/MakeBoot.dyapp b/MakeBoot.dyapp new file mode 100644 index 0000000..7c60faa --- /dev/null +++ b/MakeBoot.dyapp @@ -0,0 +1,2 @@ +Load MakeBoot +Run MakeBoot \ No newline at end of file diff --git a/TestVecdbSrv.dyalog b/TestVecdbSrv.dyalog new file mode 100644 index 0000000..0366615 --- /dev/null +++ b/TestVecdbSrv.dyalog @@ -0,0 +1,107 @@ +:Namespace TestVecdbSrv + ⍝ Call TestVecdbSrv.RunAll to run Server Tests + ⍝ assumes existence of #.vecdbclt and #.vecdb + + (⎕IO ⎕ML)←1 1 + LOG←1 + toJson←(0 1)∘7160⌶ + + ∇ z←RunAll;path;source + ⎕FUNTIE ⎕FNUMS ⋄ ⎕NUNTIE ⎕NNUMS + :Trap 6 ⋄ source←SALT_Data.SourceFile + :Else ⋄ source←⎕WSID + :EndTrap + path←{(-⌊/(⌽⍵)⍳'\/')↓⍵}source + ⎕←ServerBasic + ∇ + + ∇ z←ServerBasic;columns;data;options;params;folder;types;name;db;ix;vecdbsrv;config;users;user;srvproc;clt;TEST + ⍝ Test database with 2 shards + ⍝ Also acts as test for add/remove columns + + folder←path,'/',(name←'srvtest'),'/' + ⎕←'Clearing: ',folder + :Trap 22 ⋄ #.vecdb.Delete folder ⋄ :EndTrap + + ⍝ --- Create configuration file --- + + user←⎕NS '' + user.(Name Id Admin)←'mkrom' 1001 1 + vecdbsrv←⎕NS'' + vecdbsrv.Name←'Test Server' + vecdbsrv.Users←,user + db←⎕NS'' + db.Folder←folder + db.Slaves←,¨1 2 ⍝ Distribution of shards to slave processors + config←⎕NS'' + config.Server←vecdbsrv + config.DBs←,db + (folder,'config.json')⎕NPUT toJson folder,'config.json' + + ⍝ --- Create database --- + + columns←'Name' 'BlockSize' 'Flag' + types←,¨'C' 'F' 'C' + data←('IBM' 'AAPL' 'MSFT' 'GOOG' 'DYALOG')(160.97 112.6 47.21 531.23 999.99)(5⍴'Buy' 'Sell') + + options←⎕NS'' + options.BlockSize←10000 + options.ShardFolders←(folder,'Shard')∘,¨'12' + options.(ShardFn ShardCols)←'{2-2|⎕UCS ⊃¨⊃⍵}' 1 + + params←name folder columns types options data + TEST←'Create sharded database' + db←⎕NEW #.vecdb params + assert (≢data)=db.Count + + ⍝ --- Launch and connect to server, open database --- + + srvproc←#.vecdbsrv.Launch folder 8100 + assert 0=srvproc.HasExited + + clt←#.vecdbclt.Connect '127.0.0.1' 8100 'mkrom' + db←clt.Open folder + + ix←db.Query('Name'((columns⍳⊂'Name')⊃data))⍬ ⍝ Should find everything + assert(1 2,⍪⍳¨4 1)≡ix + TEST←'Read it all back' + assert data≡db.Read time ix columns + + z←db.Close + clt.ShutDown 'Shutting down now!' + ⎕DL 3 + svrproc.Kill + ⎕DL 3 + + TEST←'Erase database' + db←⎕NEW #.vecdb(,⊂folder) + assert 0={db.Erase}time ⍬ + + z←'Server Tests Completed' + ∇ + + ∇ x←output x + :If LOG ⋄ ⍞←x ⋄ :EndIf + ∇ + + ∇ r←fmtnum x + ⍝ Nice formatting of large integers + r←(↓((⍴x),20)⍴'CI20'⎕FMT⍪,x)~¨' ' + ∇ + + assert←{'Assertion failed'⎕SIGNAL(⍵=0)/11} + + time←{⍺←⊣ ⋄ t←⎕AI[3] + o←output TEST,' ... ' + z←⍺ ⍺⍺ ⍵ + o←output(⍕⎕AI[3]-t),'ms',⎕UCS 10 + z + } + + expecterror←{ + 0::⎕SIGNAL(⍺≡⊃⎕DMX.DM)↓11 + z←⍺⍺ ⍵ + ⎕SIGNAL 11 + } + +:EndNamespace diff --git a/dev.dyapp b/dev.dyapp index 0422624..edd52ae 100644 --- a/dev.dyapp +++ b/dev.dyapp @@ -1,2 +1,8 @@ -Load TestVecdb -Load vecdb \ No newline at end of file +Load vecdb +Load vecdbclt +Load vecdbsrv +Load MakeBoot +Load BootServers +Load APLProcess +Load TestVecdb +Load TestVecdbSrv \ No newline at end of file diff --git a/vecdbboot.dws b/vecdbboot.dws new file mode 100644 index 0000000000000000000000000000000000000000..ea9909c96654b99537d5351071a129a69c5a0805 GIT binary patch literal 41528 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 M#+d~jhf>b|2SbUzr~m)} literal 0 HcmV?d00001