Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 135 additions & 0 deletions src/MongoDB/Session.c
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,61 @@

zend_class_entry *php_phongo_session_ce;

static bool php_phongo_session_get_timestamp_parts(zval *obj, uint32_t *timestamp, uint32_t *increment TSRMLS_DC)
{
bool retval = false;
#if PHP_VERSION_ID >= 70000
zval ztimestamp;
zval zincrement;

zend_call_method_with_0_params(obj, NULL, NULL, "getTimestamp", &ztimestamp);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm uncomfortable with calling methods through the PHP/Zend layer here. Is there no other way to obtain the timestamp?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can certainly add logic to check for a Timestamp object and access these properties directly without invoking methods, and will do so.

That said, there is no other solution if we're dealing with a non-Timestamp TimestampInterface.

In my testing, if the getTimestamp() throws, getIncrement() is never even called. This may be the result of logic inside zend_call_method that no-ops if a previous call failed or an exception has already been thrown. I still found that a bit odd, as here I am invoking zend_call_method twice without logic in-between. If you're more comfortable with it, I can also split up those calls and check for errors between them, instead of a combined check for undefined ztimestamp and zincrement down below.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As this goes through zend_execute, I expected that the throw would abort further calls. I think it's better to check for EG(exception) in between. But you're right on the non-Timestamp TimestampInterface. I now believe you shouldn't short-cut it as an exception for "Timestamp" too.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The IS_UNDEF check was inspired by what I saw in zend_interfaces.c:

I actually think the best approach is checking for (Z_TYPE(retval) == IS_UNDEF || EG(exception)), as is done in the last example (zend_user_serialize).

I personally don't see any harm in optimizing for the common use case (Timestamp instance), and simply forgot to make that change before I last updated this PR. I'll leave it out for now given your feedback, but we can always add it in later if you change your mind.


if (Z_ISUNDEF(ztimestamp) || EG(exception)) {
goto cleanup;
}

zend_call_method_with_0_params(obj, NULL, NULL, "getIncrement", &zincrement);

if (Z_ISUNDEF(zincrement) || EG(exception)) {
goto cleanup;
}

*timestamp = Z_LVAL(ztimestamp);
*increment = Z_LVAL(zincrement);
#else
zval *ztimestamp = NULL;
zval *zincrement = NULL;

zend_call_method_with_0_params(&obj, NULL, NULL, "getTimestamp", &ztimestamp);

if (Z_ISUNDEF(ztimestamp) || EG(exception)) {
goto cleanup;
}

zend_call_method_with_0_params(&obj, NULL, NULL, "getIncrement", &zincrement);

if (Z_ISUNDEF(zincrement) || EG(exception)) {
goto cleanup;
}

*timestamp = Z_LVAL_P(ztimestamp);
*increment = Z_LVAL_P(zincrement);
#endif

retval = true;

cleanup:
if (!Z_ISUNDEF(ztimestamp)) {
zval_ptr_dtor(&ztimestamp);
}

if (!Z_ISUNDEF(zincrement)) {
zval_ptr_dtor(&zincrement);
}

return retval;
}

/* {{{ proto void MongoDB\Driver\Session::advanceClusterTime(array|object $clusterTime)
Advances the cluster time for this Session */
static PHP_METHOD(Session, advanceClusterTime)
Expand Down Expand Up @@ -56,6 +111,30 @@ static PHP_METHOD(Session, advanceClusterTime)
bson_destroy(&cluster_time);
} /* }}} */

/* {{{ proto void MongoDB\Driver\Session::advanceOperationTime(MongoDB\BSON\Timestamp $timestamp)
Advances the operation time for this Session */
static PHP_METHOD(Session, advanceOperationTime)
{
php_phongo_session_t *intern;
zval *ztimestamp;
uint32_t timestamp = 0;
uint32_t increment = 0;
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, "O", &ztimestamp, php_phongo_timestamp_interface_ce) == FAILURE) {
return;
}

if (!php_phongo_session_get_timestamp_parts(ztimestamp, &timestamp, &increment TSRMLS_CC)) {
return;
}

mongoc_client_session_advance_operation_time(intern->client_session, timestamp, increment);
} /* }}} */

