@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
3/**
4 * @task config Configuring Storage
5 */
6abstract class PhabricatorLiskDAO extends LiskDAO {
7
8 private static $namespaceStack = array();
9 private $forcedNamespace;
10
11 const ATTACHABLE = '<attachable>';
12 const CONFIG_APPLICATION_SERIALIZERS = 'phabricator/serializers';
13
14/* -( Configuring Storage )------------------------------------------------ */
15
16 /**
17 * @task config
18 */
19 public static function pushStorageNamespace($namespace) {
20 self::$namespaceStack[] = $namespace;
21 }
22
23 /**
24 * @task config
25 */
26 public static function popStorageNamespace() {
27 array_pop(self::$namespaceStack);
28 }
29
30 /**
31 * @task config
32 */
33 public static function getDefaultStorageNamespace() {
34 return PhabricatorEnv::getEnvConfig('storage.default-namespace');
35 }
36
37 /**
38 * @task config
39 */
40 public static function getStorageNamespace() {
41 $namespace = end(self::$namespaceStack);
42 if (!strlen($namespace)) {
43 $namespace = self::getDefaultStorageNamespace();
44 }
45 if (!strlen($namespace)) {
46 throw new Exception(pht('No storage namespace configured!'));
47 }
48 return $namespace;
49 }
50
51 public function setForcedStorageNamespace($namespace) {
52 $this->forcedNamespace = $namespace;
53 return $this;
54 }
55
56 /**
57 * @task config
58 */
59 protected function establishLiveConnection($mode) {
60 $namespace = self::getStorageNamespace();
61 $database = $namespace.'_'.$this->getApplicationName();
62
63 $is_readonly = PhabricatorEnv::isReadOnly();
64
65 if ($is_readonly && ($mode != 'r')) {
66 $this->raiseImproperWrite($database);
67 }
68
69 $connection = $this->newClusterConnection(
70 $this->getApplicationName(),
71 $database,
72 $mode);
73
74 // TODO: This should be testing if the mode is "r", but that would probably
75 // break a lot of things. Perform a more narrow test for readonly mode
76 // until we have greater certainty that this works correctly most of the
77 // time.
78 if ($is_readonly) {
79 $connection->setReadOnly(true);
80 }
81
82 return $connection;
83 }
84
85 private function newClusterConnection($application, $database, $mode) {
86 $master = PhabricatorDatabaseRef::getMasterDatabaseRefForApplication(
87 $application);
88
89 $master_exception = null;
90
91 if ($master && !$master->isSevered()) {
92 $connection = $master->newApplicationConnection($database);
93 if ($master->isReachable($connection)) {
94 return $connection;
95 } else {
96 if ($mode == 'w') {
97 $this->raiseImpossibleWrite($database);
98 }
99 PhabricatorEnv::setReadOnly(
100 true,
101 PhabricatorEnv::READONLY_UNREACHABLE);
102
103 $master_exception = $master->getConnectionException();
104 }
105 }
106
107 $replica = PhabricatorDatabaseRef::getReplicaDatabaseRefForApplication(
108 $application);
109 if ($replica) {
110 $connection = $replica->newApplicationConnection($database);
111 $connection->setReadOnly(true);
112 if ($replica->isReachable($connection)) {
113 if ($master_exception) {
114 // If we ended up here as the result of a failover, log the
115 // exception. This is seriously bad news even if we are able
116 // to recover from it.
117 $proxy_exception = new Exception(
118 pht(
119 'Failed to connect to master database ("%s"), failing over '.
120 'into read-only mode.',
121 $database),
122 0,
123 $master_exception);
124 phlog($proxy_exception);
125 }
126
127 return $connection;
128 }
129 }
130
131 if (!$master && !$replica) {
132 $this->raiseUnconfigured($database);
133 }
134
135 $this->raiseUnreachable($database, $master_exception);
136 }
137
138 private function raiseImproperWrite($database) {
139 throw new PhabricatorClusterImproperWriteException(
140 pht(
141 'Unable to establish a write-mode connection (to application '.
142 'database "%s") because this server is in read-only mode. Whatever '.
143 'you are trying to do does not function correctly in read-only mode.',
144 $database));
145 }
146
147 private function raiseImpossibleWrite($database) {
148 throw new PhabricatorClusterImpossibleWriteException(
149 pht(
150 'Unable to connect to master database ("%s"). This is a severe '.
151 'failure; your request did not complete.',
152 $database));
153 }
154
155 private function raiseUnconfigured($database) {
156 throw new Exception(
157 pht(
158 'Unable to establish a connection to any database host '.
159 '(while trying "%s"). No masters or replicas are configured.',
160 $database));
161 }
162
163 private function raiseUnreachable($database, ?Exception $proxy = null) {
164 $message = pht(
165 'Unable to establish a connection to any database host '.
166 '(while trying "%s"). All masters and replicas are completely '.
167 'unreachable.',
168 $database);
169
170 if ($proxy) {
171 $proxy_message = pht(
172 '%s: %s',
173 get_class($proxy),
174 $proxy->getMessage());
175 $message = $message."\n\n".$proxy_message;
176 }
177
178 throw new PhabricatorClusterStrandedException($message);
179 }
180
181
182 /**
183 * Get the database table name
184 * @return string Name of the database table
185 * @task config
186 */
187 public function getTableName() {
188 $str = 'phabricator';
189 $len = strlen($str);
190
191 $class = strtolower(get_class($this));
192 if (!strncmp($class, $str, $len)) {
193 $class = substr($class, $len);
194 }
195 $app = $this->getApplicationName();
196 if (!strncmp($class, $app, strlen($app))) {
197 $class = substr($class, strlen($app));
198 }
199
200 if (strlen($class)) {
201 return $app.'_'.$class;
202 } else {
203 return $app;
204 }
205 }
206
207 /**
208 * Get the database name
209 * @return string Name of the database
210 * @task config
211 */
212 abstract public function getApplicationName();
213
214 protected function getDatabaseName() {
215 if ($this->forcedNamespace) {
216 $namespace = $this->forcedNamespace;
217 } else {
218 $namespace = self::getStorageNamespace();
219 }
220
221 return $namespace.'_'.$this->getApplicationName();
222 }
223
224 /**
225 * Break a list of escaped SQL statement fragments (e.g., VALUES lists for
226 * INSERT, previously built with @{function:qsprintf}) into chunks which will
227 * fit under the MySQL 'max_allowed_packet' limit.
228 *
229 * If a statement is too large to fit within the limit, it is broken into
230 * its own chunk (but might fail when the query executes).
231 */
232 public static function chunkSQL(
233 array $fragments,
234 $limit = null) {
235
236 if ($limit === null) {
237 // NOTE: Hard-code this at 1MB for now, minus a 10% safety buffer.
238 // Eventually we could query MySQL or let the user configure it.
239 $limit = (int)((1024 * 1024) * 0.90);
240 }
241
242 $result = array();
243
244 $chunk = array();
245 $len = 0;
246 $glue_len = strlen(', ');
247 foreach ($fragments as $fragment) {
248 if ($fragment instanceof PhutilQueryString) {
249 $this_len = strlen($fragment->getUnmaskedString());
250 } else {
251 $this_len = strlen($fragment);
252 }
253
254 if ($chunk) {
255 // Chunks after the first also imply glue.
256 $this_len += $glue_len;
257 }
258
259 if ($len + $this_len <= $limit) {
260 $len += $this_len;
261 $chunk[] = $fragment;
262 } else {
263 if ($chunk) {
264 $result[] = $chunk;
265 }
266 $len = ($this_len - $glue_len);
267 $chunk = array($fragment);
268 }
269 }
270
271 if ($chunk) {
272 $result[] = $chunk;
273 }
274
275 return $result;
276 }
277
278 protected function assertAttached($property) {
279 if ($property === self::ATTACHABLE) {
280 throw new PhabricatorDataNotAttachedException($this);
281 }
282 return $property;
283 }
284
285 protected function assertAttachedKey($value, $key) {
286 $this->assertAttached($value);
287 if (!array_key_exists($key, $value)) {
288 throw new PhabricatorDataNotAttachedException($this);
289 }
290 return $value[$key];
291 }
292
293 protected function detectEncodingForStorage($string) {
294 return phutil_is_utf8($string) ? 'utf8' : null;
295 }
296
297 protected function getUTF8StringFromStorage($string, $encoding) {
298 if ($encoding == 'utf8' || !phutil_nonempty_string($string)) {
299 return $string;
300 }
301
302 if (function_exists('mb_detect_encoding')) {
303 if (phutil_nonempty_string($encoding)) {
304 $try_encodings = array(
305 $encoding,
306 );
307 } else {
308 // TODO: This is pretty much a guess, and probably needs to be
309 // configurable in the long run.
310 $try_encodings = array(
311 'JIS',
312 'EUC-JP',
313 'SJIS',
314 'ISO-8859-1',
315 );
316 }
317
318 $guess = mb_detect_encoding($string, $try_encodings);
319 if ($guess) {
320 return mb_convert_encoding($string, 'UTF-8', $guess);
321 }
322 }
323
324 return phutil_utf8ize($string);
325 }
326
327 protected function willReadData(array &$data) {
328 parent::willReadData($data);
329
330 static $custom = array();
331 if (!isset($custom[static::class])) {
332 $custom[static::class] = $this->getConfigOption(
333 self::CONFIG_APPLICATION_SERIALIZERS);
334 }
335
336 if (!empty($custom[static::class])) {
337 foreach ($custom[static::class] as $key => $serializer) {
338 $data[$key] = $serializer->willReadValue($data[$key]);
339 }
340 }
341 }
342
343 protected function willWriteData(array &$data) {
344 static $custom = array();
345 if (!isset($custom[static::class])) {
346 $custom[static::class] = $this->getConfigOption(
347 self::CONFIG_APPLICATION_SERIALIZERS);
348 }
349
350 if (!empty($custom[static::class])) {
351 foreach ($custom[static::class] as $key => $serializer) {
352 $data[$key] = $serializer->willWriteValue($data[$key]);
353 }
354 }
355
356 parent::willWriteData($data);
357 }
358
359
360}