Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/phabricator
Path: blob/master/src/infrastructure/storage/lisk/PhabricatorLiskDAO.php
12241 views
1
<?php
2
3
/**
4
* @task config Configuring Storage
5
*/
6
abstract 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 ($namespace === null || !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 PhutilProxyException(
118
pht(
119
'Failed to connect to master database ("%s"), failing over '.
120
'into read-only mode.',
121
$database),
122
$master_exception);
123
phlog($proxy_exception);
124
}
125
126
return $connection;
127
}
128
}
129
130
if (!$master && !$replica) {
131
$this->raiseUnconfigured($database);
132
}
133
134
$this->raiseUnreachable($database, $master_exception);
135
}
136
137
private function raiseImproperWrite($database) {
138
throw new PhabricatorClusterImproperWriteException(
139
pht(
140
'Unable to establish a write-mode connection (to application '.
141
'database "%s") because this server is in read-only mode. Whatever '.
142
'you are trying to do does not function correctly in read-only mode.',
143
$database));
144
}
145
146
private function raiseImpossibleWrite($database) {
147
throw new PhabricatorClusterImpossibleWriteException(
148
pht(
149
'Unable to connect to master database ("%s"). This is a severe '.
150
'failure; your request did not complete.',
151
$database));
152
}
153
154
private function raiseUnconfigured($database) {
155
throw new Exception(
156
pht(
157
'Unable to establish a connection to any database host '.
158
'(while trying "%s"). No masters or replicas are configured.',
159
$database));
160
}
161
162
private function raiseUnreachable($database, Exception $proxy = null) {
163
$message = pht(
164
'Unable to establish a connection to any database host '.
165
'(while trying "%s"). All masters and replicas are completely '.
166
'unreachable.',
167
$database);
168
169
if ($proxy) {
170
$proxy_message = pht(
171
'%s: %s',
172
get_class($proxy),
173
$proxy->getMessage());
174
$message = $message."\n\n".$proxy_message;
175
}
176
177
throw new PhabricatorClusterStrandedException($message);
178
}
179
180
181
/**
182
* @task config
183
*/
184
public function getTableName() {
185
$str = 'phabricator';
186
$len = strlen($str);
187
188
$class = strtolower(get_class($this));
189
if (!strncmp($class, $str, $len)) {
190
$class = substr($class, $len);
191
}
192
$app = $this->getApplicationName();
193
if (!strncmp($class, $app, strlen($app))) {
194
$class = substr($class, strlen($app));
195
}
196
197
if (strlen($class)) {
198
return $app.'_'.$class;
199
} else {
200
return $app;
201
}
202
}
203
204
/**
205
* @task config
206
*/
207
abstract public function getApplicationName();
208
209
protected function getDatabaseName() {
210
if ($this->forcedNamespace) {
211
$namespace = $this->forcedNamespace;
212
} else {
213
$namespace = self::getStorageNamespace();
214
}
215
216
return $namespace.'_'.$this->getApplicationName();
217
}
218
219
/**
220
* Break a list of escaped SQL statement fragments (e.g., VALUES lists for
221
* INSERT, previously built with @{function:qsprintf}) into chunks which will
222
* fit under the MySQL 'max_allowed_packet' limit.
223
*
224
* If a statement is too large to fit within the limit, it is broken into
225
* its own chunk (but might fail when the query executes).
226
*/
227
public static function chunkSQL(
228
array $fragments,
229
$limit = null) {
230
231
if ($limit === null) {
232
// NOTE: Hard-code this at 1MB for now, minus a 10% safety buffer.
233
// Eventually we could query MySQL or let the user configure it.
234
$limit = (int)((1024 * 1024) * 0.90);
235
}
236
237
$result = array();
238
239
$chunk = array();
240
$len = 0;
241
$glue_len = strlen(', ');
242
foreach ($fragments as $fragment) {
243
if ($fragment instanceof PhutilQueryString) {
244
$this_len = strlen($fragment->getUnmaskedString());
245
} else {
246
$this_len = strlen($fragment);
247
}
248
249
if ($chunk) {
250
// Chunks after the first also imply glue.
251
$this_len += $glue_len;
252
}
253
254
if ($len + $this_len <= $limit) {
255
$len += $this_len;
256
$chunk[] = $fragment;
257
} else {
258
if ($chunk) {
259
$result[] = $chunk;
260
}
261
$len = ($this_len - $glue_len);
262
$chunk = array($fragment);
263
}
264
}
265
266
if ($chunk) {
267
$result[] = $chunk;
268
}
269
270
return $result;
271
}
272
273
protected function assertAttached($property) {
274
if ($property === self::ATTACHABLE) {
275
throw new PhabricatorDataNotAttachedException($this);
276
}
277
return $property;
278
}
279
280
protected function assertAttachedKey($value, $key) {
281
$this->assertAttached($value);
282
if (!array_key_exists($key, $value)) {
283
throw new PhabricatorDataNotAttachedException($this);
284
}
285
return $value[$key];
286
}
287
288
protected function detectEncodingForStorage($string) {
289
return phutil_is_utf8($string) ? 'utf8' : null;
290
}
291
292
protected function getUTF8StringFromStorage($string, $encoding) {
293
if ($encoding == 'utf8') {
294
return $string;
295
}
296
297
if (function_exists('mb_detect_encoding')) {
298
if ($encoding !== null && strlen($encoding)) {
299
$try_encodings = array(
300
$encoding,
301
);
302
} else {
303
// TODO: This is pretty much a guess, and probably needs to be
304
// configurable in the long run.
305
$try_encodings = array(
306
'JIS',
307
'EUC-JP',
308
'SJIS',
309
'ISO-8859-1',
310
);
311
}
312
313
$guess = mb_detect_encoding($string, $try_encodings);
314
if ($guess) {
315
return mb_convert_encoding($string, 'UTF-8', $guess);
316
}
317
}
318
319
return phutil_utf8ize($string);
320
}
321
322
protected function willReadData(array &$data) {
323
parent::willReadData($data);
324
325
static $custom;
326
if ($custom === null) {
327
$custom = $this->getConfigOption(self::CONFIG_APPLICATION_SERIALIZERS);
328
}
329
330
if ($custom) {
331
foreach ($custom as $key => $serializer) {
332
$data[$key] = $serializer->willReadValue($data[$key]);
333
}
334
}
335
}
336
337
protected function willWriteData(array &$data) {
338
static $custom;
339
if ($custom === null) {
340
$custom = $this->getConfigOption(self::CONFIG_APPLICATION_SERIALIZERS);
341
}
342
343
if ($custom) {
344
foreach ($custom as $key => $serializer) {
345
$data[$key] = $serializer->willWriteValue($data[$key]);
346
}
347
}
348
349
parent::willWriteData($data);
350
}
351
352
353
}
354
355