deuill / go-php

PHP bindings for the Go programming language (Golang)
MIT License
925 stars 105 forks source link

Long running PHP script #56

Open clanstyles opened 6 years ago

clanstyles commented 6 years ago

How would you handle a long running PHP script? How would we get "sigterm" notifications to allow the PHP script to close gracefully?

deuill commented 6 years ago

Generally speaking, calling context.Destroy() or engine.Destroy() will gracefully shut down PHP and call any defined __destruct handlers for instantiated classes. So, for a PHP script like this:

<?php

class Test {
    public function __construct() {
        echo 'Constructing Test' . PHP_EOL;
    }

    public function __destruct() {
        echo 'Destroying Test' . PHP_EOL;
    }

    public function run() {
        while (true) {
            echo $i++ . PHP_EOL;
            sleep(1);
        }
    }
}

$t = new Test;
$t->run();

And a Go program like this:

package main

import (
    "fmt"
    "github.com/deuill/go-php"
    "os"
    "os/signal"
)

func main() {
    engine, _ := php.New()

    context, _ := engine.NewContext()
    context.Output = os.Stdout

    var stop = make(chan os.Signal, 1)
    signal.Notify(stop, os.Interrupt, os.Kill)

    go func() {
        fmt.Println("Executing script...")
        context.Exec("index.php")
    }()

    <-stop

    fmt.Println("\rDestroying engine...")
    engine.Destroy()
}

You'd get output like this:

Executing script...
Instantiating Test

1
2
3
4
Destroying engine...
Destroying Test

It is not possible to bind variables into running scripts (that is, running context.Bind does not affect current instances of context.Exec or context.Eval). However, it may be interesting to have channels exposed as custom resources, with an accompanying API for handling these.

clanstyles commented 6 years ago

Hey @deuill thanks for the response!

I like the use of a class deconstructor to control the shutting down, I hadn't thought of that. In additional tests, it doesn't seem like you can create multiple instances of a script, even if you share the context without a segfault / memory corruption. Do you have any examples of that? These would just be isolated scripts with nothing in common.

deuill commented 6 years ago

Correct, running concurrent scripts in the same context is treacherous, even if the two share no global scope (class names, function names, global/file-level variables etc).

There is no real way to avoid this without using PHP with ZTS, which isn't currently supported here (I "temporarily" removed ZTS support during the move to PHP7, as it was significantly refactored between PHP5 and PHP7). I'd be glad to help if you or anyone else wanted to go down the rabbit-hole of integrating ZTS back in, however most of the things I've used go-php for are limited to one execution thread (as stupid and limiting that may seem).

clanstyles commented 6 years ago

Yeah I'd be interested in helping. I have a use case where I would prefer to keep mpst of the code in Go but run a library in PHP. I need to be able to run many instances isolated.

Where do we start? Whats required to get ZTS support working?

deuill commented 6 years ago

While the way PHP handles thread-safety has been cleaned up between PHP 5.x and 7.x (starting with this commit), the principle is the same: configuring PHP with --enable-maintainer-zts will define the ZTS macro, which is checked during compilation.

Documentation on PHP internals is miserable, and the code isn't documented for the most part either, so the way I've mostly developed through this library is trying to find equivalent functionality in the PHP source code.

Take, for example, the engine_init function, which runs for php.New() and initializes the PHP VM. My starting point for this was the equivalent embed SAPI and the php_embed_init function in particular. You may notice similarities. In a similar vein, support for binding Go method receivers as PHP classes was based on internal PHP classes, such as curl.

Adding support for ZTS would involve looking at points of integration such as engine_init and adding equivalent #ifdef ZTS parts just like the PHP source does it, for example:

#ifdef ZTS
  tsrm_startup(1, 1, 0, NULL);
  (void)ts_resource(0);
  ZEND_TSRMLS_CACHE_UPDATE();
#endif

Which should go near the top of engine_init, just like it appears near the top of php_embed_init.

Supporting ZTS across PHP 5.x and 7.x is a PITA, so I wouldn't bother supporting PHP 5.x. Getting pre-built PHP versions with ZTS enabled is also a major PITA, so I'd suggest extending the Dockerfile to include the necessary ./configure parameter, and use that environment for testing (via make docker-test).