diff --git a/.travis.yml b/.travis.yml index 962d01232..723a2284d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -32,6 +32,9 @@ matrix: - php: 7.0 env: - SERVER_VERSION=3.4.11 + - php: 7.0 + env: + - SERVER_VERSION=4.0.0-rc4 before_install: - pip install "mongo-orchestration>=0.6.7,<1.0" --user `whoami` diff --git a/Vagrantfile b/Vagrantfile index f3c19df3f..a8e24d4c8 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -39,48 +39,5 @@ Vagrant.configure(2) do |config| ldap.vm.provision "shell", path: "scripts/centos/essentials.sh", privileged: true ldap.vm.provision "shell", path: "scripts/centos/ldap/install.sh", privileged: true end - - config.vm.define "freebsd", autostart: false do |bsd| - bsd.vm.network "private_network", ip: "192.168.112.30" - - bsd.vm.box = "geoffgarside/freebsd-10.0" - - bsd.vm.provision "shell", path: "scripts/freebsd/essentials.sh", privileged: true - bsd.vm.provision "file", source: "/tmp/PHONGO-SERVERS.json", destination: "/tmp/PHONGO-SERVERS.json" - bsd.vm.provision "file", source: "scripts/configs/.gdbinit", destination: "/home/vagrant/.gdbinit" - bsd.vm.provision "shell", path: "scripts/freebsd/phongo.sh", privileged: true - bsd.vm.synced_folder ".", "/phongo", :nfs => true, id: "vagrant-root" - end - - config.vm.define "precise64" do |linux| - linux.vm.network "private_network", ip: "192.168.112.40" - - linux.vm.box = "http://files.vagrantup.com/precise64.box" - linux.vm.provider "vmware_workstation" do |vmware, override| - override.vm.box_url = 'http://files.vagrantup.com/precise64_vmware.box' - override.vm.provision "shell", path: "scripts/vmware/kernel.sh", privileged: true - end - - linux.vm.provision "shell", path: "scripts/ubuntu/essentials.sh", privileged: true - linux.vm.provision "file", source: "/tmp/PHONGO-SERVERS.json", destination: "/tmp/PHONGO-SERVERS.json" - linux.vm.provision "file", source: "scripts/configs/.gdbinit", destination: "/home/vagrant/.gdbinit" - linux.vm.provision "shell", path: "scripts/ubuntu/phongo.sh", privileged: true - end - - config.vm.define "precise32" do |linux| - linux.vm.network "private_network", ip: "192.168.112.50" - - linux.vm.box = "bjori/precise32" - linux.vm.provider "vmware_workstation" do |vmware, override| - override.vm.box_url = "bjori/precise32" - override.vm.provision "shell", path: "scripts/vmware/kernel.sh", privileged: true - end - - linux.vm.provision "shell", path: "scripts/ubuntu/essentials.sh", privileged: true - linux.vm.provision "file", source: "/tmp/PHONGO-SERVERS.json", destination: "/tmp/PHONGO-SERVERS.json" - linux.vm.provision "file", source: "scripts/configs/.gdbinit", destination: "/home/vagrant/.gdbinit" - linux.vm.provision "shell", path: "scripts/ubuntu/phongo.sh", privileged: true - end - end diff --git a/php_phongo.c b/php_phongo.c index a8b28da56..5c11df16c 100644 --- a/php_phongo.c +++ b/php_phongo.c @@ -180,11 +180,39 @@ void phongo_throw_exception(php_phongo_error_domain_t domain TSRMLS_DC, const ch efree(message); va_end(args); } + void phongo_throw_exception_from_bson_error_t(bson_error_t* error TSRMLS_DC) { zend_throw_exception(phongo_exception_from_mongoc_domain(error->domain, error->code), error->message, error->code TSRMLS_CC); } +void phongo_throw_exception_from_bson_error_and_reply_t(bson_error_t* error, bson_t* reply TSRMLS_DC) +{ + /* Server errors (other than ExceededTimeLimit) and write concern errors + * may use CommandException and report the result document for the + * failed command. For BC, ExceededTimeLimit errors will continue to use + * ExcecutionTimeoutException and omit the result document. */ + if ((error->domain == MONGOC_ERROR_SERVER && error->code != PHONGO_SERVER_ERROR_EXCEEDED_TIME_LIMIT) || error->domain == MONGOC_ERROR_WRITE_CONCERN) { +#if PHP_VERSION_ID >= 70000 + zval zv; +#else + zval* zv; +#endif + + zend_throw_exception(php_phongo_commandexception_ce, error->message, error->code TSRMLS_CC); + php_phongo_bson_to_zval(bson_get_data(reply), reply->len, &zv); + +#if PHP_VERSION_ID >= 70000 + phongo_add_exception_prop(ZEND_STRL("resultDocument"), &zv); +#else + phongo_add_exception_prop(ZEND_STRL("resultDocument"), zv TSRMLS_CC); +#endif + zval_ptr_dtor(&zv); + } else { + phongo_throw_exception_from_bson_error_t(error TSRMLS_CC); + } +} + static void php_phongo_log(mongoc_log_level_t log_level, const char* log_domain, const char* message, void* user_data) { struct timeval tv; @@ -927,29 +955,7 @@ bool phongo_execute_command(mongoc_client_t* client, php_phongo_command_type_t t free_reply = true; if (!result) { - /* Server errors (other than ExceededTimeLimit) and write concern errors - * may use CommandException and report the result document for the - * failed command. For BC, ExceededTimeLimit errors will continue to use - * ExcecutionTimeoutException and omit the result document. */ - if ((error.domain == MONGOC_ERROR_SERVER && error.code != PHONGO_SERVER_ERROR_EXCEEDED_TIME_LIMIT) || error.domain == MONGOC_ERROR_WRITE_CONCERN) { -#if PHP_VERSION_ID >= 70000 - zval zv; -#else - zval* zv; -#endif - - zend_throw_exception(php_phongo_commandexception_ce, error.message, error.code TSRMLS_CC); - php_phongo_bson_to_zval(bson_get_data(&reply), reply.len, &zv); - -#if PHP_VERSION_ID >= 70000 - phongo_add_exception_prop(ZEND_STRL("resultDocument"), &zv); -#else - phongo_add_exception_prop(ZEND_STRL("resultDocument"), zv TSRMLS_CC); -#endif - zval_ptr_dtor(&zv); - } else { - phongo_throw_exception_from_bson_error_t(&error TSRMLS_CC); - } + phongo_throw_exception_from_bson_error_and_reply_t(&error, &reply TSRMLS_CC); goto cleanup; } diff --git a/php_phongo.h b/php_phongo.h index 8008b4ae4..7251359d5 100644 --- a/php_phongo.h +++ b/php_phongo.h @@ -111,6 +111,7 @@ void phongo_throw_exception(php_phongo_error_domain_t domain TSRMLS #endif /* PHP_VERSION_ID < 70000 */ ; void phongo_throw_exception_from_bson_error_t(bson_error_t* error TSRMLS_DC); +void phongo_throw_exception_from_bson_error_and_reply_t(bson_error_t* error, bson_t* reply TSRMLS_DC); /* This enum is used for processing options in phongo_execute_parse_options and * selecting a libmongoc function to use in phongo_execute_command. The values diff --git a/scripts/presets/replicaset-30.json b/scripts/presets/replicaset-30.json index 65c5d1f20..d09ac796f 100644 --- a/scripts/presets/replicaset-30.json +++ b/scripts/presets/replicaset-30.json @@ -12,6 +12,7 @@ "noprealloc": true, "nssize": 1, "port": 3100, + "bind_ip": "0.0.0.0,::", "smallfiles": true, "setParameter": {"enableTestCommands": 1} }, @@ -34,6 +35,7 @@ "noprealloc": true, "nssize": 1, "port": 3101, + "bind_ip": "0.0.0.0,::", "smallfiles": true, "setParameter": {"enableTestCommands": 1} }, @@ -56,6 +58,7 @@ "noprealloc": true, "nssize": 1, "port": 3102, + "bind_ip": "0.0.0.0,::", "smallfiles": true, "setParameter": {"enableTestCommands": 1} }, diff --git a/scripts/presets/replicaset-dns.json b/scripts/presets/replicaset-dns.json index 9850d7266..b57f7c8c4 100644 --- a/scripts/presets/replicaset-dns.json +++ b/scripts/presets/replicaset-dns.json @@ -12,6 +12,7 @@ "noprealloc": true, "nssize": 1, "port": 27017, + "bind_ip_all": true, "smallfiles": true, "setParameter": {"enableTestCommands": 1} }, @@ -30,6 +31,7 @@ "noprealloc": true, "nssize": 1, "port": 27018, + "bind_ip_all": true, "smallfiles": true, "setParameter": {"enableTestCommands": 1} }, @@ -48,6 +50,7 @@ "noprealloc": true, "nssize": 1, "port": 27019, + "bind_ip_all": true, "smallfiles": true, "setParameter": {"enableTestCommands": 1} }, diff --git a/scripts/presets/replicaset.json b/scripts/presets/replicaset.json index 88299b417..f89c58138 100644 --- a/scripts/presets/replicaset.json +++ b/scripts/presets/replicaset.json @@ -12,6 +12,7 @@ "noprealloc": true, "nssize": 1, "port": 3000, + "bind_ip_all": true, "smallfiles": true, "setParameter": {"enableTestCommands": 1} }, @@ -34,6 +35,7 @@ "noprealloc": true, "nssize": 1, "port": 3001, + "bind_ip_all": true, "smallfiles": true, "setParameter": {"enableTestCommands": 1} }, @@ -56,6 +58,7 @@ "noprealloc": true, "nssize": 1, "port": 3002, + "bind_ip_all": true, "smallfiles": true, "setParameter": {"enableTestCommands": 1} }, diff --git a/scripts/presets/standalone-30.json b/scripts/presets/standalone-30.json index 380a74ce6..9675dd38b 100644 --- a/scripts/presets/standalone-30.json +++ b/scripts/presets/standalone-30.json @@ -8,6 +8,7 @@ "logpath": "/tmp/standalone-30/mongod.log", "journal": true, "port": 2700, + "bind_ip": "0.0.0.0,::", "setParameter": {"enableTestCommands": 1} }, "version": "30-release" diff --git a/scripts/presets/standalone-auth.json b/scripts/presets/standalone-auth.json index f60537516..585784b98 100644 --- a/scripts/presets/standalone-auth.json +++ b/scripts/presets/standalone-auth.json @@ -11,6 +11,7 @@ "logpath": "/tmp/standalone-auth/m.log", "journal": true, "port": 2200, + "bind_ip_all": true, "setParameter": {"enableTestCommands": 1} } } diff --git a/scripts/presets/standalone-plain.json b/scripts/presets/standalone-plain.json index 13d41a972..62c50cb78 100644 --- a/scripts/presets/standalone-plain.json +++ b/scripts/presets/standalone-plain.json @@ -11,7 +11,8 @@ "logpath": "/tmp/standalone-plain/m.log", "journal": true, "port": 2400, - "setParameter": {"enableTestCommands": 1, "saslauthdPath": "/var/run/saslauthd/mux", "authenticationMechanisms": "MONGODB-CR,PLAIN"} + "bind_ip_all": true, + "setParameter": {"enableTestCommands": 1, "saslauthdPath": "/var/run/saslauthd/mux", "authenticationMechanisms": "SCRAM-SHA-1,PLAIN"} } } diff --git a/scripts/presets/standalone-ssl.json b/scripts/presets/standalone-ssl.json index 54534b4ea..ec41cbf31 100644 --- a/scripts/presets/standalone-ssl.json +++ b/scripts/presets/standalone-ssl.json @@ -8,6 +8,7 @@ "logpath": "/tmp/standalone-ssl/m.log", "journal": true, "port": 2100, + "bind_ip_all": true, "setParameter": {"enableTestCommands": 1} }, "sslParams": { diff --git a/scripts/presets/standalone-x509.json b/scripts/presets/standalone-x509.json index 20cf0365f..27002cd7a 100644 --- a/scripts/presets/standalone-x509.json +++ b/scripts/presets/standalone-x509.json @@ -10,6 +10,7 @@ "logpath": "/tmp/standalone-x509/m.log", "journal": true, "port": 2300, + "bind_ip_all": true, "setParameter": {"enableTestCommands": 1, "authenticationMechanisms": "MONGODB-X509"} }, "sslParams": { diff --git a/scripts/presets/standalone.json b/scripts/presets/standalone.json index 7ad841517..273df822e 100644 --- a/scripts/presets/standalone.json +++ b/scripts/presets/standalone.json @@ -8,6 +8,7 @@ "logpath": "/tmp/standalone/mongod.log", "journal": true, "port": 2000, + "bind_ip_all": true, "setParameter": {"enableTestCommands": 1} } } diff --git a/scripts/ubuntu/mongo-orchestration-config.json b/scripts/ubuntu/mongo-orchestration-config.json index 5aea8a8f6..ac9185b0c 100644 --- a/scripts/ubuntu/mongo-orchestration-config.json +++ b/scripts/ubuntu/mongo-orchestration-config.json @@ -1,5 +1,6 @@ { "releases": { + "40-release": "/home/vagrant/4.0/usr/bin", "36-release": "/home/vagrant/3.6/usr/bin", "30-release": "/home/vagrant/3.0/usr/bin" } diff --git a/scripts/ubuntu/mongo-orchestration.sh b/scripts/ubuntu/mongo-orchestration.sh index 8077a737d..5a036f117 100644 --- a/scripts/ubuntu/mongo-orchestration.sh +++ b/scripts/ubuntu/mongo-orchestration.sh @@ -4,8 +4,12 @@ apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 7F0CEB10 # 3.6 apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 2930ADAE8CAF5059EE73BB4B58712A2291FA4AD5 +# 4.0 +apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 68818C72E52529D4 + echo 'deb http://repo.mongodb.com/apt/ubuntu trusty/mongodb-enterprise/3.0 multiverse' | tee /etc/apt/sources.list.d/mongodb-enterprise-3.0.list echo 'deb http://repo.mongodb.com/apt/ubuntu trusty/mongodb-enterprise/3.6 multiverse' | tee /etc/apt/sources.list.d/mongodb-enterprise-3.6.list +echo 'deb http://repo.mongodb.com/apt/ubuntu trusty/mongodb-enterprise/testing multiverse' | tee /etc/apt/sources.list.d/mongodb-enterprise-4.0.list apt-get update @@ -14,9 +18,11 @@ apt-get install -y libsnmp30 libgsasl7 libcurl4-openssl-dev apt-get download mongodb-enterprise-server=3.0.15 apt-get download mongodb-enterprise-server=3.6.1 apt-get download mongodb-enterprise-mongos=3.6.1 +apt-get download mongodb-enterprise-server=4.0.0~rc5 dpkg -x mongodb-enterprise-server_3.0.15_amd64.deb 3.0 dpkg -x mongodb-enterprise-server_3.6.1_amd64.deb 3.6 dpkg -x mongodb-enterprise-mongos_3.6.1_amd64.deb 3.6 +dpkg -x mongodb-enterprise-server_4.0.0~rc5_amd64.deb 4.0 # Python stuff for mongo-orchestration apt-get install -y python python-dev diff --git a/src/MongoDB/Manager.c b/src/MongoDB/Manager.c index a851bc1dd..08e2be463 100644 --- a/src/MongoDB/Manager.c +++ b/src/MongoDB/Manager.c @@ -26,6 +26,7 @@ #include "php_array_api.h" #include "phongo_compat.h" #include "php_phongo.h" +#include "Session.h" #define PHONGO_MANAGER_URI_DEFAULT "mongodb://127.0.0.1/" @@ -655,11 +656,12 @@ static PHP_METHOD(Manager, selectServer) Returns a new client session */ static PHP_METHOD(Manager, startSession) { - php_phongo_manager_t* intern; - zval* options = NULL; - mongoc_session_opt_t* cs_opts = NULL; - mongoc_client_session_t* cs; - bson_error_t error = { 0 }; + php_phongo_manager_t* intern; + zval* options = NULL; + mongoc_session_opt_t* cs_opts = NULL; + mongoc_client_session_t* cs; + bson_error_t error = { 0 }; + mongoc_transaction_opt_t* txn_opts = NULL; SUPPRESS_UNUSED_WARNING(return_value_ptr) SUPPRESS_UNUSED_WARNING(return_value_used) @@ -669,22 +671,56 @@ static PHP_METHOD(Manager, startSession) return; } - if (options && php_array_exists(options, "causalConsistency")) { + if (options && php_array_existsc(options, "causalConsistency")) { cs_opts = mongoc_session_opts_new(); mongoc_session_opts_set_causal_consistency(cs_opts, php_array_fetchc_bool(options, "causalConsistency")); } - cs = mongoc_client_start_session(intern->client, cs_opts, &error); + if (options && php_array_existsc(options, "defaultTransactionOptions")) { + zval* txn_options = php_array_fetchc(options, "defaultTransactionOptions"); - if (cs_opts) { - mongoc_session_opts_destroy(cs_opts); + /* Thrown exception and return if the defaultTransactionOptions is not an array */ + if (Z_TYPE_P(txn_options) != IS_ARRAY) { + phongo_throw_exception( + PHONGO_ERROR_INVALID_ARGUMENT TSRMLS_CC, + "Expected \"defaultTransactionOptions\" option to be an array, %s given", + PHONGO_ZVAL_CLASS_OR_TYPE_NAME_P(txn_options) + ); + goto cleanup; + } + + /* Parse transaction options */ + txn_opts = php_mongodb_session_parse_transaction_options(txn_options TSRMLS_CC); + + /* If an exception is thrown while parsing, the txn_opts struct is also + * NULL, so no need to free it here */ + if (EG(exception)) { + goto cleanup; + } + + /* If the options are non-empty, add them to the client session opts struct */ + if (txn_opts) { + if (!cs_opts) { + cs_opts = mongoc_session_opts_new(); + } + + mongoc_session_opts_set_default_transaction_opts(cs_opts, txn_opts); + mongoc_transaction_opts_destroy(txn_opts); + } } + cs = mongoc_client_start_session(intern->client, cs_opts, &error); + if (cs) { phongo_session_init(return_value, cs TSRMLS_CC); } else { phongo_throw_exception_from_bson_error_t(&error TSRMLS_CC); } + +cleanup: + if (cs_opts) { + mongoc_session_opts_destroy(cs_opts); + } } /* }}} */ /* {{{ MongoDB\Driver\Manager function entries */ diff --git a/src/MongoDB/Session.c b/src/MongoDB/Session.c index 95f972e97..42cccfd51 100644 --- a/src/MongoDB/Session.c +++ b/src/MongoDB/Session.c @@ -24,6 +24,8 @@ #include "phongo_compat.h" #include "php_phongo.h" #include "php_bson.h" +#include "php_array_api.h" +#include "Session.h" zend_class_entry* php_phongo_session_ce; @@ -228,6 +230,166 @@ static PHP_METHOD(Session, getOperationTime) php_phongo_new_timestamp_from_increment_and_timestamp(return_value, increment, timestamp TSRMLS_CC); } /* }}} */ +/* Creates a opts structure from an array optionally containing an RP, RC, + * and/or WC object. Returns NULL if no options were found, or there was an + * invalid option. If there was an invalid option or structure, an exception + * will be thrown too. */ +mongoc_transaction_opt_t* php_mongodb_session_parse_transaction_options(zval* options TSRMLS_DC) +{ + mongoc_transaction_opt_t* opts = NULL; + + if (php_array_existsc(options, "readConcern")) { + zval* read_concern = php_array_fetchc(options, "readConcern"); + + if (Z_TYPE_P(read_concern) != IS_OBJECT || !instanceof_function(Z_OBJCE_P(read_concern), php_phongo_readconcern_ce TSRMLS_CC)) { + phongo_throw_exception(PHONGO_ERROR_INVALID_ARGUMENT TSRMLS_CC, "Expected \"readConcern\" option to be %s, %s given", ZSTR_VAL(php_phongo_readconcern_ce->name), PHONGO_ZVAL_CLASS_OR_TYPE_NAME_P(read_concern)); + /* Freeing opts is not needed here, as it can't be set yet. The + * code is here to keep it consistent with the others in case more + * options are added before this one. */ + if (opts) { + mongoc_transaction_opts_destroy(opts); + } + return NULL; + } + + if (!opts) { + opts = mongoc_transaction_opts_new(); + } + + mongoc_transaction_opts_set_read_concern(opts, phongo_read_concern_from_zval(read_concern TSRMLS_CC)); + } + + if (php_array_existsc(options, "readPreference")) { + zval* read_preference = php_array_fetchc(options, "readPreference"); + + if (Z_TYPE_P(read_preference) != IS_OBJECT || !instanceof_function(Z_OBJCE_P(read_preference), php_phongo_readpreference_ce TSRMLS_CC)) { + phongo_throw_exception(PHONGO_ERROR_INVALID_ARGUMENT TSRMLS_CC, "Expected \"readPreference\" option to be %s, %s given", ZSTR_VAL(php_phongo_readpreference_ce->name), PHONGO_ZVAL_CLASS_OR_TYPE_NAME_P(read_preference)); + if (opts) { + mongoc_transaction_opts_destroy(opts); + } + return NULL; + } + + if (!opts) { + opts = mongoc_transaction_opts_new(); + } + + mongoc_transaction_opts_set_read_prefs(opts, phongo_read_preference_from_zval(read_preference TSRMLS_CC)); + } + + if (php_array_existsc(options, "writeConcern")) { + zval* write_concern = php_array_fetchc(options, "writeConcern"); + + if (Z_TYPE_P(write_concern) != IS_OBJECT || !instanceof_function(Z_OBJCE_P(write_concern), php_phongo_writeconcern_ce TSRMLS_CC)) { + phongo_throw_exception(PHONGO_ERROR_INVALID_ARGUMENT TSRMLS_CC, "Expected \"writeConcern\" option to be %s, %s given", ZSTR_VAL(php_phongo_writeconcern_ce->name), PHONGO_ZVAL_CLASS_OR_TYPE_NAME_P(write_concern)); + if (opts) { + mongoc_transaction_opts_destroy(opts); + } + return NULL; + } + + if (!opts) { + opts = mongoc_transaction_opts_new(); + } + + mongoc_transaction_opts_set_write_concern(opts, phongo_write_concern_from_zval(write_concern TSRMLS_CC)); + } + + return opts; +} + +/* {{{ proto void MongoDB\Driver\Session::startTransaction([array $options = null]) + Starts a new transaction */ +static PHP_METHOD(Session, startTransaction) +{ + php_phongo_session_t* intern; + zval* options = NULL; + mongoc_transaction_opt_t* txn_options = NULL; + bson_error_t error; + SUPPRESS_UNUSED_WARNING(return_value_ptr) + SUPPRESS_UNUSED_WARNING(return_value_used) + + intern = Z_SESSION_OBJ_P(getThis()); + + if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "|a", &options) == FAILURE) { + return; + } + + if (options) { + txn_options = php_mongodb_session_parse_transaction_options(options TSRMLS_CC); + } + if (EG(exception)) { + return; + } + + if (!mongoc_client_session_start_transaction(intern->client_session, txn_options, &error)) { + phongo_throw_exception_from_bson_error_t(&error TSRMLS_CC); + } + + if (txn_options) { + mongoc_transaction_opts_destroy(txn_options); + } +} /* }}} */ + +/* {{{ proto void MongoDB\Driver\Session::commitTransaction(void) + Commits an existing transaction */ +static PHP_METHOD(Session, commitTransaction) +{ + php_phongo_session_t* intern; + bson_error_t error; + bson_t reply; + SUPPRESS_UNUSED_WARNING(return_value_ptr) + SUPPRESS_UNUSED_WARNING(return_value_used) + + intern = Z_SESSION_OBJ_P(getThis()); + + if (zend_parse_parameters_none() == FAILURE) { + return; + } + + if (!mongoc_client_session_commit_transaction(intern->client_session, &reply, &error)) { + phongo_throw_exception_from_bson_error_and_reply_t(&error, &reply TSRMLS_CC); + bson_destroy(&reply); + } +} /* }}} */ + +/* {{{ proto void MongoDB\Driver\Session::abortTransaction(void) + Aborts (rolls back) an existing transaction */ +static PHP_METHOD(Session, abortTransaction) +{ + php_phongo_session_t* intern; + bson_error_t error; + SUPPRESS_UNUSED_WARNING(return_value_ptr) + SUPPRESS_UNUSED_WARNING(return_value_used) + + intern = Z_SESSION_OBJ_P(getThis()); + + if (zend_parse_parameters_none() == FAILURE) { + return; + } + + if (!mongoc_client_session_abort_transaction(intern->client_session, &error)) { + phongo_throw_exception_from_bson_error_t(&error TSRMLS_CC); + } +} /* }}} */ + +/* {{{ proto void MongoDB\Driver\Session::endSession(void) + Ends the session, and a running transaction if active */ +static PHP_METHOD(Session, endSession) +{ + php_phongo_session_t* intern; + SUPPRESS_UNUSED_WARNING(return_value_ptr) + SUPPRESS_UNUSED_WARNING(return_value_used) + + intern = Z_SESSION_OBJ_P(getThis()); + + if (zend_parse_parameters_none() == FAILURE) { + return; + } + + mongoc_client_session_destroy(intern->client_session); +} /* }}} */ + /* {{{ MongoDB\Driver\Session function entries */ ZEND_BEGIN_ARG_INFO_EX(ai_Session_advanceClusterTime, 0, 0, 1) ZEND_ARG_INFO(0, clusterTime) @@ -237,6 +399,10 @@ ZEND_BEGIN_ARG_INFO_EX(ai_Session_advanceOperationTime, 0, 0, 1) ZEND_ARG_INFO(0, timestamp) ZEND_END_ARG_INFO() +ZEND_BEGIN_ARG_INFO_EX(ai_Session_startTransaction, 0, 0, 0) + ZEND_ARG_INFO(0, options) +ZEND_END_ARG_INFO() + ZEND_BEGIN_ARG_INFO_EX(ai_Session_void, 0, 0, 0) ZEND_END_ARG_INFO() @@ -247,6 +413,10 @@ static zend_function_entry php_phongo_session_me[] = { PHP_ME(Session, getClusterTime, ai_Session_void, ZEND_ACC_PUBLIC | ZEND_ACC_FINAL) PHP_ME(Session, getLogicalSessionId, ai_Session_void, ZEND_ACC_PUBLIC | ZEND_ACC_FINAL) PHP_ME(Session, getOperationTime, ai_Session_void, ZEND_ACC_PUBLIC | ZEND_ACC_FINAL) + PHP_ME(Session, startTransaction, ai_Session_startTransaction, ZEND_ACC_PUBLIC | ZEND_ACC_FINAL) + PHP_ME(Session, commitTransaction, ai_Session_void, ZEND_ACC_PUBLIC | ZEND_ACC_FINAL) + PHP_ME(Session, abortTransaction, ai_Session_void, ZEND_ACC_PUBLIC | ZEND_ACC_FINAL) + PHP_ME(Session, endSession, ai_Session_void, ZEND_ACC_PUBLIC | ZEND_ACC_FINAL) ZEND_NAMED_ME(__construct, PHP_FN(MongoDB_disabled___construct), ai_Session_void, ZEND_ACC_PRIVATE | ZEND_ACC_FINAL) ZEND_NAMED_ME(__wakeup, PHP_FN(MongoDB_disabled___wakeup), ai_Session_void, ZEND_ACC_PUBLIC | ZEND_ACC_FINAL) PHP_FE_END diff --git a/src/MongoDB/Session.h b/src/MongoDB/Session.h new file mode 100644 index 000000000..601051953 --- /dev/null +++ b/src/MongoDB/Session.h @@ -0,0 +1,22 @@ +/* + * Copyright 2017 MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef PHP_MONGODB_DRIVER_SESSION_H +#define PHP_MONGODB_DRIVER_SESSION_H + +mongoc_transaction_opt_t* php_mongodb_session_parse_transaction_options(zval* txnOptions TSRMLS_DC); + +#endif /* PHP_MONGODB_DRIVER_SESSION_H */ diff --git a/tests/session/session-startTransaction-001.phpt b/tests/session/session-startTransaction-001.phpt new file mode 100644 index 000000000..f63057ba7 --- /dev/null +++ b/tests/session/session-startTransaction-001.phpt @@ -0,0 +1,24 @@ +--TEST-- +MongoDB\Driver\Session::startTransaction() ensure that methods can be called +--SKIPIF-- + + + +--FILE-- +startSession(); + +$session->startTransaction(); +$session->abortTransaction(); + +$session->startTransaction(); +$session->commitTransaction(); + +?> +===DONE=== + +--EXPECTF-- +===DONE=== diff --git a/tests/session/session-startTransaction_error-001.phpt b/tests/session/session-startTransaction_error-001.phpt new file mode 100644 index 000000000..048a22228 --- /dev/null +++ b/tests/session/session-startTransaction_error-001.phpt @@ -0,0 +1,25 @@ +--TEST-- +MongoDB\Driver\Session::startTransaction() twice +--SKIPIF-- + + +--FILE-- +startSession(); + +$session->startTransaction(); + +echo throws(function() use ($session) { + $session->startTransaction(); +}, 'MongoDB\Driver\Exception\RuntimeException'), "\n"; + +?> +===DONE=== + +--EXPECTF-- +OK: Got MongoDB\Driver\Exception\RuntimeException +Transaction already in progress +===DONE=== diff --git a/tests/session/session-startTransaction_error-002.phpt b/tests/session/session-startTransaction_error-002.phpt new file mode 100644 index 000000000..d3da50b13 --- /dev/null +++ b/tests/session/session-startTransaction_error-002.phpt @@ -0,0 +1,72 @@ +--TEST-- +MongoDB\Driver\Session::startTransaction() with wrong values in options array +--SKIPIF-- + + +--FILE-- +startSession(); + +$options = [ + [ 'readConcern' => 42 ], + [ 'readConcern' => new stdClass ], + [ 'readConcern' => new \MongoDB\Driver\WriteConcern( 2 ) ], + [ 'readPreference' => 42 ], + [ 'readPreference' => new stdClass ], + [ 'readPreference' => new \MongoDB\Driver\ReadConcern( \MongoDB\Driver\ReadConcern::LOCAL ) ], + [ 'writeConcern' => 42 ], + [ 'writeConcern' => new stdClass ], + [ 'writeConcern' => new \MongoDB\Driver\ReadPreference( \MongoDB\Driver\ReadPreference::RP_SECONDARY ) ], + + [ + 'readConcern' => new \MongoDB\Driver\ReadConcern( \MongoDB\Driver\ReadConcern::LOCAL ), + 'readPreference' => new \MongoDB\Driver\ReadConcern( \MongoDB\Driver\ReadConcern::LOCAL ), + ], + [ + 'readConcern' => new \MongoDB\Driver\ReadConcern( \MongoDB\Driver\ReadConcern::LOCAL ), + 'writeConcern' => new \MongoDB\Driver\ReadPreference( \MongoDB\Driver\ReadPreference::RP_SECONDARY ), + ], + [ + 'readPreference' => new \MongoDB\Driver\ReadPreference( \MongoDB\Driver\ReadPreference::RP_SECONDARY ), + 'writeConcern' => new \MongoDB\Driver\ReadPreference( \MongoDB\Driver\ReadPreference::RP_SECONDARY ), + ], +]; + +foreach ($options as $txnOptions) { + echo throws(function() use ($session, $txnOptions) { + $session->startTransaction($txnOptions); + }, 'MongoDB\Driver\Exception\InvalidArgumentException'), "\n"; +} + +?> +===DONE=== + +--EXPECTF-- +OK: Got MongoDB\Driver\Exception\InvalidArgumentException +Expected "readConcern" option to be MongoDB\Driver\ReadConcern, integer given +OK: Got MongoDB\Driver\Exception\InvalidArgumentException +Expected "readConcern" option to be MongoDB\Driver\ReadConcern, stdClass given +OK: Got MongoDB\Driver\Exception\InvalidArgumentException +Expected "readConcern" option to be MongoDB\Driver\ReadConcern, MongoDB\Driver\WriteConcern given +OK: Got MongoDB\Driver\Exception\InvalidArgumentException +Expected "readPreference" option to be MongoDB\Driver\ReadPreference, integer given +OK: Got MongoDB\Driver\Exception\InvalidArgumentException +Expected "readPreference" option to be MongoDB\Driver\ReadPreference, stdClass given +OK: Got MongoDB\Driver\Exception\InvalidArgumentException +Expected "readPreference" option to be MongoDB\Driver\ReadPreference, MongoDB\Driver\ReadConcern given +OK: Got MongoDB\Driver\Exception\InvalidArgumentException +Expected "writeConcern" option to be MongoDB\Driver\WriteConcern, integer given +OK: Got MongoDB\Driver\Exception\InvalidArgumentException +Expected "writeConcern" option to be MongoDB\Driver\WriteConcern, stdClass given +OK: Got MongoDB\Driver\Exception\InvalidArgumentException +Expected "writeConcern" option to be MongoDB\Driver\WriteConcern, MongoDB\Driver\ReadPreference given +OK: Got MongoDB\Driver\Exception\InvalidArgumentException +Expected "readPreference" option to be MongoDB\Driver\ReadPreference, MongoDB\Driver\ReadConcern given +OK: Got MongoDB\Driver\Exception\InvalidArgumentException +Expected "writeConcern" option to be MongoDB\Driver\WriteConcern, MongoDB\Driver\ReadPreference given +OK: Got MongoDB\Driver\Exception\InvalidArgumentException +Expected "writeConcern" option to be MongoDB\Driver\WriteConcern, MongoDB\Driver\ReadPreference given +===DONE=== diff --git a/tests/session/session-startTransaction_error-003.phpt b/tests/session/session-startTransaction_error-003.phpt new file mode 100644 index 000000000..523ad4dbd --- /dev/null +++ b/tests/session/session-startTransaction_error-003.phpt @@ -0,0 +1,29 @@ +--TEST-- +MongoDB\Driver\Session::startTransaction() with wrong argument for options array +--SKIPIF-- + + +--FILE-- +startSession(); + +$options = [ + 2, + new stdClass, +]; + +foreach ($options as $txnOptions) { + $session->startTransaction($txnOptions); +} + +?> +===DONE=== + +--EXPECTF-- +Warning: MongoDB\Driver\Session::startTransaction() expects parameter 1 to be array, integer given in %s on line %d + +Warning: MongoDB\Driver\Session::startTransaction() expects parameter 1 to be array, object given in %s on line %d +===DONE=== diff --git a/tests/session/session_error-001.phpt b/tests/session/session_error-001.phpt new file mode 100644 index 000000000..3f8a3191c --- /dev/null +++ b/tests/session/session_error-001.phpt @@ -0,0 +1,79 @@ +--TEST-- +MongoDB\Driver\Session with wrong defaultTransactionOptions +--SKIPIF-- + +--FILE-- + 42 ], + [ 'readConcern' => new stdClass ], + [ 'readConcern' => new \MongoDB\Driver\WriteConcern( 2 ) ], + [ 'readPreference' => 42 ], + [ 'readPreference' => new stdClass ], + [ 'readPreference' => new \MongoDB\Driver\ReadConcern( \MongoDB\Driver\ReadConcern::LOCAL ) ], + [ 'writeConcern' => 42 ], + [ 'writeConcern' => new stdClass ], + [ 'writeConcern' => new \MongoDB\Driver\ReadPreference( \MongoDB\Driver\ReadPreference::RP_SECONDARY ) ], + + [ + 'readConcern' => new \MongoDB\Driver\ReadConcern( \MongoDB\Driver\ReadConcern::LOCAL ), + 'readPreference' => new \MongoDB\Driver\ReadConcern( \MongoDB\Driver\ReadConcern::LOCAL ), + ], + [ + 'readConcern' => new \MongoDB\Driver\ReadConcern( \MongoDB\Driver\ReadConcern::LOCAL ), + 'writeConcern' => new \MongoDB\Driver\ReadPreference( \MongoDB\Driver\ReadPreference::RP_SECONDARY ), + ], + [ + 'readPreference' => new \MongoDB\Driver\ReadPreference( \MongoDB\Driver\ReadPreference::RP_SECONDARY ), + 'writeConcern' => new \MongoDB\Driver\ReadPreference( \MongoDB\Driver\ReadPreference::RP_SECONDARY ), + ], + + 42, + new stdClass, +]; + +foreach ($options as $txnOptions) { + echo throws(function() use ($manager, $txnOptions) { + $session = $manager->startSession([ + 'defaultTransactionOptions' => $txnOptions + ]); + }, 'MongoDB\Driver\Exception\InvalidArgumentException'), "\n"; +} + +?> +===DONE=== + +--EXPECTF-- +OK: Got MongoDB\Driver\Exception\InvalidArgumentException +Expected "readConcern" option to be MongoDB\Driver\ReadConcern, integer given +OK: Got MongoDB\Driver\Exception\InvalidArgumentException +Expected "readConcern" option to be MongoDB\Driver\ReadConcern, stdClass given +OK: Got MongoDB\Driver\Exception\InvalidArgumentException +Expected "readConcern" option to be MongoDB\Driver\ReadConcern, MongoDB\Driver\WriteConcern given +OK: Got MongoDB\Driver\Exception\InvalidArgumentException +Expected "readPreference" option to be MongoDB\Driver\ReadPreference, integer given +OK: Got MongoDB\Driver\Exception\InvalidArgumentException +Expected "readPreference" option to be MongoDB\Driver\ReadPreference, stdClass given +OK: Got MongoDB\Driver\Exception\InvalidArgumentException +Expected "readPreference" option to be MongoDB\Driver\ReadPreference, MongoDB\Driver\ReadConcern given +OK: Got MongoDB\Driver\Exception\InvalidArgumentException +Expected "writeConcern" option to be MongoDB\Driver\WriteConcern, integer given +OK: Got MongoDB\Driver\Exception\InvalidArgumentException +Expected "writeConcern" option to be MongoDB\Driver\WriteConcern, stdClass given +OK: Got MongoDB\Driver\Exception\InvalidArgumentException +Expected "writeConcern" option to be MongoDB\Driver\WriteConcern, MongoDB\Driver\ReadPreference given +OK: Got MongoDB\Driver\Exception\InvalidArgumentException +Expected "readPreference" option to be MongoDB\Driver\ReadPreference, MongoDB\Driver\ReadConcern given +OK: Got MongoDB\Driver\Exception\InvalidArgumentException +Expected "writeConcern" option to be MongoDB\Driver\WriteConcern, MongoDB\Driver\ReadPreference given +OK: Got MongoDB\Driver\Exception\InvalidArgumentException +Expected "writeConcern" option to be MongoDB\Driver\WriteConcern, MongoDB\Driver\ReadPreference given +OK: Got MongoDB\Driver\Exception\InvalidArgumentException +Expected "defaultTransactionOptions" option to be an array, integer given +OK: Got MongoDB\Driver\Exception\InvalidArgumentException +Expected "defaultTransactionOptions" option to be an array, stdClass given +===DONE=== diff --git a/tests/session/transaction-integration-001.phpt b/tests/session/transaction-integration-001.phpt new file mode 100644 index 000000000..44b4c6ad4 --- /dev/null +++ b/tests/session/transaction-integration-001.phpt @@ -0,0 +1,81 @@ +--TEST-- +MongoDB\Driver\Session::startTransaction() Committing a transaction with example for how to handle failures +--SKIPIF-- + + + + + +--FILE-- + $EMPLOYEES_COL +]); +$manager->executeCommand(DATABASE_NAME, $cmd); + +$cmd = new \MongoDB\Driver\Command([ + 'create' => $EVENTS_COL +]); +$manager->executeCommand(DATABASE_NAME, $cmd); + + +/* Do the transaction */ +$session = $manager->startSession(); + +$session->startTransaction( [ + 'readConcern' => new \MongoDB\Driver\ReadConcern( "snapshot" ), + 'writeConcern' => new \MongoDB\Driver\WriteConcern( \MongoDB\Driver\WriteConcern::MAJORITY ) +] ); + +while (true) { + try { + $cmd = new \MongoDB\Driver\Command( [ + 'update' => $EMPLOYEES_COL, + 'updates' => [ + [ + 'q' => [ 'employee' => 3 ], + 'u' => [ '$set' => [ 'status' => 'Inactive' ] ], + ] + ] + ] ); + $manager->executeCommand(DATABASE_NAME, $cmd, ['session' => $session]); + + $cmd = new \MongoDB\Driver\Command( [ + 'insert' => $EVENTS_COL, + 'documents' => [ + [ 'employee' => 3, 'status' => [ 'new' => 'Inactive', 'old' => 'Active' ] ] + ] + ] ); + $manager->executeCommand(DATABASE_NAME, $cmd, ['session' => $session]); + + $session->commitTransaction(); + echo "Transaction committed.\n";break; + } catch (\MongoDB\Driver\Exception\Exception $e) { + $rd = $e->getResultDocument(); + + if (isset($rd->errorLabels) && in_array('TransientTransactionError', $rd->errorLabels)) { + echo "Temporary error: ", $e->getMessage(), ", retrying...\n"; + $rd = $e->getResultDocument(); + var_dump($rd); + continue; + } else { + var_dump($e); + } + break; + } +} + +?> +===DONE=== + +--EXPECTF-- +Transaction committed. +===DONE=== diff --git a/tests/session/transaction-integration-002.phpt b/tests/session/transaction-integration-002.phpt new file mode 100644 index 000000000..3ea2073e1 --- /dev/null +++ b/tests/session/transaction-integration-002.phpt @@ -0,0 +1,74 @@ +--TEST-- +MongoDB\Driver\Session::startTransaction() Transient Error Test +--SKIPIF-- + + + + +--FILE-- + COLLECTION_NAME, +]); +$manager->executeCommand(DATABASE_NAME, $cmd); + +/* Insert Data */ +$bw = new \MongoDB\Driver\BulkWrite(); +$bw->insert( [ '_id' => 0, 'msg' => 'Initial Value' ] ); +$manager->executeBulkWrite(NS, $bw); + +/* First 'thread', try to update document, but don't close transaction */ +$sessionA = $manager->startSession(); +$sessionA->startTransaction( [ + 'readConcern' => new \MongoDB\Driver\ReadConcern( "snapshot" ), + 'writeConcern' => new \MongoDB\Driver\WriteConcern( \MongoDB\Driver\WriteConcern::MAJORITY ) +] ); + +$cmd = new \MongoDB\Driver\Command( [ + 'update' => COLLECTION_NAME, + 'updates' => [ + [ + 'q' => [ '_id' => 0 ], + 'u' => [ '$set' => [ 'msg' => 'Update from session A' ] ], + ] + ] +] ); +$manager->executeCommand(DATABASE_NAME, $cmd, ['session' => $sessionA]); + + +/* Second 'thread', try to update the same document, should trigger exception. In handler, commit + * first settion, verify result, and redo this transaction. */ +$sessionB = $manager->startSession(); +$sessionB->startTransaction( [ + 'readConcern' => new \MongoDB\Driver\ReadConcern( "snapshot" ), + 'writeConcern' => new \MongoDB\Driver\WriteConcern( \MongoDB\Driver\WriteConcern::MAJORITY ) +] ); + +try { + $cmd = new \MongoDB\Driver\Command( [ + 'update' => COLLECTION_NAME, + 'updates' => [ + [ + 'q' => [ '_id' => 0 ], + 'u' => [ '$set' => [ 'msg' => 'Update from session B' ] ], + ] + ] + ] ); + $manager->executeCommand(DATABASE_NAME, $cmd, ['session' => $sessionB]); +} catch (MongoDB\Driver\Exception\CommandException $e) { + $rd = $e->getResultDocument(); + + echo (isset($rd->errorLabels) && in_array("TransientTransactionError", $rd->errorLabels)) ? + "found a TransientTransactionError" : "did NOT get a TransientTransactionError", "\n"; +} +?> +===DONE=== + +--EXPECTF-- +found a TransientTransactionError +===DONE=== diff --git a/tests/utils/basic-skipif.inc b/tests/utils/basic-skipif.inc index df23d43ca..1005bcf4a 100644 --- a/tests/utils/basic-skipif.inc +++ b/tests/utils/basic-skipif.inc @@ -12,7 +12,7 @@ set_error_handler(function($errno, $errstr) { }); set_exception_handler(function($e) { - exit(sprintf('skip %s(%d): %s', get_class($e), $e->getCode(), $e->getMessage())); + exit(sprintf('skip %s(%d): %s @ %s:%d', get_class($e), $e->getCode(), $e->getMessage(), $e->getFile(), $e->getLine())); }); register_shutdown_function(function() { diff --git a/tests/utils/tools.php b/tests/utils/tools.php index df28eb67f..9f8e2a1c8 100644 --- a/tests/utils/tools.php +++ b/tests/utils/tools.php @@ -45,7 +45,16 @@ function drop_collection($uri, $databaseName, $collectionName) $command = new Command(['drop' => $collectionName]); try { - $server->executeCommand($databaseName, $command); + /* We need to use WriteConcern::MAJORITY here due to the issue + * explained in SERVER-35613: "drop" uses a two phase commit, and due + * to that, it is possible that a lock can't be acquired for a + * transaction that gets quickly started as the "drop" reaper hasn't + * completed yet. */ + $server->executeCommand( + $databaseName, + $command, + [ 'writeConcern' => new \MongoDB\Driver\WriteConcern( \MongoDB\Driver\WriteConcern::MAJORITY ) ] + ); } catch (RuntimeException $e) { if ($e->getMessage() !== 'ns not found') { throw $e;