Correct Aphlict websocket URI construction after PHP8 compatibility changes
[phabricator.git] / src / infrastructure / storage / lisk / PhabricatorLiskDAO.php
blobc6466a6127eb2f40ab066bf4ec54f5a7b25db88e
1 <?php
3 /**
4 * @task config Configuring Storage
5 */
6 abstract class PhabricatorLiskDAO extends LiskDAO {
8 private static $namespaceStack = array();
9 private $forcedNamespace;
11 const ATTACHABLE = '<attachable>';
12 const CONFIG_APPLICATION_SERIALIZERS = 'phabricator/serializers';
14 /* -( Configuring Storage )------------------------------------------------ */
16 /**
17 * @task config
19 public static function pushStorageNamespace($namespace) {
20 self::$namespaceStack[] = $namespace;
23 /**
24 * @task config
26 public static function popStorageNamespace() {
27 array_pop(self::$namespaceStack);
30 /**
31 * @task config
33 public static function getDefaultStorageNamespace() {
34 return PhabricatorEnv::getEnvConfig('storage.default-namespace');
37 /**
38 * @task config
40 public static function getStorageNamespace() {
41 $namespace = end(self::$namespaceStack);
42 if (!strlen($namespace)) {
43 $namespace = self::getDefaultStorageNamespace();
45 if ($namespace === null || !strlen($namespace)) {
46 throw new Exception(pht('No storage namespace configured!'));
48 return $namespace;
51 public function setForcedStorageNamespace($namespace) {
52 $this->forcedNamespace = $namespace;
53 return $this;
56 /**
57 * @task config
59 protected function establishLiveConnection($mode) {
60 $namespace = self::getStorageNamespace();
61 $database = $namespace.'_'.$this->getApplicationName();
63 $is_readonly = PhabricatorEnv::isReadOnly();
65 if ($is_readonly && ($mode != 'r')) {
66 $this->raiseImproperWrite($database);
69 $connection = $this->newClusterConnection(
70 $this->getApplicationName(),
71 $database,
72 $mode);
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);
82 return $connection;
85 private function newClusterConnection($application, $database, $mode) {
86 $master = PhabricatorDatabaseRef::getMasterDatabaseRefForApplication(
87 $application);
89 $master_exception = null;
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);
99 PhabricatorEnv::setReadOnly(
100 true,
101 PhabricatorEnv::READONLY_UNREACHABLE);
103 $master_exception = $master->getConnectionException();
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);
126 return $connection;
130 if (!$master && !$replica) {
131 $this->raiseUnconfigured($database);
134 $this->raiseUnreachable($database, $master_exception);
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));
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));
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));
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);
169 if ($proxy) {
170 $proxy_message = pht(
171 '%s: %s',
172 get_class($proxy),
173 $proxy->getMessage());
174 $message = $message."\n\n".$proxy_message;
177 throw new PhabricatorClusterStrandedException($message);
182 * @task config
184 public function getTableName() {
185 $str = 'phabricator';
186 $len = strlen($str);
188 $class = strtolower(get_class($this));
189 if (!strncmp($class, $str, $len)) {
190 $class = substr($class, $len);
192 $app = $this->getApplicationName();
193 if (!strncmp($class, $app, strlen($app))) {
194 $class = substr($class, strlen($app));
197 if (strlen($class)) {
198 return $app.'_'.$class;
199 } else {
200 return $app;
205 * @task config
207 abstract public function getApplicationName();
209 protected function getDatabaseName() {
210 if ($this->forcedNamespace) {
211 $namespace = $this->forcedNamespace;
212 } else {
213 $namespace = self::getStorageNamespace();
216 return $namespace.'_'.$this->getApplicationName();
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.
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).
227 public static function chunkSQL(
228 array $fragments,
229 $limit = null) {
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);
237 $result = array();
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);
249 if ($chunk) {
250 // Chunks after the first also imply glue.
251 $this_len += $glue_len;
254 if ($len + $this_len <= $limit) {
255 $len += $this_len;
256 $chunk[] = $fragment;
257 } else {
258 if ($chunk) {
259 $result[] = $chunk;
261 $len = ($this_len - $glue_len);
262 $chunk = array($fragment);
266 if ($chunk) {
267 $result[] = $chunk;
270 return $result;
273 protected function assertAttached($property) {
274 if ($property === self::ATTACHABLE) {
275 throw new PhabricatorDataNotAttachedException($this);
277 return $property;
280 protected function assertAttachedKey($value, $key) {
281 $this->assertAttached($value);
282 if (!array_key_exists($key, $value)) {
283 throw new PhabricatorDataNotAttachedException($this);
285 return $value[$key];
288 protected function detectEncodingForStorage($string) {
289 return phutil_is_utf8($string) ? 'utf8' : null;
292 protected function getUTF8StringFromStorage($string, $encoding) {
293 if ($encoding == 'utf8') {
294 return $string;
297 if (function_exists('mb_detect_encoding')) {
298 if ($encoding !== null && strlen($encoding)) {
299 $try_encodings = array(
300 $encoding,
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',
313 $guess = mb_detect_encoding($string, $try_encodings);
314 if ($guess) {
315 return mb_convert_encoding($string, 'UTF-8', $guess);
319 return phutil_utf8ize($string);
322 protected function willReadData(array &$data) {
323 parent::willReadData($data);
325 static $custom;
326 if ($custom === null) {
327 $custom = $this->getConfigOption(self::CONFIG_APPLICATION_SERIALIZERS);
330 if ($custom) {
331 foreach ($custom as $key => $serializer) {
332 $data[$key] = $serializer->willReadValue($data[$key]);
337 protected function willWriteData(array &$data) {
338 static $custom;
339 if ($custom === null) {
340 $custom = $this->getConfigOption(self::CONFIG_APPLICATION_SERIALIZERS);
343 if ($custom) {
344 foreach ($custom as $key => $serializer) {
345 $data[$key] = $serializer->willWriteValue($data[$key]);
349 parent::willWriteData($data);