/* {{{ proto object|null MongoDB\Driver\Session::getClusterTime()
Returns the cluster time for this Session */
static PHP_METHOD(Session, getClusterTime)
Expand Down Expand Up @@ -123,18 +202,51 @@ static PHP_METHOD(Session, getLogicalSessionId)
#endif
} /* }}} */

/* {{{ proto MongoDB\BSON\Timestamp MongoDB\Driver\Session::getOperationTime()
Returns the operation time for this Session */
static PHP_METHOD(Session, getOperationTime)
{
php_phongo_session_t *intern;
uint32_t timestamp, increment;
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_get_operation_time(intern->client_session, &timestamp, &increment);

/* mongoc_client_session_get_operation_time() returns 0 for both parts if
* the session has not been used. According to the causal consistency spec,
* the operation time for an unused session is null. */
if (timestamp == 0 && increment == 0) {
RETURN_NULL();
}

php_phongo_new_timestamp_from_increment_and_timestamp(return_value, increment, timestamp TSRMLS_CC);
} /* }}} */

/* {{{ MongoDB\Driver\Session function entries */
ZEND_BEGIN_ARG_INFO_EX(ai_Session_advanceClusterTime, 0, 0, 1)
ZEND_ARG_INFO(0, clusterTime)
ZEND_END_ARG_INFO()

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_void, 0, 0, 0)
ZEND_END_ARG_INFO()

static zend_function_entry php_phongo_session_me[] = {
PHP_ME(Session, advanceClusterTime, ai_Session_advanceClusterTime, ZEND_ACC_PUBLIC|ZEND_ACC_FINAL)
PHP_ME(Session, advanceOperationTime, ai_Session_advanceOperationTime, ZEND_ACC_PUBLIC|ZEND_ACC_FINAL)
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)
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
Expand Down Expand Up @@ -243,6 +355,29 @@ static HashTable *php_phongo_session_get_debug_info(zval *object, int *is_temp T
cs_opts = mongoc_client_session_get_opts(intern->client_session);
ADD_ASSOC_BOOL_EX(&retval, "causalConsistency", mongoc_session_opts_get_causal_consistency(cs_opts));

{
uint32_t timestamp, increment;

mongoc_client_session_get_operation_time(intern->client_session, &timestamp, &increment);

if (timestamp && increment) {
#if PHP_VERSION_ID >= 70000
zval ztimestamp;

php_phongo_new_timestamp_from_increment_and_timestamp(&ztimestamp, increment, timestamp TSRMLS_CC);
ADD_ASSOC_ZVAL_EX(&retval, "operationTime", &ztimestamp);
#else
zval *ztimestamp;

MAKE_STD_ZVAL(ztimestamp);
php_phongo_new_timestamp_from_increment_and_timestamp(ztimestamp, increment, timestamp TSRMLS_CC);
ADD_ASSOC_ZVAL_EX(&retval, "operationTime", ztimestamp);
#endif
} else {
ADD_ASSOC_NULL_EX(&retval, "operationTime");
}
}

return Z_ARRVAL(retval);
} /* }}} */
/* }}} */
Expand Down
22 changes: 22 additions & 0 deletions tests/causal-consistency/causal-consistency-001.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
--TEST--
Causal consistency: new session has no operation time
--SKIPIF--
<?php require __DIR__ . "/../utils/basic-skipif.inc"; ?>
<?php NEEDS('REPLICASET'); ?>
--FILE--
<?php
require_once __DIR__ . "/../utils/basic.inc";

$manager = new MongoDB\Driver\Manager(REPLICASET);
$session = $manager->startSession();

echo "Initial operation time:\n";
var_dump($session->getOperationTime());

?>
===DONE===
<?php exit(0); ?>
--EXPECT--
Initial operation time:
NULL
===DONE===
31 changes: 31 additions & 0 deletions tests/causal-consistency/causal-consistency-002.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
--TEST--
Causal consistency: first read in session does not include afterClusterTime
--SKIPIF--
<?php require __DIR__ . "/../utils/basic-skipif.inc"; ?>
<?php NEEDS('REPLICASET'); ?>
--FILE--
<?php
require_once __DIR__ . "/../utils/basic.inc";
require_once __DIR__ . "/../utils/observer.php";

