mirror of
https://we.phorge.it/source/phorge.git
synced 2024-12-01 03:02:43 +01:00
e41c25de50
Summary: The goal is to make fulltext search back-ends more extensible, configurable and robust. When this is finished it will be possible to have multiple search storage back-ends and potentially multiple instances of each. Individual instances can be configured with roles such as 'read', 'write' which control which hosts will receive writes to the index and which hosts will respond to queries. These two roles make it possible to have any combination of: * read-only * write-only * read-write * disabled This 'roles' mechanism is extensible to add new roles should that be needed in the future. In addition to supporting multiple elasticsearch and mysql search instances, this refactors the connection health monitoring infrastructure from PhabricatorDatabaseHealthRecord and utilizes the same system for monitoring the health of elasticsearch nodes. This will allow Wikimedia's phabricator to be redundant across data centers (mysql already is, elasticsearch should be as well). The real-world use-case I have in mind here is writing to two indexes (two elasticsearch clusters in different data centers) but reading from only one. Then toggling the 'read' property when we want to migrate to the other data center (and when we migrate from elasticsearch 2.x to 5.x) Hopefully this is useful in the upstream as well. Remaining TODO: * test cases * documentation Test Plan: (WARNING) This will most likely require the elasticsearch index to be deleted and re-created due to schema changes. Tested with elasticsearch versions 2.4 and 5.2 using the following config: ```lang=json "cluster.search": [ { "type": "elasticsearch", "hosts": [ { "host": "localhost", "roles": { "read": true, "write": true } } ], "port": 9200, "protocol": "http", "path": "/phabricator", "version": 5 }, { "type": "mysql", "roles": { "write": true } } ] Also deployed the same changes to Wikimedia's production Phabricator instance without any issues whatsoever. ``` Reviewers: epriestley, #blessed_reviewers Reviewed By: epriestley, #blessed_reviewers Subscribers: Korvin, epriestley Tags: #elasticsearch, #clusters, #wikimedia Differential Revision: https://secure.phabricator.com/D17384
731 lines
19 KiB
PHP
731 lines
19 KiB
PHP
<?php
|
|
|
|
final class PhabricatorDatabaseRef
|
|
extends Phobject {
|
|
|
|
const STATUS_OKAY = 'okay';
|
|
const STATUS_FAIL = 'fail';
|
|
const STATUS_AUTH = 'auth';
|
|
const STATUS_REPLICATION_CLIENT = 'replication-client';
|
|
|
|
const REPLICATION_OKAY = 'okay';
|
|
const REPLICATION_MASTER_REPLICA = 'master-replica';
|
|
const REPLICATION_REPLICA_NONE = 'replica-none';
|
|
const REPLICATION_SLOW = 'replica-slow';
|
|
const REPLICATION_NOT_REPLICATING = 'not-replicating';
|
|
|
|
const KEY_HEALTH = 'cluster.db.health';
|
|
const KEY_REFS = 'cluster.db.refs';
|
|
const KEY_INDIVIDUAL = 'cluster.db.individual';
|
|
|
|
private $host;
|
|
private $port;
|
|
private $user;
|
|
private $pass;
|
|
private $disabled;
|
|
private $isMaster;
|
|
private $isIndividual;
|
|
|
|
private $connectionLatency;
|
|
private $connectionStatus;
|
|
private $connectionMessage;
|
|
|
|
private $replicaStatus;
|
|
private $replicaMessage;
|
|
private $replicaDelay;
|
|
|
|
private $healthRecord;
|
|
private $didFailToConnect;
|
|
|
|
private $isDefaultPartition;
|
|
private $applicationMap = array();
|
|
private $masterRef;
|
|
private $replicaRefs = array();
|
|
private $usePersistentConnections;
|
|
|
|
public function setHost($host) {
|
|
$this->host = $host;
|
|
return $this;
|
|
}
|
|
|
|
public function getHost() {
|
|
return $this->host;
|
|
}
|
|
|
|
public function setPort($port) {
|
|
$this->port = $port;
|
|
return $this;
|
|
}
|
|
|
|
public function getPort() {
|
|
return $this->port;
|
|
}
|
|
|
|
public function setUser($user) {
|
|
$this->user = $user;
|
|
return $this;
|
|
}
|
|
|
|
public function getUser() {
|
|
return $this->user;
|
|
}
|
|
|
|
public function setPass(PhutilOpaqueEnvelope $pass) {
|
|
$this->pass = $pass;
|
|
return $this;
|
|
}
|
|
|
|
public function getPass() {
|
|
return $this->pass;
|
|
}
|
|
|
|
public function setIsMaster($is_master) {
|
|
$this->isMaster = $is_master;
|
|
return $this;
|
|
}
|
|
|
|
public function getIsMaster() {
|
|
return $this->isMaster;
|
|
}
|
|
|
|
public function setDisabled($disabled) {
|
|
$this->disabled = $disabled;
|
|
return $this;
|
|
}
|
|
|
|
public function getDisabled() {
|
|
return $this->disabled;
|
|
}
|
|
|
|
public function setConnectionLatency($connection_latency) {
|
|
$this->connectionLatency = $connection_latency;
|
|
return $this;
|
|
}
|
|
|
|
public function getConnectionLatency() {
|
|
return $this->connectionLatency;
|
|
}
|
|
|
|
public function setConnectionStatus($connection_status) {
|
|
$this->connectionStatus = $connection_status;
|
|
return $this;
|
|
}
|
|
|
|
public function getConnectionStatus() {
|
|
if ($this->connectionStatus === null) {
|
|
throw new PhutilInvalidStateException('queryAll');
|
|
}
|
|
|
|
return $this->connectionStatus;
|
|
}
|
|
|
|
public function setConnectionMessage($connection_message) {
|
|
$this->connectionMessage = $connection_message;
|
|
return $this;
|
|
}
|
|
|
|
public function getConnectionMessage() {
|
|
return $this->connectionMessage;
|
|
}
|
|
|
|
public function setReplicaStatus($replica_status) {
|
|
$this->replicaStatus = $replica_status;
|
|
return $this;
|
|
}
|
|
|
|
public function getReplicaStatus() {
|
|
return $this->replicaStatus;
|
|
}
|
|
|
|
public function setReplicaMessage($replica_message) {
|
|
$this->replicaMessage = $replica_message;
|
|
return $this;
|
|
}
|
|
|
|
public function getReplicaMessage() {
|
|
return $this->replicaMessage;
|
|
}
|
|
|
|
public function setReplicaDelay($replica_delay) {
|
|
$this->replicaDelay = $replica_delay;
|
|
return $this;
|
|
}
|
|
|
|
public function getReplicaDelay() {
|
|
return $this->replicaDelay;
|
|
}
|
|
|
|
public function setIsIndividual($is_individual) {
|
|
$this->isIndividual = $is_individual;
|
|
return $this;
|
|
}
|
|
|
|
public function getIsIndividual() {
|
|
return $this->isIndividual;
|
|
}
|
|
|
|
public function setIsDefaultPartition($is_default_partition) {
|
|
$this->isDefaultPartition = $is_default_partition;
|
|
return $this;
|
|
}
|
|
|
|
public function getIsDefaultPartition() {
|
|
return $this->isDefaultPartition;
|
|
}
|
|
|
|
public function setUsePersistentConnections($use_persistent_connections) {
|
|
$this->usePersistentConnections = $use_persistent_connections;
|
|
return $this;
|
|
}
|
|
|
|
public function getUsePersistentConnections() {
|
|
return $this->usePersistentConnections;
|
|
}
|
|
|
|
public function setApplicationMap(array $application_map) {
|
|
$this->applicationMap = $application_map;
|
|
return $this;
|
|
}
|
|
|
|
public function getApplicationMap() {
|
|
return $this->applicationMap;
|
|
}
|
|
|
|
public function getPartitionStateForCommit() {
|
|
$state = PhabricatorEnv::getEnvConfig('cluster.databases');
|
|
foreach ($state as $key => $value) {
|
|
// Don't store passwords, since we don't care if they differ and
|
|
// users may find it surprising.
|
|
unset($state[$key]['pass']);
|
|
}
|
|
|
|
return phutil_json_encode($state);
|
|
}
|
|
|
|
public function setMasterRef(PhabricatorDatabaseRef $master_ref) {
|
|
$this->masterRef = $master_ref;
|
|
return $this;
|
|
}
|
|
|
|
public function getMasterRef() {
|
|
return $this->masterRef;
|
|
}
|
|
|
|
public function addReplicaRef(PhabricatorDatabaseRef $replica_ref) {
|
|
$this->replicaRefs[] = $replica_ref;
|
|
return $this;
|
|
}
|
|
|
|
public function getReplicaRefs() {
|
|
return $this->replicaRefs;
|
|
}
|
|
|
|
|
|
public function getRefKey() {
|
|
$host = $this->getHost();
|
|
|
|
$port = $this->getPort();
|
|
if (strlen($port)) {
|
|
return "{$host}:{$port}";
|
|
}
|
|
|
|
return $host;
|
|
}
|
|
|
|
public static function getConnectionStatusMap() {
|
|
return array(
|
|
self::STATUS_OKAY => array(
|
|
'icon' => 'fa-exchange',
|
|
'color' => 'green',
|
|
'label' => pht('Okay'),
|
|
),
|
|
self::STATUS_FAIL => array(
|
|
'icon' => 'fa-times',
|
|
'color' => 'red',
|
|
'label' => pht('Failed'),
|
|
),
|
|
self::STATUS_AUTH => array(
|
|
'icon' => 'fa-key',
|
|
'color' => 'red',
|
|
'label' => pht('Invalid Credentials'),
|
|
),
|
|
self::STATUS_REPLICATION_CLIENT => array(
|
|
'icon' => 'fa-eye-slash',
|
|
'color' => 'yellow',
|
|
'label' => pht('Missing Permission'),
|
|
),
|
|
);
|
|
}
|
|
|
|
public static function getReplicaStatusMap() {
|
|
return array(
|
|
self::REPLICATION_OKAY => array(
|
|
'icon' => 'fa-download',
|
|
'color' => 'green',
|
|
'label' => pht('Okay'),
|
|
),
|
|
self::REPLICATION_MASTER_REPLICA => array(
|
|
'icon' => 'fa-database',
|
|
'color' => 'red',
|
|
'label' => pht('Replicating Master'),
|
|
),
|
|
self::REPLICATION_REPLICA_NONE => array(
|
|
'icon' => 'fa-download',
|
|
'color' => 'red',
|
|
'label' => pht('Not A Replica'),
|
|
),
|
|
self::REPLICATION_SLOW => array(
|
|
'icon' => 'fa-hourglass',
|
|
'color' => 'red',
|
|
'label' => pht('Slow Replication'),
|
|
),
|
|
self::REPLICATION_NOT_REPLICATING => array(
|
|
'icon' => 'fa-exclamation-triangle',
|
|
'color' => 'red',
|
|
'label' => pht('Not Replicating'),
|
|
),
|
|
);
|
|
}
|
|
|
|
public static function getClusterRefs() {
|
|
$cache = PhabricatorCaches::getRequestCache();
|
|
|
|
$refs = $cache->getKey(self::KEY_REFS);
|
|
if (!$refs) {
|
|
$refs = self::newRefs();
|
|
$cache->setKey(self::KEY_REFS, $refs);
|
|
}
|
|
|
|
return $refs;
|
|
}
|
|
|
|
public static function getLiveIndividualRef() {
|
|
$cache = PhabricatorCaches::getRequestCache();
|
|
|
|
$ref = $cache->getKey(self::KEY_INDIVIDUAL);
|
|
if (!$ref) {
|
|
$ref = self::newIndividualRef();
|
|
$cache->setKey(self::KEY_INDIVIDUAL, $ref);
|
|
}
|
|
|
|
return $ref;
|
|
}
|
|
|
|
public static function newRefs() {
|
|
$default_port = PhabricatorEnv::getEnvConfig('mysql.port');
|
|
$default_port = nonempty($default_port, 3306);
|
|
|
|
$default_user = PhabricatorEnv::getEnvConfig('mysql.user');
|
|
|
|
$default_pass = PhabricatorEnv::getEnvConfig('mysql.pass');
|
|
$default_pass = new PhutilOpaqueEnvelope($default_pass);
|
|
|
|
$config = PhabricatorEnv::getEnvConfig('cluster.databases');
|
|
|
|
return id(new PhabricatorDatabaseRefParser())
|
|
->setDefaultPort($default_port)
|
|
->setDefaultUser($default_user)
|
|
->setDefaultPass($default_pass)
|
|
->newRefs($config);
|
|
}
|
|
|
|
public static function queryAll() {
|
|
$refs = self::getActiveDatabaseRefs();
|
|
return self::queryRefs($refs);
|
|
}
|
|
|
|
private static function queryRefs(array $refs) {
|
|
foreach ($refs as $ref) {
|
|
$conn = $ref->newManagementConnection();
|
|
|
|
$t_start = microtime(true);
|
|
$replica_status = false;
|
|
try {
|
|
$replica_status = queryfx_one($conn, 'SHOW SLAVE STATUS');
|
|
$ref->setConnectionStatus(self::STATUS_OKAY);
|
|
} catch (AphrontAccessDeniedQueryException $ex) {
|
|
$ref->setConnectionStatus(self::STATUS_REPLICATION_CLIENT);
|
|
$ref->setConnectionMessage(
|
|
pht(
|
|
'No permission to run "SHOW SLAVE STATUS". Grant this user '.
|
|
'"REPLICATION CLIENT" permission to allow Phabricator to '.
|
|
'monitor replica health.'));
|
|
} catch (AphrontInvalidCredentialsQueryException $ex) {
|
|
$ref->setConnectionStatus(self::STATUS_AUTH);
|
|
$ref->setConnectionMessage($ex->getMessage());
|
|
} catch (AphrontQueryException $ex) {
|
|
$ref->setConnectionStatus(self::STATUS_FAIL);
|
|
|
|
$class = get_class($ex);
|
|
$message = $ex->getMessage();
|
|
$ref->setConnectionMessage(
|
|
pht(
|
|
'%s: %s',
|
|
get_class($ex),
|
|
$ex->getMessage()));
|
|
}
|
|
$t_end = microtime(true);
|
|
$ref->setConnectionLatency($t_end - $t_start);
|
|
|
|
if ($replica_status !== false) {
|
|
$is_replica = (bool)$replica_status;
|
|
if ($ref->getIsMaster() && $is_replica) {
|
|
$ref->setReplicaStatus(self::REPLICATION_MASTER_REPLICA);
|
|
$ref->setReplicaMessage(
|
|
pht(
|
|
'This host has a "master" role, but is replicating data from '.
|
|
'another host ("%s")!',
|
|
idx($replica_status, 'Master_Host')));
|
|
} else if (!$ref->getIsMaster() && !$is_replica) {
|
|
$ref->setReplicaStatus(self::REPLICATION_REPLICA_NONE);
|
|
$ref->setReplicaMessage(
|
|
pht(
|
|
'This host has a "replica" role, but is not replicating data '.
|
|
'from a master (no output from "SHOW SLAVE STATUS").'));
|
|
} else {
|
|
$ref->setReplicaStatus(self::REPLICATION_OKAY);
|
|
}
|
|
|
|
if ($is_replica) {
|
|
$latency = idx($replica_status, 'Seconds_Behind_Master');
|
|
if (!strlen($latency)) {
|
|
$ref->setReplicaStatus(self::REPLICATION_NOT_REPLICATING);
|
|
} else {
|
|
$latency = (int)$latency;
|
|
$ref->setReplicaDelay($latency);
|
|
if ($latency > 30) {
|
|
$ref->setReplicaStatus(self::REPLICATION_SLOW);
|
|
$ref->setReplicaMessage(
|
|
pht(
|
|
'This replica is lagging far behind the master. Data is at '.
|
|
'risk!'));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return $refs;
|
|
}
|
|
|
|
public function newManagementConnection() {
|
|
return $this->newConnection(
|
|
array(
|
|
'retries' => 0,
|
|
'timeout' => 2,
|
|
));
|
|
}
|
|
|
|
public function newApplicationConnection($database) {
|
|
return $this->newConnection(
|
|
array(
|
|
'database' => $database,
|
|
));
|
|
}
|
|
|
|
public function isSevered() {
|
|
// If we only have an individual database, never sever our connection to
|
|
// it, at least for now. It's possible that using the same severing rules
|
|
// might eventually make sense to help alleviate load-related failures,
|
|
// but we should wait for all the cluster stuff to stabilize first.
|
|
if ($this->getIsIndividual()) {
|
|
return false;
|
|
}
|
|
|
|
if ($this->didFailToConnect) {
|
|
return true;
|
|
}
|
|
|
|
$record = $this->getHealthRecord();
|
|
$is_healthy = $record->getIsHealthy();
|
|
if (!$is_healthy) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public function isReachable(AphrontDatabaseConnection $connection) {
|
|
$record = $this->getHealthRecord();
|
|
$should_check = $record->getShouldCheck();
|
|
|
|
if ($this->isSevered() && !$should_check) {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
$connection->openConnection();
|
|
$reachable = true;
|
|
} catch (AphrontSchemaQueryException $ex) {
|
|
// We get one of these if the database we're trying to select does not
|
|
// exist. In this case, just re-throw the exception. This is expected
|
|
// during first-time setup, when databases like "config" will not exist
|
|
// yet.
|
|
throw $ex;
|
|
} catch (Exception $ex) {
|
|
$reachable = false;
|
|
}
|
|
|
|
if ($should_check) {
|
|
$record->didHealthCheck($reachable);
|
|
}
|
|
|
|
if (!$reachable) {
|
|
$this->didFailToConnect = true;
|
|
}
|
|
|
|
return $reachable;
|
|
}
|
|
|
|
public function checkHealth() {
|
|
$health = $this->getHealthRecord();
|
|
|
|
$should_check = $health->getShouldCheck();
|
|
if ($should_check) {
|
|
// This does an implicit health update.
|
|
$connection = $this->newManagementConnection();
|
|
$this->isReachable($connection);
|
|
}
|
|
|
|
return $this;
|
|
}
|
|
|
|
private function getHealthRecordCacheKey() {
|
|
$host = $this->getHost();
|
|
$port = $this->getPort();
|
|
$key = self::KEY_HEALTH;
|
|
|
|
return "{$key}({$host}, {$port})";
|
|
}
|
|
|
|
public function getHealthRecord() {
|
|
if (!$this->healthRecord) {
|
|
$this->healthRecord = new PhabricatorClusterServiceHealthRecord(
|
|
$this->getHealthRecordCacheKey());
|
|
}
|
|
return $this->healthRecord;
|
|
}
|
|
|
|
public static function getActiveDatabaseRefs() {
|
|
$refs = array();
|
|
|
|
foreach (self::getMasterDatabaseRefs() as $ref) {
|
|
$refs[] = $ref;
|
|
}
|
|
|
|
foreach (self::getReplicaDatabaseRefs() as $ref) {
|
|
$refs[] = $ref;
|
|
}
|
|
|
|
return $refs;
|
|
}
|
|
|
|
public static function getAllMasterDatabaseRefs() {
|
|
$refs = self::getClusterRefs();
|
|
|
|
if (!$refs) {
|
|
return array(self::getLiveIndividualRef());
|
|
}
|
|
|
|
$masters = array();
|
|
foreach ($refs as $ref) {
|
|
if ($ref->getIsMaster()) {
|
|
$masters[] = $ref;
|
|
}
|
|
}
|
|
|
|
return $masters;
|
|
}
|
|
|
|
public static function getMasterDatabaseRefs() {
|
|
$refs = self::getAllMasterDatabaseRefs();
|
|
return self::getEnabledRefs($refs);
|
|
}
|
|
|
|
public function isApplicationHost($database) {
|
|
return isset($this->applicationMap[$database]);
|
|
}
|
|
|
|
public function loadRawMySQLConfigValue($key) {
|
|
$conn = $this->newManagementConnection();
|
|
|
|
try {
|
|
$value = queryfx_one($conn, 'SELECT @@%Q', $key);
|
|
$value = $value['@@'.$key];
|
|
} catch (AphrontQueryException $ex) {
|
|
$value = null;
|
|
}
|
|
|
|
return $value;
|
|
}
|
|
|
|
public static function getMasterDatabaseRefForApplication($application) {
|
|
$masters = self::getMasterDatabaseRefs();
|
|
|
|
$application_master = null;
|
|
$default_master = null;
|
|
foreach ($masters as $master) {
|
|
if ($master->isApplicationHost($application)) {
|
|
$application_master = $master;
|
|
break;
|
|
}
|
|
if ($master->getIsDefaultPartition()) {
|
|
$default_master = $master;
|
|
}
|
|
}
|
|
|
|
if ($application_master) {
|
|
$masters = array($application_master);
|
|
} else if ($default_master) {
|
|
$masters = array($default_master);
|
|
} else {
|
|
$masters = array();
|
|
}
|
|
|
|
$masters = self::getEnabledRefs($masters);
|
|
$master = head($masters);
|
|
|
|
return $master;
|
|
}
|
|
|
|
public static function newIndividualRef() {
|
|
$default_user = PhabricatorEnv::getEnvConfig('mysql.user');
|
|
$default_pass = new PhutilOpaqueEnvelope(
|
|
PhabricatorEnv::getEnvConfig('mysql.pass'));
|
|
$default_host = PhabricatorEnv::getEnvConfig('mysql.host');
|
|
$default_port = PhabricatorEnv::getEnvConfig('mysql.port');
|
|
|
|
return id(new self())
|
|
->setUser($default_user)
|
|
->setPass($default_pass)
|
|
->setHost($default_host)
|
|
->setPort($default_port)
|
|
->setIsIndividual(true)
|
|
->setIsMaster(true)
|
|
->setIsDefaultPartition(true)
|
|
->setUsePersistentConnections(false);
|
|
}
|
|
|
|
public static function getAllReplicaDatabaseRefs() {
|
|
$refs = self::getClusterRefs();
|
|
|
|
if (!$refs) {
|
|
return array();
|
|
}
|
|
|
|
$replicas = array();
|
|
foreach ($refs as $ref) {
|
|
if ($ref->getIsMaster()) {
|
|
continue;
|
|
}
|
|
|
|
$replicas[] = $ref;
|
|
}
|
|
|
|
return $replicas;
|
|
}
|
|
|
|
public static function getReplicaDatabaseRefs() {
|
|
$refs = self::getAllReplicaDatabaseRefs();
|
|
return self::getEnabledRefs($refs);
|
|
}
|
|
|
|
private static function getEnabledRefs(array $refs) {
|
|
foreach ($refs as $key => $ref) {
|
|
if ($ref->getDisabled()) {
|
|
unset($refs[$key]);
|
|
}
|
|
}
|
|
return $refs;
|
|
}
|
|
|
|
public static function getReplicaDatabaseRefForApplication($application) {
|
|
$replicas = self::getReplicaDatabaseRefs();
|
|
|
|
$application_replicas = array();
|
|
$default_replicas = array();
|
|
foreach ($replicas as $replica) {
|
|
$master = $replica->getMasterRef();
|
|
|
|
if ($master->isApplicationHost($application)) {
|
|
$application_replicas[] = $replica;
|
|
}
|
|
|
|
if ($master->getIsDefaultPartition()) {
|
|
$default_replicas[] = $replica;
|
|
}
|
|
}
|
|
|
|
if ($application_replicas) {
|
|
$replicas = $application_replicas;
|
|
} else {
|
|
$replicas = $default_replicas;
|
|
}
|
|
|
|
$replicas = self::getEnabledRefs($replicas);
|
|
|
|
// TODO: We may have multiple replicas to choose from, and could make
|
|
// more of an effort to pick the "best" one here instead of always
|
|
// picking the first one. Once we've picked one, we should try to use
|
|
// the same replica for the rest of the request, though.
|
|
|
|
return head($replicas);
|
|
}
|
|
|
|
private function newConnection(array $options) {
|
|
// If we believe the database is unhealthy, don't spend as much time
|
|
// trying to connect to it, since it's likely to continue to fail and
|
|
// hammering it can only make the problem worse.
|
|
$record = $this->getHealthRecord();
|
|
if ($record->getIsHealthy()) {
|
|
$default_retries = 3;
|
|
$default_timeout = 10;
|
|
} else {
|
|
$default_retries = 0;
|
|
$default_timeout = 2;
|
|
}
|
|
|
|
$spec = $options + array(
|
|
'user' => $this->getUser(),
|
|
'pass' => $this->getPass(),
|
|
'host' => $this->getHost(),
|
|
'port' => $this->getPort(),
|
|
'database' => null,
|
|
'retries' => $default_retries,
|
|
'timeout' => $default_timeout,
|
|
'persistent' => $this->getUsePersistentConnections(),
|
|
);
|
|
|
|
$is_cli = (php_sapi_name() == 'cli');
|
|
|
|
$use_persistent = false;
|
|
if (!empty($spec['persistent']) && !$is_cli) {
|
|
$use_persistent = true;
|
|
}
|
|
unset($spec['persistent']);
|
|
|
|
$connection = self::newRawConnection($spec);
|
|
|
|
// If configured, use persistent connections. See T11672 for details.
|
|
if ($use_persistent) {
|
|
$connection->setPersistent($use_persistent);
|
|
}
|
|
|
|
// Unless this is a script running from the CLI, prevent any query from
|
|
// running for more than 30 seconds. See T10849 for details.
|
|
if (!$is_cli) {
|
|
$connection->setQueryTimeout(30);
|
|
}
|
|
|
|
return $connection;
|
|
}
|
|
|
|
public static function newRawConnection(array $options) {
|
|
if (extension_loaded('mysqli')) {
|
|
return new AphrontMySQLiDatabaseConnection($options);
|
|
} else {
|
|
return new AphrontMySQLDatabaseConnection($options);
|
|
}
|
|
}
|
|
|
|
}
|