Skip to content

Commit 65ddeec

Browse files
authored
Merge pull request #13 from xp-forge/feature/transactions
Implement multi-document transactions
2 parents 9c1ae62 + bc31e46 commit 65ddeec

File tree

6 files changed

+394
-141
lines changed

6 files changed

+394
-141
lines changed

src/main/php/com/mongodb/Session.class.php

Lines changed: 128 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,21 @@
11
<?php namespace com\mongodb;
22

33
use com\mongodb\io\Protocol;
4-
use lang\{Closeable, IllegalStateException};
5-
use util\UUID;
4+
use lang\{Closeable, IllegalStateException, Value, Throwable};
5+
use util\{UUID, Objects};
66

77
/**
88
* A client session
99
*
1010
* @see https://docs.mongodb.com/manual/reference/server-sessions/
1111
* @see https://docs.mongodb.com/manual/core/read-isolation-consistency-recency/#std-label-sessions
1212
* @see https://github.com/mongodb/specifications/blob/master/source/sessions/driver-sessions.rst
13+
* @test com.mongodb.unittest.SessionsTest
1314
*/
14-
class Session implements Closeable {
15+
class Session implements Value, Closeable {
1516
private $proto, $id;
1617
private $closed= false;
18+
private $transaction= ['n' => 0];
1719

1820
/**
1921
* Creates a new session
@@ -33,31 +35,147 @@ public function id(): UUID { return $this->id; }
3335
public function closed(): bool { return $this->closed; }
3436

3537
/**
36-
* Returns fields to be sent along with the command
38+
* Starts a multi-document transaction associated with the session. At any
39+
* given time, you can have at most one open transaction for a session.
40+
*
41+
* @see https://github.com/mongodb/specifications/blob/master/source/transactions/transactions.rst#transactionoptions
42+
* @param ?string $options
43+
* @return self
44+
* @throws lang.IllegalStateException if a transaction has already been started
45+
*/
46+
public function transaction($options= null): self {
47+
if (isset($this->transaction['context'])) {
48+
throw new IllegalStateException('Cannot start more than one transaction on a session');
49+
}
50+
51+
null === $options ? $params= [] : parse_str($options, $params);
52+
$this->transaction['context']= [
53+
'txnNumber' => new Int64(++$this->transaction['n']),
54+
'autocommit' => false,
55+
'startTransaction' => true,
56+
];
57+
58+
// Overwrite readPreference, defaults to inheriting from client
59+
isset($params['readPreference']) && $this->transaction['context']['$readPreference']= ['mode' => $params['readPreference']];
60+
61+
// Support timeoutMS and the deprecated maxCommitTimeMS
62+
$timeout= $params['timeoutMS'] ?? $params['maxCommitTimeMS'] ?? null;
63+
$this->transaction['t']= null === $timeout ? [] : ['maxTimeMS' => new Int64($timeout)];
64+
65+
return $this;
66+
}
67+
68+
/**
69+
* Commits a transaction
70+
*
71+
* @return void
72+
* @throws lang.IllegalStateException if no transaction is active
73+
* @throws com.mongodb.Error
74+
*/
75+
public function commit() {
76+
if (!isset($this->transaction['context'])) {
77+
throw new IllegalStateException('No active transaction');
78+
}
79+
80+
try {
81+
if (!isset($this->transaction['context']['startTransaction'])) {
82+
$this->proto->write($this, ['commitTransaction' => 1, '$db' => 'admin'] + $this->transaction['t'] + $this->transaction['context']);
83+
}
84+
} finally {
85+
unset($this->transaction['context']);
86+
}
87+
}
88+
89+
/**
90+
* Aborts a transaction
91+
*
92+
* @return void
93+
* @throws lang.IllegalStateException if no transaction is active
94+
* @throws com.mongodb.Error
95+
*/
96+
public function abort() {
97+
if (!isset($this->transaction['context'])) {
98+
throw new IllegalStateException('No active transaction');
99+
}
100+
101+
try {
102+
if (!isset($this->transaction['context']['startTransaction'])) {
103+
$this->proto->write($this, ['abortTransaction' => 1, '$db' => 'admin'] + $this->transaction['context']);
104+
}
105+
} finally {
106+
unset($this->transaction['context']);
107+
}
108+
}
109+
110+
/**
111+
* Returns fields to be sent along with the command. Used by Protocol class.
37112
*
38113
* @param com.mongodb.io.Protocol
39114
* @return [:var]
40115
* @throws lang.IllegalStateException
41116
*/
42117
public function send($proto) {
43-
if ($proto === $this->proto) return ['lsid' => ['id' => $this->id]];
118+
if ($proto !== $this->proto) {
119+
throw new IllegalStateException('Session was created by a different client');
120+
}
44121

45-
throw new IllegalStateException('Session was created by a different client');
122+
// When constructing the first command within a transaction, drivers MUST
123+
// add the lsid, txnNumber, startTransaction, and autocommit fields. When
124+
// constructing any other command within a transaction, drivers MUST add
125+
// the lsid, txnNumber, and autocommit fields.
126+
$fields= ['lsid' => ['id' => $this->id]];
127+
if (isset($this->transaction['context'])) {
128+
$fields+= $this->transaction['context'];
129+
unset($this->transaction['context']['startTransaction']);
130+
}
131+
return $fields;
46132
}
47133

48134
/** @return void */
49135
public function close() {
50136
if ($this->closed) return;
51137

138+
// Should there be an active running transaction, abort it.
139+
if (isset($this->transaction['context'])) {
140+
try {
141+
$this->proto->write($this, ['abortTransaction' => 1, '$db' => 'admin'] + $this->transaction['context']);
142+
} catch (Throwable $ignored) {
143+
// NOOP
144+
}
145+
unset($this->transaction['context']);
146+
}
147+
52148
// Fire and forget: If the user has no session that match, the endSessions call has
53149
// no effect, see https://docs.mongodb.com/manual/reference/command/endSessions/
54-
$this->proto->read($this, [
55-
'endSessions' => [['id' => $this->id]],
56-
'$db' => 'admin'
57-
]);
150+
try {
151+
$this->proto->write($this, ['endSessions' => [['id' => $this->id]], '$db' => 'admin']);
152+
} catch (Throwable $ignored) {
153+
// NOOP
154+
}
58155
$this->closed= true;
59156
}
60157

158+
/** @return string */
159+
public function hashCode() {
160+
return 'S'.$this->id->hashCode();
161+
}
162+
163+
/** @return string */
164+
public function toString() {
165+
$context= isset($this->transaction['context']) ? ', transaction: '.Objects::stringOf($this->transaction['context']) : '';
166+
return nameof($this).'(id: '.$this->id->hashCode().$context.')';
167+
}
168+
169+
/**
170+
* Comparison
171+
*
172+
* @param var $value
173+
* @return int
174+
*/
175+
public function compareTo($value) {
176+
return $value instanceof self ? $this->id->compareTo($value->id) : 1;
177+
}
178+
61179
/** @return void */
62180
public function __destruct() {
63181
$this->close();

src/main/php/com/mongodb/io/Protocol.class.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
/**
88
* MongoDB Wire Protocol
99
*
10-
* @see https://docs.mongodb.com/manual/reference/mongodb-wire-protocol/
10+
* @see https://docs.mongodb.com/manual/reference/mongodb-wire-protocol/
11+
* @test com.mongodb.unittest.ReplicaSetTest
1112
*/
1213
class Protocol {
1314
private $options, $conn, $auth;

0 commit comments

Comments
 (0)