<?
/**
*
* Universal Radaris GO API
*
* support:
* * unix sockets "/run/radaris/$name.sock"
* * tcp sockets "port:host"
* * array of above - will choose random server on instantiation
*
* Usage:
* * define instance.api.$name node
* provide: 'socket' param
* format: '/path-to-socket' or 'hostname:port' or array of 'hostname:port'
*
* * $instance->$method(...$args)
* - unix or tcp socket call
*
* * "H_$method" prefix:
* - return HASH [$request_item => $result]
* Ex:
* > i('go-api.name-server')->h_fl2name(996432415014, 9154220285442499)
* [996432415014 => Statzer Ladopoulos, 9154220285442499 => Sergey Parf]
* > i('go-api.name-server')->fl2name(996432415014, 9154220285442499)
* ["Statzer Ladopoulos","Sergey Parf"]
*
* * "_$method" prefix: - raw api call, ignore overloaded method
* Ex:
* i('go-api.name-server')->_fname_comp("Ann")
*/
/*
Examples:
* i("Api_RadarisGo", "/name-server") --
* i('go-api.name-server')->fl2name(996432415014) -- call predefined API
* i("go-api.name-server", ['socket' => "localhost:6060"]) -- overload predefined params
* i("go-api.name-server", ['name' => "service-name"]) -- name for logging and caching
* i("go-api.name-server", ['socket' => "localhost:6060", 'backup-socket' => "host1:6060"]) # backup socket
* i("go-api.name-server", ['socket' => ['d-spider:6060','pa9-2:6060']]) -- load balancing
* i("go-api.name-server", ['backup-socket' => ['d-spider:6060','pa9-2:6060']]) -- load balancing backup socket case
*/
class Api_DownException extends RuntimeException {}
class Api_RadarisGo {
public $socket_name;
public $name; // name used for reporting
public $host; // IP
public $o_host; // original host
public $port;
public $C; // config
// "socket" is "/run/radaris"."/socket" or "hostname:port" OR (array of `socket`)
// "name" (optional) is identifier in logs
// Usage:
// * i("go-api.$name") -- socket should be defined in instance.go-api.$service-name.socket
// * i("Api_RadarisGo", "socket") -- socket defined explicitly - used for tests
function __construct($a) { # see above
$this->C = $a;
$socket = "";
if ($a['name'] ?? 0) { // Cached Fallback
$K = "rd-api-".$a['name'];
if ($cache = Cache_SHM::get($K)) {
$socket = $cache;
Profiler::alert("radaris-go-api", "'$a[name]' down. using backup server: ".$socket);
}
}
if (! $socket)
$socket = $a['socket'] ?? $a['_'] ?? ""; // suggested form i('go-api.Service')
if (! $socket)
\Log::Alert("Unknown service $a[_]");
if (is_array($socket)) { // we have a pool of possible servers
$socket = $socket[ array_rand($socket) ]; // choose random server from list
}
$this->name = $a['name'] ?? $socket;
$this->_init($socket);
}
protected function _init($socket) {
if ($socket[0] == '/') {
$socket = "/run/radaris".$socket.".sock";
} else {
[$host, $this->port] = explode(":", $socket);
if (! $this->port)
\Log::Alert("Socket should in in form of /path or host:port");
$this->o_host = $host;
$this->host = host2ip($host); // use up instead of host
}
Profiler::info("Go-API init", ['name' => $this->name, 'socket' => $socket]);
$this->socket_name = $socket;
}
// save fallback for $timeout
protected function saveFallback($socket, $timeout = 60) {
$K = "rd-api-".$this->C['name'];
Cache_SHM::put($K, $socket, $timeout);
}
/**
* Split Huge Calls into many API calls
* __call wrapper - when count($args) > limit
*/
function __xCall(string $method, array $args) {
$cnt = count($args);
if ($cnt <= static::$MAX_ARRAY_ELEMENTS_PER_REQUEST)
return $this->__call($method, $args);
// Split Query. Do multiple API requests
$r = [];
foreach (array_chunk($args, static::$MAX_ARRAY_ELEMENTS_PER_REQUEST) as $chunk) {
// API always receive an unindexed list of args and return unindexed results
$r = array_merge($r, $this->__call($method, $chunk));
}
return $r;
}
/**
* Ex:
*
* $NS = i("Api_RadarisGo", "/name-server")
* $NS->name2id("Anna\tSmith")[0]
* $NS->name2id(["Jim", "Kerry"], ["Tom", "Brown"], ["Abbath", "Occulta"])
* $NS->name2fl(["Jim", "Kerry"], ["Abbath", "Occulta"])
* $NS->id2name(...HB::fl(30689804779034126))
* $NS->fl2name(996432415014)
*
* "h_$method" prefix
*
*/
function __call($method, $args) {
if (! strncmp($method, "h_", 2)) {
$z = $this->socketCall(substr($method, 2), ...$args);
$r = [];
foreach ($args as $k => $a) {
if (is_array($a))
$a = implode("\t", $a);
$r[$a] = $z[$k];
}
return $r;
}
// raw go-api method call
if ($method[0] == "_")
$method = substr($method, 1);
try {
return $this->socketCall($method, ...$args);
} catch (Api_DownException $ex) {
1;
}
// Api_DownException !!
if ($backup = $this->C['backup-socket'] ?? 0) {
if ($cnt = once())
\Log::warning("radaris-go-api down. socket: ".$this->socket_name." using backup: $backup ; calls missed: $cnt");
if (is_array($backup))
$backup = $backup[array_rand($backup)]; // get random element
$this->_init($backup);
$r = $this->socketCall($method, ...$args); // throws exception
$this->saveFallback($backup); // saving Fallback server ONLY on success
return $r;
}
if ($cnt = once())
\Log::warning("radaris-go-api down (no backup found). socket: ".$this->socket_name." calls missed: $cnt");
throw $ex;
}
/**
* Socket communication format:
* Lines: method, arguments(optionally tab-delimited) - one argument per line
* return data is in JSON format
*
* Almost all methods supports batch mode, so every item is ONE ITEM
*
* Example:
* method-name
* arg1 << optionally tab-delimited
* arg2 << optionally tab-delimited
* arg3 << optionally tab-delimited
* ...
* . << END OF DATA
*
*/
function socketCall($method, ...$args) {
foreach ($args as &$a) {
if (is_array($a))
$a = implode("\t", $a);
}
$cnt = count($args);
Profiler::in("API:".$this->name, [$method] + ($cnt <= 4 ? [1 => $args] : ["count" => $cnt]));
$data = $method."\n".implode("\n", $args)."\n.\n"; // todo - change to \0
if ($this->socket_name[0] == '/') {
$socket = socket_create(AF_UNIX, SOCK_STREAM, 0);
socket_set_option($socket, SOL_SOCKET, SO_RCVTIMEO, array('sec' => 10, 'usec' => 0));
socket_set_option($socket, SOL_SOCKET, SO_SNDTIMEO, array('sec' => 10, 'usec' => 0));
CD::set("NOWARN",1); # @ is not enough
if (!(@socket_connect($socket, $this->socket_name) ?? false)) {
\Log::warning("can't connect to '$this->socket_name'");
throw new Api_DownException("Go-Api(socket) call $this->name::$method(".cut(json_encode($args), 100).") failed");
}
CD::set("NOWARN",0);
} else { // TCP socket
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
#$socket = fsockopen($this->host, $this->port, $errno, $errstr, 2);
CD::set("NOWARN",1); # @ is not enough
$res = @socket_connect($socket, $this->host, $this->port) ?? false;
CD::set("NOWARN",0);
if (! $res) {
if ($cnt = once())
\Log::warning("can't connect to '$this->socket_name' $cnt calls missed");
throw new Api_DownException("Go-Api($this->o_host,$this->port) $this->name::$method(".cut(json_encode($args), 100).") failed");
}
}
socket_write($socket, $data, strlen($data));
$buffer = "";
$buffer .= $b;
}
socket_close($socket);
$r = json_decode($buffer, 1);
Profiler::out();
if ($r === null && $buffer)
throw new Exception("Go-Api($this->o_host,$this->port / $this->socket_name) call $this->name::$method(".cut(json_encode($args), 100).") failed. response: `".cut($buffer, 100)."`");
return $r; // as array
}
}