{"record":{"id":"a06374df40cc1468","repo":"phacility/phabricator","slug":"attempting-to-issue-a-write-query-on-a-read-only-c","errorCode":null,"errorMessage":"Attempting to issue a write query on a read-only connection (to database \"%s\")!","messagePattern":"Attempting to issue a write query on a read-only connection \\(to database \"(.+?)\"\\)!","errorType":"exception","errorClass":"Exception","httpStatus":null,"severity":"critical","filePath":"src/infrastructure/storage/connection/mysql/AphrontBaseMySQLDatabaseConnection.php","lineNumber":299,"sourceCode":"    } else if (is_bool($result)) {\n      return $this->getAffectedRows();\n    }\n    $rows = array();\n    while (($row = $this->fetchAssoc($result))) {\n      $rows[] = $row;\n    }\n    $this->freeResult($result);\n    return $rows;\n  }\n\n  protected function checkWrite($raw_query) {\n    // NOTE: The opening \"(\" allows queries in the form of:\n    //\n    //   (SELECT ...) UNION (SELECT ...)\n    $is_write = !preg_match('/^[(]*(SELECT|SHOW|EXPLAIN)\\s/', $raw_query);\n    if ($is_write) {\n      if ($this->getReadOnly()) {\n        throw new Exception(\n          pht(\n            'Attempting to issue a write query on a read-only '.\n            'connection (to database \"%s\")!',\n            $this->getConfiguration('database')));\n      }\n      AphrontWriteGuard::willWrite();\n      return true;\n    }\n\n    return false;\n  }\n\n  protected function throwQueryException($connection) {\n    if ($this->nextError) {\n      $errno = $this->nextError;\n      $error = pht('Simulated error.');\n      $this->nextError = null;\n    } else {","sourceCodeStart":281,"sourceCodeEnd":317,"githubUrl":"https://github.com/phacility/phabricator/blob/5720a38cfe95b00ca4be5016dd0d2f3195f4fa04/src/infrastructure/storage/connection/mysql/AphrontBaseMySQLDatabaseConnection.php#L281-L317","documentation":"Thrown by AphrontBaseMySQLDatabaseConnection::checkWrite() when a statement classified as a write (anything not starting with SELECT/SHOW/EXPLAIN, after optional leading parens) is issued on a connection opened in read-only mode. Phabricator marks replica connections read-only; this guard makes accidental writes to a replica loud and immediate instead of silently corrupting data. On writes to writable connections it also fires AphrontWriteGuard::willWrite() for write-safety checking.","triggerScenarios":"Issuing execute() with INSERT/UPDATE/DELETE/CREATE through a connection obtained from a replica (getManagementConnectionData / partition or cluster configuration giving a read-only connection); daemons or lisk objects that lazily write while iterating objects loaded from a read-only connection; a host in 'replica' role receiving writes during a cluster masterswitch.","commonSituations":"Misconfigured cluster.databases marking the primary as a replica; code paths reading from a replica and then saving a Lisk object on the same connection; during masterswitch/failover the writable role moves while a request still holds an old read-only connection.","solutions":["Route the write to the master/writable connection (open a new connection without read-only, or fix the cluster role configuration)","Check bin/config get cluster.databases / partition configuration: exactly one host must be writable","In application code, never save Lisk objects on connections used for replica reads — re-load/save via the default connection","During/after a masterswitch, let connections reconnect so roles refresh"],"exampleFix":"// before: writing on whatever connection is at hand\n$conn = $this->getAnyDatabaseConnection();\nqueryfx($conn, 'UPDATE %T SET status = %s WHERE id = %d', ...);\n\n// after: writes go to the writable master connection\n$conn_w = id(new PhabricatorUser())->establishConnection('w');\nqueryfx($conn_w, 'UPDATE %T SET status = %s WHERE id = %d', ...);","handlingStrategy":"validation","validationCode":"// Before writing, confirm the connection is writable.\nif ($conn->getReadOnly()) {\n  $conn = $lisk_object->establishConnection('w'); // master connection\n}\nqueryfx($conn, 'UPDATE ...');","typeGuard":null,"tryCatchPattern":"try {\n  queryfx($conn, $write_sql);\n} catch (Exception $ex) {\n  if (preg_match('/read-only connection/', $ex->getMessage())) {\n    $conn = $lisk_object->establishConnection('w');\n    queryfx($conn, $write_sql); // retry once on the writable connection\n  } else {\n    throw $ex;\n  }\n}","preventionTips":["Use Lisk's establishConnection('w') for writes and 'r' only for reads — never cache a replica connection for reuse as a writer","Keep cluster.databases roles correct: exactly one writable master at a time","Treat this exception as a configuration bug to fix, not a transient error to retry"],"tags":["database","mysql","replica","read-only","phabricator"],"backgroundTag":"read-only-database","analyzedSha":"5720a38cfe95b00ca4be5016dd0d2f3195f4fa04","analyzedAt":"2026-08-21T05:07:25.672Z","schemaVersion":2},"datasetVersion":"2026-08-21T11:28:35.574Z"}