Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Thread autoload bootstrap #110

Open
wants to merge 17 commits into
base: fork
Choose a base branch
from
Open
13 changes: 13 additions & 0 deletions classes/thread.c
Original file line number Diff line number Diff line change
Expand Up @@ -129,3 +129,16 @@ Thread_method(getRunningCount)

RETURN_LONG(count);
}

/* {{{ proto void Thread::setAutoloadFile(string|null $file)
Sets the path of the file used to set up new threads */
Thread_method(setAutoloadFile)
{
zend_string* file;

ZEND_PARSE_PARAMETERS_START_EX(ZEND_PARSE_PARAMS_THROW, 1, 1)
Z_PARAM_STR(file)
ZEND_PARSE_PARAMETERS_END();

pmmpthread_globals_set_autoload_file(file);
} /* }}} */
17 changes: 17 additions & 0 deletions src/globals.c
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,8 @@ zend_bool pmmpthread_globals_init(){
PMMPTHREAD_G(thread_count) = 0; //only counting threads explicitly created by pmmpthread
}

PMMPTHREAD_G(autoload_file) = NULL;

#define INIT_STRING(n, v) do { \
PMMPTHREAD_G(strings).n = zend_new_interned_string(zend_string_init(v, 1)); \
} while(0)
Expand Down Expand Up @@ -222,6 +224,21 @@ zend_bool pmmpthread_globals_socket_shared(PHP_SOCKET socket) {
}
#endif

/* {{{ */
zend_bool pmmpthread_globals_set_autoload_file(const zend_string *path) {
if (pmmpthread_globals_lock()) {
zend_string *copy = path ? zend_string_init(ZSTR_VAL(path), ZSTR_LEN(path), 1) : NULL;

if (PMMPTHREAD_G(autoload_file)) {
zend_string_release(PMMPTHREAD_G(autoload_file));
}
PMMPTHREAD_G(autoload_file) = copy;
pmmpthread_globals_unlock();
return 1;
}
return 0;
} /* }}} */

