Browse Source

Unify SQL timeouts

- Exec and lock timeouts now apply to MySQL
- Lock timeout now applies to PostgreSQL
- SQLite now uses a generic lock timeout setting which applies to all
J. King 2 months ago
parent
commit
8ea1df920a

+ 1
- 0
lib/AbstractException.php View File

@@ -65,6 +65,7 @@ abstract class AbstractException extends \Exception {
65 65
         "Conf/Exception.fileCorrupt"                  => 10306,
66 66
         "Conf/Exception.typeMismatch"                 => 10311,
67 67
         "Conf/Exception.semanticMismatch"             => 10312,
68
+        "Conf/Exception.ambiguousDefault"             => 10313,
68 69
         "User/Exception.functionNotImplemented"       => 10401,
69 70
         "User/Exception.doesNotExist"                 => 10402,
70 71
         "User/Exception.alreadyExists"                => 10403,

+ 25
- 13
lib/Conf.php View File

@@ -21,16 +21,16 @@ class Conf {
21 21
     public $dbDriver                = "sqlite3";
22 22
     /** @var boolean Whether to attempt to automatically update the database when upgrading to a new version with schema changes */
23 23
     public $dbAutoUpdate            = true;
24
-    /** @var \DateInterval Number of seconds to wait before returning a timeout error when connecting to a database (zero waits forever; not applicable to SQLite) */
24
+    /** @var \DateInterval|null Number of seconds to wait before returning a timeout error when connecting to a database (null waits forever; not applicable to SQLite) */
25 25
     public $dbTimeoutConnect        = 5.0;
26
-    /** @var \DateInterval Number of seconds to wait before returning a timeout error when executing a database operation (zero waits forever; not applicable to SQLite) */
27
-    public $dbTimeoutExec           = 0.0;
26
+    /** @var \DateInterval|null Number of seconds to wait before returning a timeout error when executing a database operation (null waits forever; not applicable to SQLite) */
27
+    public $dbTimeoutExec           = null;
28
+    /** @var \DateInterval|null Number of seconds to wait before returning a timeout error when acquiring a database lock (null waits forever) */
29
+    public $dbTimeoutLock           = 60.0;
28 30
     /** @var string|null Full path and file name of SQLite database (if using SQLite) */
29 31
     public $dbSQLite3File           = null;
30 32
     /** @var string Encryption key to use for SQLite database (if using a version of SQLite with SEE) */
31 33
     public $dbSQLite3Key            = "";
32
-    /** @var \DateInterval Number of seconds for SQLite to wait before returning a timeout error when trying to acquire a write lock on the database (zero does not wait) */
33
-    public $dbSQLite3Timeout        = 60.0;
34 34
     /** @var string Host name, address, or socket path of PostgreSQL database server (if using PostgreSQL) */
35 35
     public $dbPostgreSQLHost        = "";
36 36
     /** @var string Log-in user name for PostgreSQL database server (if using PostgreSQL) */
@@ -109,6 +109,11 @@ class Conf {
109 109
     /** @var string Space-separated list of origins from which to deny cross-origin resource sharing */
110 110
     public $httpOriginsDenied       = "";
111 111
 
112
+    ### OBSOLETE SETTINGS
113
+
114
+    /** @var \DateInterval|null (OBSOLETE) Number of seconds for SQLite to wait before returning a timeout error when trying to acquire a write lock on the database (zero does not wait) */
115
+    public $dbSQLite3Timeout        = null; // previously 60.0
116
+
112 117
     const TYPE_NAMES = [
113 118
         Value::T_BOOL     => "boolean",
114 119
         Value::T_STRING   => "string",
@@ -116,6 +121,12 @@ class Conf {
116 121
         VALUE::T_INT      => "integer",
117 122
         Value::T_INTERVAL => "interval",
118 123
     ];
124
+    const EXPECTED_TYPES = [
125
+        'dbTimeoutExec'    => "double",
126
+        'dbTimeoutLock'    => "double",
127
+        'dbTimeoutConnect' => "double",
128
+        'dbSQLite3Timeout' => "double",
129
+    ];
119 130
 
120 131
     protected static $types = [];
121 132
 
@@ -261,26 +272,28 @@ class Conf {
261 272
     }
262 273
 
263 274
     protected function propertyImport(string $key, $value, string $file = "") {
275
+        $typeName = static::$types[$key]['name'] ?? "mixed";
276
+        $typeConst = static::$types[$key]['const'] ?? Value::T_MIXED;
277
+        $nullable = (int) (bool) (static::$types[$key]['const'] & Value::M_NULL);
264 278
         try {
265
-            $typeName = static::$types[$key]['name'] ?? "mixed";
266
-            $typeConst = static::$types[$key]['const'] ?? Value::T_MIXED;
267 279
             if ($typeName === "\\DateInterval") {
268 280
                 // date intervals have special handling: if the existing value (ultimately, the default value)
269 281
                 // is an integer or float, the new value should be imported as numeric. If the new value is a string
270 282
                 // it is first converted to an interval and then converted to the numeric type if necessary
283
+                $mode = $nullable ? Value::M_STRICT | Value::M_NULL : Value::M_STRICT;
271 284
                 if (is_string($value)) {
272
-                    $value =  Value::normalize($value, Value::T_INTERVAL | Value::M_STRICT);
285
+                    $value =  Value::normalize($value, Value::T_INTERVAL | $mode);
273 286
                 }
274
-                switch (gettype($this->$key)) {
287
+                switch (self::EXPECTED_TYPES[$key] ?? gettype($this->$key)) {
275 288
                     case "integer":
276
-                        return Value::normalize($value, Value::T_INT | Value::M_STRICT);
289
+                        return Value::normalize($value, Value::T_INT | $mode);
277 290
                     case "double":
278
-                        return Value::normalize($value, Value::T_FLOAT | Value::M_STRICT);
291
+                        return Value::normalize($value, Value::T_FLOAT | $mode);
279 292
                     case "string":
280 293
                     case "object":
281 294
                         return $value;
282 295
                     default:
283
-                        throw new ExceptionType("strictFailure"); // @codeCoverageIgnore
296
+                        throw new Conf\Exception("ambiguousDefault", ['param' => $key]); // @codeCoverageIgnore
284 297
                 }
285 298
             }
286 299
             $value =  Value::normalize($value, $typeConst);
@@ -303,7 +316,6 @@ class Conf {
303 316
             }
304 317
             return $value;
305 318
         } catch (ExceptionType $e) {
306
-            $nullable = (int) (bool) (static::$types[$key] & Value::M_NULL);
307 319
             $type =  static::$types[$key]['const'] & ~(Value::M_STRICT | Value::M_DROP | Value::M_NULL | Value::M_ARRAY);
308 320
             throw new Conf\Exception("typeMismatch", ['param' => $key, 'type' => self::TYPE_NAMES[$type], 'file' => $file, 'nullable' => $nullable]);
309 321
         }

+ 11
- 4
lib/Db/MySQL/Driver.php View File

@@ -18,7 +18,7 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver {
18 18
     const SQL_MODE = "ANSI_QUOTES,HIGH_NOT_PRECEDENCE,NO_BACKSLASH_ESCAPES,NO_ENGINE_SUBSTITUTION,PIPES_AS_CONCAT,STRICT_ALL_TABLES";
19 19
     const TRANSACTIONAL_LOCKS = false;
20 20
 
21
-    /** @var \mysql */
21
+    /** @var \mysqli */
22 22
     protected $db;
23 23
     protected $transStart = 0;
24 24
     protected $packetSize = 4194304;
@@ -48,7 +48,8 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver {
48 48
         return [
49 49
             "SET sql_mode = '".self::SQL_MODE."'",
50 50
             "SET time_zone = '+00:00'",
51
-            "SET lock_wait_timeout = 1",
51
+            "SET lock_wait_timeout = ".self::lockTimeout(),
52
+            "SET max_execution_time = ".ceil(Arsse::$conf->dbTimeoutExec * 1000),
52 53
         ];
53 54
     }
54 55
 
@@ -130,7 +131,7 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver {
130 131
             try {
131 132
                 $this->exec("SET lock_wait_timeout = 1; LOCK TABLES $tables");
132 133
             } finally {
133
-                $this->exec("SET lock_wait_timeout = 60");
134
+                $this->exec("SET lock_wait_timeout = ".self::lockTimeout());
134 135
             }
135 136
         }
136 137
         return true;
@@ -141,6 +142,10 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver {
141 142
         return true;
142 143
     }
143 144
 
145
+    protected static function lockTimeout(): int {
146
+        return (int) max(min(ceil(Arsse::$conf->dbTimeoutLock ?? 31536000), 31536000), 1);
147
+    }
148
+
144 149
     public function __destruct() {
145 150
         if (isset($this->db)) {
146 151
             $this->db->close();
@@ -157,7 +162,9 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver {
157 162
     }
158 163
 
159 164
     protected function makeConnection(string $db, string $user, string $password, string $host, int $port, string $socket) {
160
-        $this->db = @new \mysqli($host, $user, $password, $db, $port, $socket);
165
+        $this->db = mysqli_init();
166
+        $this->db->options(\MYSQLI_OPT_CONNECT_TIMEOUT, ceil(Arsse::$conf->dbTimeoutConnect));
167
+        @$this->db->real_connect($host, $user, $password, $db, $port, $socket);
161 168
         if ($this->db->connect_errno) {
162 169
             list($excClass, $excMsg, $excData) = $this->buildConnectionException($this->db->connect_errno, $this->db->connect_error);
163 170
             throw new $excClass($excMsg, $excData);

+ 4
- 2
lib/Db/PostgreSQL/Driver.php View File

@@ -74,11 +74,13 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver {
74 74
     }
75 75
 
76 76
     public static function makeSetupQueries(string $schema = ""): array {
77
-        $timeout = ceil(Arsse::$conf->dbTimeoutExec * 1000);
77
+        $timeExec = is_null(Arsse::$conf->dbTimeoutExec) ? 0 : ceil(max(Arsse::$conf->dbTimeoutExec * 1000, 1));
78
+        $timeLock = is_null(Arsse::$conf->dbTimeoutLock) ? 0 : ceil(max(Arsse::$conf->dbTimeoutLock * 1000, 1));
78 79
         $out = [
79 80
             "SET TIME ZONE UTC",
80 81
             "SET DateStyle = 'ISO, MDY'",
81
-            "SET statement_timeout = '$timeout'",
82
+            "SET statement_timeout = '$timeExec'",
83
+            "SET lock_timeout = '$timeLock'",
82 84
         ];
83 85
         if (strlen($schema) > 0) {
84 86
             $schema = '"'.str_replace('"', '""', $schema).'"';

+ 2
- 1
lib/Db/SQLite3/Driver.php View File

@@ -55,7 +55,8 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver {
55 55
             throw new Exception("fileCorrupt", $dbFile);
56 56
         }
57 57
         // set the timeout
58
-        $timeout = (int) ceil(Arsse::$conf->dbSQLite3Timeout * 1000);
58
+        $timeout = Arsse::$conf->dbSQLite3Timeout ?? Arsse::$conf->dbTimeoutLock; // old SQLite-specific timeout takes precedence
59
+        $timeout = is_null($timeout) ? PHP_INT_MAX : (int) ceil($timeout * 1000);
59 60
         $this->setTimeout($timeout);
60 61
         // set other initial options
61 62
         $this->exec("PRAGMA foreign_keys = yes");

+ 2
- 0
locale/en.php View File

@@ -74,6 +74,8 @@ return [
74 74
             other {, or null}
75 75
         }',
76 76
     'Exception.JKingWeb/Arsse/Conf/Exception.semanticMismatch'             => 'Configuration parameter "{param}" in file "{file}" is not a valid value. Consult the documentation for possible values',
77
+    // indicates programming error
78
+    'Exception.JKingWeb/Arsse/Conf/Exception.ambiguousDefault'             => 'Preferred type of configuration parameter "{param}" could not be inferred from its default value. The parameter must be added to the Conf::EXPECTED_TYPES array',
77 79
     'Exception.JKingWeb/Arsse/Db/Exception.extMissing'                     => 'Required PHP extension for driver "{0}" not installed',
78 80
     'Exception.JKingWeb/Arsse/Db/Exception.fileMissing'                    => 'Database file "{0}" does not exist',
79 81
     'Exception.JKingWeb/Arsse/Db/Exception.fileUnreadable'                 => 'Insufficient permissions to open database file "{0}" for reading',

+ 1
- 0
tests/cases/Db/BaseDriver.php View File

@@ -19,6 +19,7 @@ abstract class BaseDriver extends \JKingWeb\Arsse\Test\AbstractTest {
19 19
     protected $setVersion;
20 20
     protected static $conf = [
21 21
         'dbTimeoutExec' => 0.5,
22
+        'dbTimeoutLock' => 0.001,
22 23
         'dbSQLite3Timeout' => 0,
23 24
       //'dbSQLite3File' => "(temporary file)",
24 25
     ];

Loading…
Cancel
Save