4 * @task config Configuring Storage
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 )------------------------------------------------ */
19 public static function pushStorageNamespace($namespace) {
20 self
::$namespaceStack[] = $namespace;
26 public static function popStorageNamespace() {
27 array_pop(self
::$namespaceStack);
33 public static function getDefaultStorageNamespace() {
34 return PhabricatorEnv
::getEnvConfig('storage.default-namespace');
40 public static function getStorageNamespace() {
41 $namespace = end(self
::$namespaceStack);
42 if (!strlen($namespace)) {
43 $namespace = self
::getDefaultStorageNamespace();
45 if (!strlen($namespace)) {
46 throw new Exception(pht('No storage namespace configured!'));
51 public function setForcedStorageNamespace($namespace) {
52 $this->forcedNamespace
= $namespace;
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(),
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
79 $connection->setReadOnly(true);
85 private function newClusterConnection($application, $database, $mode) {
86 $master = PhabricatorDatabaseRef
::getMasterDatabaseRefForApplication(
89 $master_exception = null;
91 if ($master && !$master->isSevered()) {
92 $connection = $master->newApplicationConnection($database);
93 if ($master->isReachable($connection)) {
97 $this->raiseImpossibleWrite($database);
99 PhabricatorEnv
::setReadOnly(
101 PhabricatorEnv
::READONLY_UNREACHABLE
);
103 $master_exception = $master->getConnectionException();
107 $replica = PhabricatorDatabaseRef
::getReplicaDatabaseRefForApplication(
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(
119 'Failed to connect to master database ("%s"), failing over '.
120 'into read-only mode.',
123 phlog($proxy_exception);
130 if (!$master && !$replica) {
131 $this->raiseUnconfigured($database);
134 $this->raiseUnreachable($database, $master_exception);
137 private function raiseImproperWrite($database) {
138 throw new PhabricatorClusterImproperWriteException(
140 'Unable to establish a write-mode connection (to application '.
141 'database "%s") because Phabricator is in read-only mode. Whatever '.
142 'you are trying to do does not function correctly in read-only mode.',
146 private function raiseImpossibleWrite($database) {
147 throw new PhabricatorClusterImpossibleWriteException(
149 'Unable to connect to master database ("%s"). This is a severe '.
150 'failure; your request did not complete.',
154 private function raiseUnconfigured($database) {
157 'Unable to establish a connection to any database host '.
158 '(while trying "%s"). No masters or replicas are configured.',
162 private function raiseUnreachable($database, Exception
$proxy = null) {
164 'Unable to establish a connection to any database host '.
165 '(while trying "%s"). All masters and replicas are completely '.
170 $proxy_message = pht(
173 $proxy->getMessage());
174 $message = $message."\n\n".$proxy_message;
177 throw new PhabricatorClusterStrandedException($message);
184 public function getTableName() {
185 $str = 'phabricator';
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;
207 abstract public function getApplicationName();
209 protected function getDatabaseName() {
210 if ($this->forcedNamespace
) {
211 $namespace = $this->forcedNamespace
;
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(
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);
241 $glue_len = strlen(', ');
242 foreach ($fragments as $fragment) {
243 if ($fragment instanceof PhutilQueryString
) {
244 $this_len = strlen($fragment->getUnmaskedString());
246 $this_len = strlen($fragment);
250 // Chunks after the first also imply glue.
251 $this_len +
= $glue_len;
254 if ($len +
$this_len <= $limit) {
256 $chunk[] = $fragment;
261 $len = ($this_len - $glue_len);
262 $chunk = array($fragment);
273 protected function assertAttached($property) {
274 if ($property === self
::ATTACHABLE
) {
275 throw new PhabricatorDataNotAttachedException($this);
280 protected function assertAttachedKey($value, $key) {
281 $this->assertAttached($value);
282 if (!array_key_exists($key, $value)) {
283 throw new PhabricatorDataNotAttachedException($this);
288 protected function detectEncodingForStorage($string) {
289 return phutil_is_utf8($string) ?
'utf8' : null;
292 protected function getUTF8StringFromStorage($string, $encoding) {
293 if ($encoding == 'utf8') {
297 if (function_exists('mb_detect_encoding')) {
298 if (strlen($encoding)) {
299 $try_encodings = array(
303 // TODO: This is pretty much a guess, and probably needs to be
304 // configurable in the long run.
305 $try_encodings = array(
313 $guess = mb_detect_encoding($string, $try_encodings);
315 return mb_convert_encoding($string, 'UTF-8', $guess);
319 return phutil_utf8ize($string);
322 protected function willReadData(array &$data) {
323 parent
::willReadData($data);
326 if ($custom === null) {
327 $custom = $this->getConfigOption(self
::CONFIG_APPLICATION_SERIALIZERS
);
331 foreach ($custom as $key => $serializer) {
332 $data[$key] = $serializer->willReadValue($data[$key]);
337 protected function willWriteData(array &$data) {
339 if ($custom === null) {
340 $custom = $this->getConfigOption(self
::CONFIG_APPLICATION_SERIALIZERS
);
344 foreach ($custom as $key => $serializer) {
345 $data[$key] = $serializer->willWriteValue($data[$key]);
349 parent
::willWriteData($data);