The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.

NAME

Patro - proxy access to remote objects

VERSION

0.12

SYNOPSIS

    # on machine 1 (server)
    use Patro;
    my $obj = ...
    $config = patronize($obj);
    $config->to_file( 'config_file' );


    # on machines 2 through n (clients)
    use Patro;
    my ($proxy) = Patro->new( 'config_file' )->getProxies;
    ...
    $proxy->{key} = $val;         # updates $obj->{key} for obj on server
    $val = $proxy->method(@args); # calls $obj->method for obj on server

DESCRIPTION

Patro is a mechanism for making any Perl reference in one Perl program accessible is other processes, even processes running on different hosts. The "proxy" references have the same look and feel as the native references in the original process, and any manipulation of the proxy reference will have an effect on the original reference.

Some important features:

  • Hash members and array elements

    Accessing or updating hash values or array values on a remote reference is done with the same syntax as with the local reference:

        # host 1
        use Patro;
        my $hash1 = { abc => 123, def => [ 456, { ghi => "jkl" }, "mno" ] };
        my $config = patronize($hash1);
        ...
    
        # host 2
        use Patro;
        my $hash2 = Patro->new($config)->getProxies;
        print $hash2->{abc};                # "123"
        $hash2->{def}[2] = "pqr";           # updates $hash1 on host 1
        print delete $hash2->{def}[1]{ghi}; # "jkl", updates $hash1 on host1
  • Remote method calls

    Method calls on the proxy object are propagated to the original object, affecting the remote object and returning the result of the call.

        # host 1
        use Patro;
        sub Foofie::new { bless \$_[1],'Foofie' }
        sub Foofie::blerp { my $self=shift; wantarray ? (5,6,7,$$self) : ++$$self }
        patronize(Foofie->new(17))->to_file('/config/file');
        ...
    
        # host 2
        use Patro;
        my $foo = Patro->new('/config/file')->getProxies;
        my @x = $foo->blerp;           # (5,6,7,17)
        my $x = $foo->blerp;           # 18
  • Overloaded operators

    Any overloaded operations on the original object are supported on the remote object.

        # host 1
        use Patro;
        my $obj = Barfie->new(2,5);
        $config = patronize($obj);
        $config->to_file( 'config' );
        package Barfie;
        use overload '+=' => sub { $_ += $_[1] for @{$_[0]->{vals}};$_[0] },
             fallback => 1;
        sub new {
            my $pkg = shift;
            bless { vals => [ @_ ] }, $pkg;
        }
        sub prod { my $self = shift; my $z=1; $z*=$_ for @{$_[0]->{vals}}; $z }
    
        # host 2
        use Patro;
        my $proxy = getProxies('config');
        print $proxy->prod;      # calls Barfie::prod($obj) on host1, 2 * 5 => 10
        $proxy += 4;             # calls Barfie '+=' sub on host1
        print $proxy->prod;      # 6 * 9 => 54
  • Code references

    Patro supports sharing code references and data structures that contain code references (think dispatch tables). Proxies to these code references can invoke the code, which will then run on the server.

        # host 1
        use Patro;
        my $foo = sub { $_[0] + 42 };
        my $d = {
            f1 => sub { $_[0] + $_[1] },
            f2 => sub { $_[0] * $_[1] },
            f3 => sub { int( $_[0] / ($_[1] || 1) ) },
            g1 => sub { $_[0] += $_[1]; 18 },
        };
        patronize($foo,$d)->to_file('config');
        ...
    
        # host 2
        use Patro;
        my ($p_foo, $p_d) = getProxies('config');
        print $p_foo->(17);        # "59"   (42+17)
        print $p_d->{f1}->(7,33);  # "40"   (7+33)
        print $p_d->{f3}->(33,7);  # "4"    int(33/7)
        ($x,$y) = (5,6);
        $p_d->{g1}->($x,$y);
        print $x;                  # "11"   ($x:6 += 5)
  • filehandles

    Filehandles can also be shared through the Patro framework.

        # host 1
        use Patro;
        open my $fh, '>', 'host1.log';
        patronize($fh)->to_file('config');
        ...
    
        # host 2
        use Patro;
        my $ph = getProxies('config');
        print $ph "A log message for the server\n";

    Calling open through a proxy filehandle presents some security concerns. A client could read or write any file on the server host visible to the server's user id. Or worse, a client could open a pipe through the handle to run an arbitrary command on the server. open and close operations on proxy filehandles will not be allowed unless the process running the Patro server imports Patro with the :insecure tag.

    Certain operations are not currently supported through proxy filehandles: truncate PROXY_FH,LENGTH, flock PROXY_FH,OPERATION, fcntl PROXY_FH,FUNCTION,SCALAR, stat PROXY_FH, and -X PROXY_FH.

FUNCTIONS

patronize

    CONFIG = patronize(@REFS)

Creates a server on the local machine that provides proxy access to the given list of references. It returns an object with information about how to connect to the server.

The returned object has to_string and to_file methods to store the configuration where it can be read by other processes. Either the object, its string representation, or the filename containing config information may be used as input to the "getProxies" function to retrieve proxies to the shared references.

