» /rd/lib.framework/Api/RadarisGo/RadarisGo.php

<?

/**
 *
 *  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->$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 $socketarray_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($method2), ...$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($method1);
        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 <= ? [=> $args] : ["count" => $cnt]));
        
$data $method."\n".implode("\n"$args)."\n.\n";    // todo - change to \0

        
if ($this->socket_name[0] == '/') {
            
$socket socket_create(AF_UNIXSOCK_STREAM0);
            
socket_set_option($socketSOL_SOCKETSO_RCVTIMEO, array('sec' => 10'usec' => 0));
            
socket_set_option($socketSOL_SOCKETSO_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_INETSOCK_STREAMSOL_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$datastrlen($data));
        
$buffer "";

        while ($b socket_read($socket2048)) {

            
$buffer .= $b;
        }
        
socket_close($socket);
        
$r json_decode($buffer1);
        
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($buffer100)."`");
        return 
$r// as array
    
}


}