protected function getShardConnection($shardID, array $options = []) { if (!is_numeric($shardID)) { throw new Exception('$shardID must be an integer'); } $isWriteQuery = !empty($options['isWriteQuery']); $lastLinkFailed = !empty($options['lastLinkFailed']); $writeInReadMode = !empty($options['writeInReadMode']); // TEMP if (get_called_class() == 'Zotero_FullText_DB') { $linkID = "FT" . $shardID; } else { $linkID = $shardID; } if ($this->isReadOnly($shardID) && $isWriteQuery && !$writeInReadMode) { throw new Exception("Cannot get link for writing to shard {$shardID} in read-only mode"); } // Master queries always use the cached link that was created at class construction if ($shardID == 0) { //error_log($this->connections[$linkID]->link->getConnection()->host_info); return $this->connections[$linkID]; } // For read-only mode and read queries, use a cached link if available. Since this is // done before checking the latest shard info, it's possible for subsequent read queries // in a request to go through even if the shard was since disabled, but that's generally // not a big deal, and new requests will check the shard info again and throw. // // Read-only mode if ($this->isReadOnly($shardID) && !$writeInReadMode) { // Use a cached replica link if available. if (isset($this->replicaConnections[$linkID])) { // If the last link failed, try the next one. If no more, that's fatal. if ($lastLinkFailed) { $lastHost = $this->replicaConnections[$linkID][0]->host; if (sizeOf($this->replicaConnections[$linkID]) == 1) { throw new Exception("Read failed from replica {$lastHost} -- no more replica connections", Z_ERROR_SHARD_UNAVAILABLE); } error_log("WARNING: Read failed from replica {$lastHost} -- retrying on another replica"); array_shift($this->replicaConnections[$linkID]); } //error_log($this->replicaConnections[$linkID][0]->link->getConnection()->host_info); return $this->replicaConnections[$linkID][0]; } } else { if (!$isWriteQuery && isset($this->connections[$linkID])) { //error_log($this->connections[$linkID]->link->getConnection()->host_info); return $this->connections[$linkID]; } } $shardInfo = Zotero_Shards::getShardInfo($shardID); if (!$shardInfo) { throw new Exception("Invalid shard {$shardID}"); } if ($shardInfo['state'] == 'down') { throw new Exception("Shard {$shardID} is down", Z_ERROR_SHARD_UNAVAILABLE); } else { if ($shardInfo['state'] == 'readonly') { if ($isWriteQuery && get_called_class() != 'Zotero_Admin_DB') { throw new Exception("Cannot write to read-only shard {$shardID}", Z_ERROR_SHARD_READ_ONLY); } } } if ($this->isReadOnly($shardID) && !$writeInReadMode) { $replicas = Zotero_Shards::getReplicaInfo($shardInfo['shardHostID']); if ($replicas) { $writerCacheKey = 'shardHostReplicasWriter_' . $shardInfo['shardHostID']; $writerAddress = Z_Core::$MC->get($writerCacheKey); $writerConn = null; // Randomize replica order shuffle($replicas); foreach ($replicas as $replica) { $info = $shardInfo; $info['address'] = $replica['address']; $info['port'] = $replica['port']; $conn = new Zotero_DB_Connection(); $conn->host = $info['address']; $conn->shardID = $linkID; $conn->link = $this->getLinkFromConnectionInfo($info); if ($replica['address'] == $writerAddress) { $writerConn = $conn; } else { $this->replicaConnections[$linkID][] = $conn; } } // If we know the writer, add it last, so that all read replicas are tried first if ($writerConn) { $this->replicaConnections[$linkID][] = $writerConn; } else { try { $results = $this->replicaConnections[$linkID][0]->link->query("SHOW GLOBAL VARIABLES LIKE 'innodb_read_only'"); $row = $results->fetch(Zend_Db::FETCH_ASSOC); $connReadOnly = $row ? $row['Value'] : false; // If we found the writer if ($connReadOnly == "OFF") { // Store host in memcached so that every request doesn't need to check // the variables to sort the writer last. // // This can probably be increased, because the only consequence of not // knowing the writer is that a few requests could use the writer // for reads. Z_Core::$MC->set($writerCacheKey, $this->replicaConnections[$linkID][0]->host, 60); // If more than one connection, move writer connection to end and // close it if (sizeOf($this->replicaConnections[$linkID]) > 1) { $conn = array_shift($this->replicaConnections[$linkID]); $this->replicaConnections[$linkID][] = $conn; $conn->link->closeConnection(); } } } catch (Exception $e) { error_log("WARNING: Failed checking replica state: {$e}"); } } //error_log($this->replicaConnections[$linkID][0]->link->getConnection()->host_info); return $this->replicaConnections[$linkID][0]; } } // Host isn't read-only or down, so write queries can use a cached link if available. // Otherwise, make a new link. if (!isset($this->connections[$linkID])) { $conn = new Zotero_DB_Connection(); $conn->host = $shardInfo['address']; $conn->shardID = $linkID; $conn->link = $this->getLinkFromConnectionInfo($shardInfo); $this->connections[$linkID] = $conn; } //error_log($this->connections[$linkID]->link->getConnection()->host_info); return $this->connections[$linkID]; }