getProxies

    PROXIES = getProxies(CONFIG)
    PROXIES = getProxies(STRING)
    PROXIES = getProxies(FILENAME)

Connects to a server on another machine, specified in the CONFIG string, and returns proxies to the list of references that are served. In scalar context, returns a proxy to the first reference that is served.

See the "PROXIES" section below for what you can do with the output of this function.

ref

    TYPE = Patro::ref(PROXY)

For the given proxy object, returns the ref type of the remote object being served. If the input is not a proxy, returns undef. See also "reftype".

reftype

    TYPE = Patro::reftype(PROXY)

Returns the simple reference type (e.g., ARRAY) of the remote object associated with the given proxy, as if we were calling Scalar::Util::reftype on the remote object. Returns undef if the input is not a proxy object.

client

    CLIENT = Patro::client(PROXY)

Returns the IPC client object used by the given proxy to communicate with the remote object server. The client object contains information about how to communicate with the server and other connection configuration.

PROXIES

Proxy references, as returned by the "getProxies" function above, or sometimes returned in other calls to the server, are designed to look and feel as much as possible as the real references on the remote server that they provide access to, so any operation or expression with the proxy on the local machine should evaluate to the same value(s) as the same operation or expression with the real object/reference on the remote server. When the server if using threads and is sharing the served objects between threads, an update to the proxy object will affect the remote object, and vice versa.

Example 1: network file synchronization

Network file systems are notoriously flaky when it comes to synchronizing files that are being written to by processes on many different hosts [citation needed]. Patro provides a workaround, in that every machine can hold to a proxy to an object that writes to a file, with the object running on a single machine.

    # master
    package SyncWriter;
    use Fcntl qw(:flock SEEK_END);
    sub new {
        my ($pkg,$filename) = @_;
        open my $fh, '>', $filename;
        bless { fh => $fh }, $pkg;
    }
    sub print {
        my $self = shift;
        flock $self->{fh}, LOCK_EX;
        seek $self->{fh}, 0, SEEK_END;
        print {$self->{fh}} @_;
        flock $self->{fh}, LOCK_UN;
    }
    sub printf { ... }

    use Patro;
    my $writer = SyncWriter->new("file.log");
    my $cfg = patronize($writer);
    open my $fh,'>','/network/accessible/file';
    print $fh $cfg;
    close $fh;
    ...

    # slaves
    use Patro;
    open my $fh, '<', '/network/accessible/file';
    my $cfg = <$fh>;
    close $fh;
    my $writer = Patro->new($cfg)->getProxies;
    ...
    # $writer->print with a proxy $writer
    # invokes $writer->print on the host. Since all
    # the file operations are done on a single machine,
    # there are no network synchronization issues
    $writer->print("a log message\n");
    ...

Example 2: Distributed queue

A program that distributes tasks to several threads or several child processes can be extended to distribute tasks to several machines.

    # master
    use Patro;
    my $queue = [ 'job 1', 'job 2', ... ];
    patronize($queue)->to_file('/network/accessible/file');
    ...

    # slaves
    use Patro;
    my $queue = Patro->new('/network/accessible/file')->getProxies;

    while (my $task = shift @$queue) {
        ... do task ...
    }

(This example will not work without threads. For a more robust network-safe queue that will run with forks, see Forks::Queue)

Example 3: Keep your code secret

If you distribute your Perl code for others to use, it is very difficult to keep others from being able to see (and potentially steal) your code. Obfuscators are penetrable by any determined reverse engineer. Most other suggestions for keeping your code secret revolve around running your code on a server, and having your clients send input and receive output through a network service.

The Patro framework can make this service model easier to use. Design a small set of objects that can execute your code, provide your clients with a public API for those objects, and make proxies to your objects available through Patro.

    # code to run on client machine
    use Patro;
    my $cfg = ...    # provided by you
    my ($obj1,$obj2) = Patro->new($cfg)->getProxies;
    $result = $obj1->method($arg1,$arg2);
    ...

In this model, the client can use the objects and methods of your code, and inspect the members of your objects through the proxies, but the client cannot see the source code.

ENVIRONMENT

Patro pays attention to the following environment variables.

PATRO_THREADS

If the environment variable PATRO_THREADS is set, Patro will use it to determine whether to use a forked server or a threaded server to provide proxy access to objects. If this variable is not set, Patro will use threads if the threads module can be loaded.

LIMITATIONS

Proxy filehandles use the tie mechanism for filehandles, so sysopen, truncate, flock, fcntl, stat HANDLE, and -X HANDLE functions are not supported on proxy filehandles, as documented in perltie.

When the server uses forks (because threads are unavailable or because "PATRO_THREADS" was set to a false value), it is less practical to share variables between processes. When you manipulate a proxy reference, you are manipulating the copy of the reference running in a different process than the remote server. So you will not observe a change in the reference on the server (unless you use a class that does not save state in local memory, like Forks::Queue).

DOCUMENTATION AND SUPPORT

Up-to-date (blead version) sources for Patro are on github at https://github.com/mjob/p5-patro

You can find documentation for this module with the perldoc command.

    perldoc Patro

You can also look for information at:

LICENSE AND COPYRIGHT

MIT License

Copyright (c) 2017, Marty O'Brien

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.