(new CommandObserver)->observe(
function() {
$manager = new MongoDB\Driver\Manager(REPLICASET);
$session = $manager->startSession();

$query = new MongoDB\Driver\Query([]);
$manager->executeQuery(NS, $query, ['session' => $session]);
},
function(stdClass $command)
{
$hasAfterClusterTime = isset($command->readConcern->afterClusterTime);
printf("Read includes afterClusterTime: %s\n", ($hasAfterClusterTime ? 'yes' : 'no'));
}
);

?>
===DONE===
<?php exit(0); ?>
--EXPECT--
Read includes afterClusterTime: no
===DONE===
111 changes: 111 additions & 0 deletions tests/causal-consistency/causal-consistency-003.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
--TEST--
Causal consistency: first read or write in session updates operationTime
--SKIPIF--
<?php require __DIR__ . "/../utils/basic-skipif.inc"; ?>
<?php NEEDS('REPLICASET'); CLEANUP(REPLICASET); ?>
--FILE--
<?php
require_once __DIR__ . "/../utils/basic.inc";

class Test implements MongoDB\Driver\Monitoring\CommandSubscriber
{
private $lastSeenOperationTime;

public function executeBulkWrite()
{
$this->lastSeenOperationTime = null;

MongoDB\Driver\Monitoring\addSubscriber($this);

$manager = new MongoDB\Driver\Manager(REPLICASET);
$session = $manager->startSession();

$bulk = new MongoDB\Driver\BulkWrite;
$bulk->insert(['x' => 1]);
$manager->executeBulkWrite(NS, $bulk, ['session' => $session]);

printf("Session reports last seen operationTime: %s\n", ($session->getOperationTime() == $this->lastSeenOperationTime) ? 'yes' : 'no');

MongoDB\Driver\Monitoring\removeSubscriber($this);
}

public function executeCommand()
{
$this->lastSeenOperationTime = null;

MongoDB\Driver\Monitoring\addSubscriber($this);

$manager = new MongoDB\Driver\Manager(REPLICASET);
$session = $manager->startSession();

$command = new MongoDB\Driver\Command(['ping' => 1]);
$manager->executeCommand(DATABASE_NAME, $command, ['session' => $session]);

printf("Session reports last seen operationTime: %s\n", ($session->getOperationTime() == $this->lastSeenOperationTime) ? 'yes' : 'no');

MongoDB\Driver\Monitoring\removeSubscriber($this);
}

public function executeQuery()
{
$this->lastSeenOperationTime = null;

MongoDB\Driver\Monitoring\addSubscriber($this);

$manager = new MongoDB\Driver\Manager(REPLICASET);
$session = $manager->startSession();

$query = new MongoDB\Driver\Query([]);
$manager->executeQuery(NS, $query, ['session' => $session]);

printf("Session reports last seen operationTime: %s\n", ($session->getOperationTime() == $this->lastSeenOperationTime) ? 'yes' : 'no');

MongoDB\Driver\Monitoring\removeSubscriber($this);
}

public function commandStarted(MongoDB\Driver\Monitoring\CommandStartedEvent $event)
{
}

public function commandSucceeded(MongoDB\Driver\Monitoring\CommandSucceededEvent $event)
{
$reply = $event->getReply();
$hasOperationTime = isset($reply->{'operationTime'});

printf("%s command reply includes operationTime: %s\n", $event->getCommandName(), $hasOperationTime ? 'yes' : 'no');

if ($hasOperationTime) {
$this->lastSeenOperationTime = $reply->operationTime;
}
}

public function commandFailed(MongoDB\Driver\Monitoring\CommandFailedEvent $event)
{
}
}

echo "Testing executeBulkWrite()\n";
(new Test)->executeBulkWrite();

echo "\nTesting executeCommand()\n";
(new Test)->executeCommand();

echo "\nTesting executeQuery()\n";
(new Test)->executeQuery();

?>
===DONE===
<?php exit(0); ?>
--EXPECT--
Testing executeBulkWrite()
insert command reply includes operationTime: yes
Session reports last seen operationTime: yes

Testing executeCommand()
ping command reply includes operationTime: yes
Session reports last seen operationTime: yes

Testing executeQuery()
find command reply includes operationTime: yes
Session reports last seen operationTime: yes
===DONE===
Loading