Page Menu
Home
c4science
Search
Configure Global Search
Log In
Files
F90796551
PhabricatorDatabaseRef.php
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Subscribers
None
File Metadata
Details
File Info
Storage
Attached
Created
Mon, Nov 4, 20:40
Size
18 KB
Mime Type
text/x-php
Expires
Wed, Nov 6, 20:40 (2 d)
Engine
blob
Format
Raw Data
Handle
22116026
Attached To
rPH Phabricator
PhabricatorDatabaseRef.php
View Options
<?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
);
}
}
}
Event Timeline
Log In to Comment