/* {{{ */
void pmmpthread_globals_shutdown() {
if (PMMPTHREAD_G(init)) {
Expand Down
8 changes: 8 additions & 0 deletions src/globals.h
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@ struct _pmmpthread_globals {

zval undef_zval;

/*
* File included on all new threads before any user code runs, usually an autoloader
*/
zend_string *autoload_file;

/*
* High Frequency Strings
*/
Expand Down Expand Up @@ -113,6 +118,9 @@ zend_bool pmmpthread_globals_lock(); /* }}} */
/* {{{ release global lock */
void pmmpthread_globals_unlock(); /* }}} */

/* {{{ set autoload file used to bootstrap new threads */
zend_bool pmmpthread_globals_set_autoload_file(const zend_string *autoload_file); /* }}} */

/* {{{ shutdown global structures */
void pmmpthread_globals_shutdown(); /* }}} */

Expand Down
7 changes: 5 additions & 2 deletions src/prepare.c
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
#include <src/copy.h>
#include <Zend/zend_enum.h>
#include <Zend/zend_observer.h>
#include <Zend/zend_stream.h>

#define PMMPTHREAD_PREPARATION_BEGIN_CRITICAL() pmmpthread_globals_lock();
#define PMMPTHREAD_PREPARATION_END_CRITICAL() pmmpthread_globals_unlock()
Expand Down Expand Up @@ -826,8 +827,7 @@ static inline void pmmpthread_prepare_sapi(const pmmpthread_ident_t* source) {
} /* }}} */

/* {{{ */
int pmmpthread_prepared_startup(pmmpthread_object_t* thread, pmmpthread_monitor_t *ready, zend_class_entry *thread_ce, zend_ulong thread_options) {

int pmmpthread_prepared_startup(pmmpthread_object_t* thread, pmmpthread_monitor_t *ready, zend_class_entry *thread_ce, zend_ulong thread_options, zend_string **autoload_file) {
PMMPTHREAD_PREPARATION_BEGIN_CRITICAL() {
thread->local.id = pmmpthread_self();
thread->local.ls = ts_resource(0);
Expand Down Expand Up @@ -882,6 +882,9 @@ int pmmpthread_prepared_startup(pmmpthread_object_t* thread, pmmpthread_monitor_
if (thread_options & PMMPTHREAD_INHERIT_INCLUDES)
pmmpthread_prepare_includes(&thread->creator);

if (PMMPTHREAD_G(autoload_file)) {
*autoload_file = zend_string_init(ZSTR_VAL(PMMPTHREAD_G(autoload_file)), ZSTR_LEN(PMMPTHREAD_G(autoload_file)), 1);
}
pmmpthread_monitor_add(ready, PMMPTHREAD_MONITOR_READY);

PMMPTHREAD_G(thread_count)++;
Expand Down
2 changes: 1 addition & 1 deletion src/prepare.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ void pmmpthread_prepared_entry_late_bindings(const pmmpthread_ident_t* source, z
void pmmpthread_context_late_bindings(const pmmpthread_ident_t* source); /* }}} */

/* {{{ */
int pmmpthread_prepared_startup(pmmpthread_object_t* thread, pmmpthread_monitor_t *ready, zend_class_entry *thread_ce, zend_ulong thread_options); /* }}} */
int pmmpthread_prepared_startup(pmmpthread_object_t* thread, pmmpthread_monitor_t *ready, zend_class_entry *thread_ce, zend_ulong thread_options, zend_string **autoload_file); /* }}} */

/* {{{ */
void pmmpthread_call_shutdown_functions(void); /* }}} */
Expand Down
67 changes: 66 additions & 1 deletion src/routine.c
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,57 @@ static void pmmpthread_routine_free(pmmpthread_routine_arg_t* r) {
pmmpthread_monitor_destroy(&r->ready);
} /* }}} */

/* {{{ Includes the autoloader provided, if any. This code is borrowed from krakjoe/parallel. */
static int pmmpthread_routine_run_bootstrap(zend_string* file) {
zend_file_handle fh;
zend_op_array* ops;
zval rv;
int result;

if (!file) {
return SUCCESS;
}

zend_stream_init_filename_ex(&fh, file);
result = php_stream_open_for_zend_ex(&fh, USE_PATH | REPORT_ERRORS | STREAM_OPEN_FOR_INCLUDE);

if (result != SUCCESS) {
zend_error(E_ERROR, "Unable to open thread autoload file %s", ZSTR_VAL(file));
return FAILURE;
}

zend_hash_add_empty_element(&EG(included_files),
fh.opened_path ?
fh.opened_path : file);

ops = zend_compile_file(&fh, ZEND_REQUIRE);

zend_destroy_file_handle(&fh);

if (ops) {
ZVAL_UNDEF(&rv);
zend_execute(ops, &rv);
destroy_op_array(ops);
efree(ops);

if (EG(exception)) {
zend_exception_error(EG(exception), E_ERROR);
zend_error(E_ERROR, "Uncaught exception thrown from thread autoload file %s", ZSTR_VAL(file));
return FAILURE;
}

zval_ptr_dtor(&rv);
return SUCCESS;
}

if (EG(exception)) {
zend_exception_error(EG(exception), E_ERROR);
zend_error(E_ERROR, "Error compiling thread autoload file %s", ZSTR_VAL(file));
}

return FAILURE;
} /* }}} */

/* {{{ */
static inline zend_result pmmpthread_routine_run_function(pmmpthread_zend_object_t* connection) {
zend_function* run;
Expand Down Expand Up @@ -106,12 +157,24 @@ static void* pmmpthread_routine(pmmpthread_routine_arg_t* routine) {
zend_ulong thread_options = routine->options;
pmmpthread_object_t* ts_obj = thread->ts_obj;
pmmpthread_monitor_t* ready = &routine->ready;
zend_string* autoload_file = NULL;

if (pmmpthread_prepared_startup(ts_obj, ready, thread->std.ce, thread_options) == SUCCESS) {
if (pmmpthread_prepared_startup(ts_obj, ready, thread->std.ce, thread_options, &autoload_file) == SUCCESS) {
pmmpthread_queue done_tasks_cache;
memset(&done_tasks_cache, 0, sizeof(pmmpthread_queue));

zend_first_try{
if (autoload_file != NULL) {
zend_try {
if (pmmpthread_routine_run_bootstrap(autoload_file) == FAILURE) {
zend_bailout();
}
} zend_catch {
zend_string_release(autoload_file);
zend_bailout();
} zend_end_try();
}

ZVAL_UNDEF(&PMMPTHREAD_ZG(this));
pmmpthread_object_connect(thread, &PMMPTHREAD_ZG(this));
if (pmmpthread_routine_run_function(PMMPTHREAD_FETCH_FROM(Z_OBJ_P(&PMMPTHREAD_ZG(this)))) == FAILURE) {
Expand Down Expand Up @@ -140,6 +203,8 @@ static void* pmmpthread_routine(pmmpthread_routine_arg_t* routine) {
}
}
}
} zend_catch {
pmmpthread_monitor_add(&ts_obj->monitor, PMMPTHREAD_MONITOR_ERROR);
} zend_end_try();

pmmpthread_call_shutdown_functions();
Expand Down
8 changes: 8 additions & 0 deletions stubs/Thread.stub.php
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,14 @@ public static function getSharedGlobals() : ThreadSafeArray{}
*/
public static function getRunningCount() : int{}

/**
* Sets the file to be included when a new thread is started
* Typically this would be the path of your vendor/autoload.php
*
* @param string $file
*/
public static function setAutoloadFile(string $file) : void{}

/**
* Will return the identity of the referenced Thread
*
Expand Down
8 changes: 7 additions & 1 deletion stubs/Thread_arginfo.h
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/* This is a generated file, edit the .stub.php file instead.
* Stub hash: f07c76a75d7bf70fbbcf7bfe841cc003d1ec6727 */
* Stub hash: 48854b4be7c9443ff7d4eb5490f3fd79a6bc6118 */

ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_pmmp_thread_Thread_getCreatorId, 0, 0, IS_LONG, 0)
ZEND_END_ARG_INFO()
Expand All @@ -14,6 +14,10 @@ ZEND_END_ARG_INFO()

#define arginfo_class_pmmp_thread_Thread_getRunningCount arginfo_class_pmmp_thread_Thread_getCreatorId

ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_pmmp_thread_Thread_setAutoloadFile, 0, 1, IS_VOID, 0)
ZEND_ARG_TYPE_INFO(0, file, IS_STRING, 0)
ZEND_END_ARG_INFO()

#define arginfo_class_pmmp_thread_Thread_getThreadId arginfo_class_pmmp_thread_Thread_getCreatorId

ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_pmmp_thread_Thread_isJoined, 0, 0, _IS_BOOL, 0)
Expand All @@ -33,6 +37,7 @@ ZEND_METHOD(pmmp_thread_Thread, getCurrentThread);
ZEND_METHOD(pmmp_thread_Thread, getCurrentThreadId);
ZEND_METHOD(pmmp_thread_Thread, getSharedGlobals);
ZEND_METHOD(pmmp_thread_Thread, getRunningCount);
ZEND_METHOD(pmmp_thread_Thread, setAutoloadFile);
ZEND_METHOD(pmmp_thread_Thread, getThreadId);
ZEND_METHOD(pmmp_thread_Thread, isJoined);
ZEND_METHOD(pmmp_thread_Thread, isStarted);
Expand All @@ -46,6 +51,7 @@ static const zend_function_entry class_pmmp_thread_Thread_methods[] = {
ZEND_ME(pmmp_thread_Thread, getCurrentThreadId, arginfo_class_pmmp_thread_Thread_getCurrentThreadId, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC)
ZEND_ME(pmmp_thread_Thread, getSharedGlobals, arginfo_class_pmmp_thread_Thread_getSharedGlobals, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC)
ZEND_ME(pmmp_thread_Thread, getRunningCount, arginfo_class_pmmp_thread_Thread_getRunningCount, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC)
ZEND_ME(pmmp_thread_Thread, setAutoloadFile, arginfo_class_pmmp_thread_Thread_setAutoloadFile, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC)
ZEND_ME(pmmp_thread_Thread, getThreadId, arginfo_class_pmmp_thread_Thread_getThreadId, ZEND_ACC_PUBLIC)
ZEND_ME(pmmp_thread_Thread, isJoined, arginfo_class_pmmp_thread_Thread_isJoined, ZEND_ACC_PUBLIC)
ZEND_ME(pmmp_thread_Thread, isStarted, arginfo_class_pmmp_thread_Thread_isStarted, ZEND_ACC_PUBLIC)
Expand Down
7 changes: 7 additions & 0 deletions tests/assets/TestAutoloadFile.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php declare(strict_types=1);

class TestAutoloadClass{
public function hi() : void{
var_dump("everything is great!");
}
}
5 changes: 5 additions & 0 deletions tests/assets/autoload-file-recursion.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?php

use pmmp\thread\Thread;

Thread::setAutoloadFile(__FILE__);
3 changes: 3 additions & 0 deletions tests/assets/autoload-file-syntax-error.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<?php

function test(
3 changes: 3 additions & 0 deletions tests/assets/autoload-file-uncaught-exception.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<?php

throw new \Exception("cya");
26 changes: 26 additions & 0 deletions tests/autoload-file-invalid-path.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
--TEST--
Test that using Thread::setAutoloadFile() with invalid paths errors correctly
--DESCRIPTION--
Not sure if we can validate paths at the time of setting them. They might not exist
when set, or might be deleted before we can use them. This means it's ultimately
up to the thread itself to handle errors from wrong include paths correctly.
--FILE--
<?php

use pmmp\thread\Thread;

Thread::setAutoloadFile(__DIR__ . '/assets/i-dont-exist.php');

$thread = new class extends Thread{
public function run() : void{
echo "unreachable\n";
}
};

$thread->start(Thread::INHERIT_NONE);
$thread->join();
?>
--EXPECTF--
Warning: Unknown: Failed to open stream: No such file or directory in Unknown on line 0

Fatal error: Unable to open thread autoload file %si-dont-exist.php in Unknown on line 0
26 changes: 26 additions & 0 deletions tests/autoload-file-invalid-script.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
--TEST--
Test that using Thread::setAutoloadFile() with a broken PHP file errors properly
--DESCRIPTION--
Not sure if we can validate paths at the time of setting them. They might not exist
when set, or might be deleted before we can use them. This means it's ultimately
up to the thread itself to handle errors from wrong include paths correctly.
--FILE--
<?php

use pmmp\thread\Thread;

Thread::setAutoloadFile(__DIR__ . '/assets/autoload-file-syntax-error.php');

$thread = new class extends Thread{
public function run() : void{
echo "unreachable\n";
}
};

$thread->start(Thread::INHERIT_NONE);
$thread->join();
?>
--EXPECTF--
Parse error: Unclosed '(' on line 3 in %s on line %d

Fatal error: Error compiling thread autoload file %sautoload-file-syntax-error.php in Unknown on line 0
20 changes: 20 additions & 0 deletions tests/autoload-file-recursion.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
--TEST--
Test that using Thread::setAutoloadFile() inside an autoload file doesn't cause a deadlock
--FILE--
<?php

use pmmp\thread\Thread;

Thread::setAutoloadFile(__DIR__ . '/assets/autoload-file-recursion.php');

$thread = new class extends Thread{
public function run() : void{
echo "ok\n";
}
};

$thread->start(Thread::INHERIT_NONE);
$thread->join();
?>
--EXPECT--
ok
25 changes: 25 additions & 0 deletions tests/autoload-file-throws-exception.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
--TEST--
Test that using Thread::setAutoloadFile() behaves properly when a file throws errors
--FILE--
<?php

use pmmp\thread\Thread;

Thread::setAutoloadFile(__DIR__ . '/assets/autoload-file-uncaught-exception.php');

$thread = new class extends Thread{
public function run() : void{
echo "unreachable\n";
}
};

$thread->start(Thread::INHERIT_NONE);
$thread->join();
?>
--EXPECTF--
Fatal error: Uncaught Exception: cya in %s:%d
Stack trace:
#0 {main}
thrown in %s on line %d

Fatal error: Uncaught exception thrown from thread autoload file %sautoload-file-uncaught-exception.php in Unknown on line 0
19 changes: 19 additions & 0 deletions tests/autoload-file.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
--TEST--
Tests basic functionality of Thread::setAutoloadFile()
--FILE--
<?php

use pmmp\thread\Thread;

Thread::setAutoloadFile(__DIR__ . '/assets/TestAutoloadFile.php');

$t = new class extends Thread{
public function run() : void{
(new TestAutoloadClass())->hi();
}
};
$t->start(Thread::INHERIT_NONE);
$t->join();
?>
--EXPECT--
string(20) "everything is great!"