@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.)
hq.recaptime.dev/wiki/Phorge
phorge
phabricator
1<?php
2
3final class PhabricatorGlobalLockTestCase
4 extends PhabricatorTestCase {
5
6 protected function getPhabricatorTestCaseConfiguration() {
7 return array(
8 self::PHABRICATOR_TESTCONFIG_BUILD_STORAGE_FIXTURES => true,
9 );
10 }
11
12 public function testConnectionPoolWithDefaultConnection() {
13 PhabricatorGlobalLock::clearConnectionPool();
14
15 $this->assertEqual(
16 0,
17 PhabricatorGlobalLock::getConnectionPoolSize(),
18 pht('Clear Connection Pool'));
19
20 $lock_name = $this->newLockName();
21 $lock = PhabricatorGlobalLock::newLock($lock_name);
22 $lock->lock();
23
24 $this->assertEqual(
25 0,
26 PhabricatorGlobalLock::getConnectionPoolSize(),
27 pht('Connection Pool With Lock'));
28
29 $lock->unlock();
30
31 $this->assertEqual(
32 1,
33 PhabricatorGlobalLock::getConnectionPoolSize(),
34 pht('Connection Pool With Lock Released'));
35
36 PhabricatorGlobalLock::clearConnectionPool();
37 }
38
39 public function testConnectionPoolWithSpecificConnection() {
40 $conn = PhabricatorGlobalLock::newConnection();
41
42 PhabricatorGlobalLock::clearConnectionPool();
43
44 $this->assertEqual(
45 0,
46 PhabricatorGlobalLock::getConnectionPoolSize(),
47 pht('Clear Connection Pool'));
48
49 $this->assertEqual(
50 false,
51 $conn->isHoldingAnyLock(),
52 pht('Specific Connection, No Lock'));
53
54 $lock_name = $this->newLockName();
55 $lock = PhabricatorGlobalLock::newLock($lock_name);
56 $lock->setExternalConnection($conn);
57 $lock->lock();
58
59 $this->assertEqual(
60 0,
61 PhabricatorGlobalLock::getConnectionPoolSize(),
62 pht('Connection Pool + Specific, With Lock'));
63
64 $this->assertEqual(
65 true,
66 $conn->isHoldingAnyLock(),
67 pht('Specific Connection, Holding Lock'));
68
69 $lock->unlock();
70
71 // The specific connection provided should NOT be returned to the
72 // connection pool.
73
74 $this->assertEqual(
75 0,
76 PhabricatorGlobalLock::getConnectionPoolSize(),
77 pht('Connection Pool + Specific, With Lock Released'));
78
79 $this->assertEqual(
80 false,
81 $conn->isHoldingAnyLock(),
82 pht('Specific Connection, No Lock'));
83
84 PhabricatorGlobalLock::clearConnectionPool();
85 }
86
87 public function testExternalConnectionMutationScope() {
88 $conn = PhabricatorGlobalLock::newConnection();
89
90 $lock_name = $this->newLockName();
91 $lock = PhabricatorGlobalLock::newLock($lock_name);
92 $lock->lock();
93
94 $caught = null;
95 try {
96 $lock->setExternalConnection($conn);
97 } catch (Exception $ex) {
98 $caught = $ex;
99 } catch (Throwable $ex) {
100 $caught = $ex;
101 }
102
103 $lock->unlock();
104
105 $this->assertTrue(
106 ($caught instanceof Exception),
107 pht('Changing connection while locked is forbidden.'));
108 }
109
110 public function testMultipleLocks() {
111 $conn = PhabricatorGlobalLock::newConnection();
112
113 PhabricatorGlobalLock::clearConnectionPool();
114
115 $lock_name_a = $this->newLockName();
116 $lock_name_b = $this->newLockName();
117
118 $lock_a = PhabricatorGlobalLock::newLock($lock_name_a);
119 $lock_a->setExternalConnection($conn);
120
121 $lock_b = PhabricatorGlobalLock::newLock($lock_name_b);
122 $lock_b->setExternalConnection($conn);
123
124 $lock_a->lock();
125
126 $caught = null;
127 try {
128 $lock_b->lock();
129 } catch (Exception $ex) {
130 $caught = $ex;
131 } catch (Throwable $ex) {
132 $caught = $ex;
133 }
134
135 // See T13627. The lock infrastructure must forbid this because it does
136 // not work in versions of MySQL older than 5.7.
137
138 $this->assertTrue(
139 ($caught instanceof Exception),
140 pht('Expect multiple locks on the same connection to fail.'));
141 }
142
143 public function testPoolReleaseOnFailure() {
144 $conn = PhabricatorGlobalLock::newConnection();
145 $lock_name = $this->newLockName();
146
147 PhabricatorGlobalLock::clearConnectionPool();
148
149 $this->assertEqual(
150 0,
151 PhabricatorGlobalLock::getConnectionPoolSize(),
152 pht('Clear Connection Pool'));
153
154 $lock = PhabricatorGlobalLock::newLock($lock_name);
155
156 // NOTE: We're cheating here, since there's a global registry of locks
157 // for the process that we have to bypass. In the real world, this lock
158 // would have to be held by some external process. To simplify this
159 // test case, just use a raw "GET_LOCK()" call to hold the lock.
160
161 $raw_conn = PhabricatorGlobalLock::newConnection();
162 $raw_name = $lock->getName();
163
164 $row = queryfx_one(
165 $raw_conn,
166 'SELECT GET_LOCK(%s, %f)',
167 $raw_name,
168 0);
169 $this->assertTrue((bool)head($row), pht('Establish Raw Lock'));
170
171 $this->assertEqual(
172 0,
173 PhabricatorGlobalLock::getConnectionPoolSize(),
174 pht('Connection Pool with Held Lock'));
175
176 // We expect this sequence to establish a new connection, fail to acquire
177 // the lock, then put the connection in the connection pool. After the
178 // first cycle, the connection should be reused.
179
180 for ($ii = 0; $ii < 3; $ii++) {
181 $this->tryHeldLock($lock_name);
182 $this->assertEqual(
183 1,
184 PhabricatorGlobalLock::getConnectionPoolSize(),
185 pht('Connection Pool After Lock Failure'));
186 }
187
188 PhabricatorGlobalLock::clearConnectionPool();
189
190 // Now, do the same thing with an external connection. This connection
191 // should not be put into the pool! See T13627.
192
193 for ($ii = 0; $ii < 3; $ii++) {
194 $this->tryHeldLock($lock_name, $conn);
195 $this->assertEqual(
196 0,
197 PhabricatorGlobalLock::getConnectionPoolSize(),
198 pht('Connection Pool After External Lock Failure'));
199 }
200 }
201
202 private function newLockName() {
203 return 'testlock-'.Filesystem::readRandomCharacters(16);
204 }
205
206 private function tryHeldLock(
207 $lock_name,
208 ?AphrontDatabaseConnection $conn = null) {
209
210 $lock = PhabricatorGlobalLock::newLock($lock_name);
211
212 if ($conn) {
213 $lock->setExternalConnection($conn);
214 }
215
216 $caught = null;
217 try {
218 $lock->lock(0);
219 } catch (PhutilLockException $ex) {
220 $caught = $ex;
221 }
222
223 $this->assertTrue($caught instanceof PhutilLockException);
224 }
225
226
227}