├── LICENSE
├── README.md
├── composer.json
├── ludicrousdb.php
├── ludicrousdb
├── drop-ins
│ ├── db-config.php
│ ├── db-error.php
│ └── db.php
└── includes
│ ├── class-ludicrousdb.php
│ └── functions.php
└── readme.txt
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU GENERAL PUBLIC LICENSE
2 | Version 2, June 1991
3 |
4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
6 | Everyone is permitted to copy and distribute verbatim copies
7 | of this license document, but changing it is not allowed.
8 |
9 | Preamble
10 |
11 | The licenses for most software are designed to take away your
12 | freedom to share and change it. By contrast, the GNU General Public
13 | License is intended to guarantee your freedom to share and change free
14 | software--to make sure the software is free for all its users. This
15 | General Public License applies to most of the Free Software
16 | Foundation's software and to any other program whose authors commit to
17 | using it. (Some other Free Software Foundation software is covered by
18 | the GNU Lesser General Public License instead.) You can apply it to
19 | your programs, too.
20 |
21 | When we speak of free software, we are referring to freedom, not
22 | price. Our General Public Licenses are designed to make sure that you
23 | have the freedom to distribute copies of free software (and charge for
24 | this service if you wish), that you receive source code or can get it
25 | if you want it, that you can change the software or use pieces of it
26 | in new free programs; and that you know you can do these things.
27 |
28 | To protect your rights, we need to make restrictions that forbid
29 | anyone to deny you these rights or to ask you to surrender the rights.
30 | These restrictions translate to certain responsibilities for you if you
31 | distribute copies of the software, or if you modify it.
32 |
33 | For example, if you distribute copies of such a program, whether
34 | gratis or for a fee, you must give the recipients all the rights that
35 | you have. You must make sure that they, too, receive or can get the
36 | source code. And you must show them these terms so they know their
37 | rights.
38 |
39 | We protect your rights with two steps: (1) copyright the software, and
40 | (2) offer you this license which gives you legal permission to copy,
41 | distribute and/or modify the software.
42 |
43 | Also, for each author's protection and ours, we want to make certain
44 | that everyone understands that there is no warranty for this free
45 | software. If the software is modified by someone else and passed on, we
46 | want its recipients to know that what they have is not the original, so
47 | that any problems introduced by others will not reflect on the original
48 | authors' reputations.
49 |
50 | Finally, any free program is threatened constantly by software
51 | patents. We wish to avoid the danger that redistributors of a free
52 | program will individually obtain patent licenses, in effect making the
53 | program proprietary. To prevent this, we have made it clear that any
54 | patent must be licensed for everyone's free use or not licensed at all.
55 |
56 | The precise terms and conditions for copying, distribution and
57 | modification follow.
58 |
59 | GNU GENERAL PUBLIC LICENSE
60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
61 |
62 | 0. This License applies to any program or other work which contains
63 | a notice placed by the copyright holder saying it may be distributed
64 | under the terms of this General Public License. The "Program", below,
65 | refers to any such program or work, and a "work based on the Program"
66 | means either the Program or any derivative work under copyright law:
67 | that is to say, a work containing the Program or a portion of it,
68 | either verbatim or with modifications and/or translated into another
69 | language. (Hereinafter, translation is included without limitation in
70 | the term "modification".) Each licensee is addressed as "you".
71 |
72 | Activities other than copying, distribution and modification are not
73 | covered by this License; they are outside its scope. The act of
74 | running the Program is not restricted, and the output from the Program
75 | is covered only if its contents constitute a work based on the
76 | Program (independent of having been made by running the Program).
77 | Whether that is true depends on what the Program does.
78 |
79 | 1. You may copy and distribute verbatim copies of the Program's
80 | source code as you receive it, in any medium, provided that you
81 | conspicuously and appropriately publish on each copy an appropriate
82 | copyright notice and disclaimer of warranty; keep intact all the
83 | notices that refer to this License and to the absence of any warranty;
84 | and give any other recipients of the Program a copy of this License
85 | along with the Program.
86 |
87 | You may charge a fee for the physical act of transferring a copy, and
88 | you may at your option offer warranty protection in exchange for a fee.
89 |
90 | 2. You may modify your copy or copies of the Program or any portion
91 | of it, thus forming a work based on the Program, and copy and
92 | distribute such modifications or work under the terms of Section 1
93 | above, provided that you also meet all of these conditions:
94 |
95 | a) You must cause the modified files to carry prominent notices
96 | stating that you changed the files and the date of any change.
97 |
98 | b) You must cause any work that you distribute or publish, that in
99 | whole or in part contains or is derived from the Program or any
100 | part thereof, to be licensed as a whole at no charge to all third
101 | parties under the terms of this License.
102 |
103 | c) If the modified program normally reads commands interactively
104 | when run, you must cause it, when started running for such
105 | interactive use in the most ordinary way, to print or display an
106 | announcement including an appropriate copyright notice and a
107 | notice that there is no warranty (or else, saying that you provide
108 | a warranty) and that users may redistribute the program under
109 | these conditions, and telling the user how to view a copy of this
110 | License. (Exception: if the Program itself is interactive but
111 | does not normally print such an announcement, your work based on
112 | the Program is not required to print an announcement.)
113 |
114 | These requirements apply to the modified work as a whole. If
115 | identifiable sections of that work are not derived from the Program,
116 | and can be reasonably considered independent and separate works in
117 | themselves, then this License, and its terms, do not apply to those
118 | sections when you distribute them as separate works. But when you
119 | distribute the same sections as part of a whole which is a work based
120 | on the Program, the distribution of the whole must be on the terms of
121 | this License, whose permissions for other licensees extend to the
122 | entire whole, and thus to each and every part regardless of who wrote it.
123 |
124 | Thus, it is not the intent of this section to claim rights or contest
125 | your rights to work written entirely by you; rather, the intent is to
126 | exercise the right to control the distribution of derivative or
127 | collective works based on the Program.
128 |
129 | In addition, mere aggregation of another work not based on the Program
130 | with the Program (or with a work based on the Program) on a volume of
131 | a storage or distribution medium does not bring the other work under
132 | the scope of this License.
133 |
134 | 3. You may copy and distribute the Program (or a work based on it,
135 | under Section 2) in object code or executable form under the terms of
136 | Sections 1 and 2 above provided that you also do one of the following:
137 |
138 | a) Accompany it with the complete corresponding machine-readable
139 | source code, which must be distributed under the terms of Sections
140 | 1 and 2 above on a medium customarily used for software interchange; or,
141 |
142 | b) Accompany it with a written offer, valid for at least three
143 | years, to give any third party, for a charge no more than your
144 | cost of physically performing source distribution, a complete
145 | machine-readable copy of the corresponding source code, to be
146 | distributed under the terms of Sections 1 and 2 above on a medium
147 | customarily used for software interchange; or,
148 |
149 | c) Accompany it with the information you received as to the offer
150 | to distribute corresponding source code. (This alternative is
151 | allowed only for noncommercial distribution and only if you
152 | received the program in object code or executable form with such
153 | an offer, in accord with Subsection b above.)
154 |
155 | The source code for a work means the preferred form of the work for
156 | making modifications to it. For an executable work, complete source
157 | code means all the source code for all modules it contains, plus any
158 | associated interface definition files, plus the scripts used to
159 | control compilation and installation of the executable. However, as a
160 | special exception, the source code distributed need not include
161 | anything that is normally distributed (in either source or binary
162 | form) with the major components (compiler, kernel, and so on) of the
163 | operating system on which the executable runs, unless that component
164 | itself accompanies the executable.
165 |
166 | If distribution of executable or object code is made by offering
167 | access to copy from a designated place, then offering equivalent
168 | access to copy the source code from the same place counts as
169 | distribution of the source code, even though third parties are not
170 | compelled to copy the source along with the object code.
171 |
172 | 4. You may not copy, modify, sublicense, or distribute the Program
173 | except as expressly provided under this License. Any attempt
174 | otherwise to copy, modify, sublicense or distribute the Program is
175 | void, and will automatically terminate your rights under this License.
176 | However, parties who have received copies, or rights, from you under
177 | this License will not have their licenses terminated so long as such
178 | parties remain in full compliance.
179 |
180 | 5. You are not required to accept this License, since you have not
181 | signed it. However, nothing else grants you permission to modify or
182 | distribute the Program or its derivative works. These actions are
183 | prohibited by law if you do not accept this License. Therefore, by
184 | modifying or distributing the Program (or any work based on the
185 | Program), you indicate your acceptance of this License to do so, and
186 | all its terms and conditions for copying, distributing or modifying
187 | the Program or works based on it.
188 |
189 | 6. Each time you redistribute the Program (or any work based on the
190 | Program), the recipient automatically receives a license from the
191 | original licensor to copy, distribute or modify the Program subject to
192 | these terms and conditions. You may not impose any further
193 | restrictions on the recipients' exercise of the rights granted herein.
194 | You are not responsible for enforcing compliance by third parties to
195 | this License.
196 |
197 | 7. If, as a consequence of a court judgment or allegation of patent
198 | infringement or for any other reason (not limited to patent issues),
199 | conditions are imposed on you (whether by court order, agreement or
200 | otherwise) that contradict the conditions of this License, they do not
201 | excuse you from the conditions of this License. If you cannot
202 | distribute so as to satisfy simultaneously your obligations under this
203 | License and any other pertinent obligations, then as a consequence you
204 | may not distribute the Program at all. For example, if a patent
205 | license would not permit royalty-free redistribution of the Program by
206 | all those who receive copies directly or indirectly through you, then
207 | the only way you could satisfy both it and this License would be to
208 | refrain entirely from distribution of the Program.
209 |
210 | If any portion of this section is held invalid or unenforceable under
211 | any particular circumstance, the balance of the section is intended to
212 | apply and the section as a whole is intended to apply in other
213 | circumstances.
214 |
215 | It is not the purpose of this section to induce you to infringe any
216 | patents or other property right claims or to contest validity of any
217 | such claims; this section has the sole purpose of protecting the
218 | integrity of the free software distribution system, which is
219 | implemented by public license practices. Many people have made
220 | generous contributions to the wide range of software distributed
221 | through that system in reliance on consistent application of that
222 | system; it is up to the author/donor to decide if he or she is willing
223 | to distribute software through any other system and a licensee cannot
224 | impose that choice.
225 |
226 | This section is intended to make thoroughly clear what is believed to
227 | be a consequence of the rest of this License.
228 |
229 | 8. If the distribution and/or use of the Program is restricted in
230 | certain countries either by patents or by copyrighted interfaces, the
231 | original copyright holder who places the Program under this License
232 | may add an explicit geographical distribution limitation excluding
233 | those countries, so that distribution is permitted only in or among
234 | countries not thus excluded. In such case, this License incorporates
235 | the limitation as if written in the body of this License.
236 |
237 | 9. The Free Software Foundation may publish revised and/or new versions
238 | of the General Public License from time to time. Such new versions will
239 | be similar in spirit to the present version, but may differ in detail to
240 | address new problems or concerns.
241 |
242 | Each version is given a distinguishing version number. If the Program
243 | specifies a version number of this License which applies to it and "any
244 | later version", you have the option of following the terms and conditions
245 | either of that version or of any later version published by the Free
246 | Software Foundation. If the Program does not specify a version number of
247 | this License, you may choose any version ever published by the Free Software
248 | Foundation.
249 |
250 | 10. If you wish to incorporate parts of the Program into other free
251 | programs whose distribution conditions are different, write to the author
252 | to ask for permission. For software which is copyrighted by the Free
253 | Software Foundation, write to the Free Software Foundation; we sometimes
254 | make exceptions for this. Our decision will be guided by the two goals
255 | of preserving the free status of all derivatives of our free software and
256 | of promoting the sharing and reuse of software generally.
257 |
258 | NO WARRANTY
259 |
260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
268 | REPAIR OR CORRECTION.
269 |
270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
278 | POSSIBILITY OF SUCH DAMAGES.
279 |
280 | END OF TERMS AND CONDITIONS
281 |
282 | How to Apply These Terms to Your New Programs
283 |
284 | If you develop a new program, and you want it to be of the greatest
285 | possible use to the public, the best way to achieve this is to make it
286 | free software which everyone can redistribute and change under these terms.
287 |
288 | To do so, attach the following notices to the program. It is safest
289 | to attach them to the start of each source file to most effectively
290 | convey the exclusion of warranty; and each file should have at least
291 | the "copyright" line and a pointer to where the full notice is found.
292 |
293 | {description}
294 | Copyright (C) {year} {fullname}
295 |
296 | This program is free software; you can redistribute it and/or modify
297 | it under the terms of the GNU General Public License as published by
298 | the Free Software Foundation; either version 2 of the License, or
299 | (at your option) any later version.
300 |
301 | This program is distributed in the hope that it will be useful,
302 | but WITHOUT ANY WARRANTY; without even the implied warranty of
303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
304 | GNU General Public License for more details.
305 |
306 | You should have received a copy of the GNU General Public License along
307 | with this program; if not, write to the Free Software Foundation, Inc.,
308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
309 |
310 | Also add information on how to contact you by electronic and paper mail.
311 |
312 | If the program is interactive, make it output a short notice like this
313 | when it starts in an interactive mode:
314 |
315 | Gnomovision version 69, Copyright (C) year name of author
316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
317 | This is free software, and you are welcome to redistribute it
318 | under certain conditions; type `show c' for details.
319 |
320 | The hypothetical commands `show w' and `show c' should show the appropriate
321 | parts of the General Public License. Of course, the commands you use may
322 | be called something other than `show w' and `show c'; they could even be
323 | mouse-clicks or menu items--whatever suits your program.
324 |
325 | You should also get your employer (if you work as a programmer) or your
326 | school, if any, to sign a "copyright disclaimer" for the program, if
327 | necessary. Here is a sample; alter the names:
328 |
329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program
330 | `Gnomovision' (which makes passes at compilers) written by James Hacker.
331 |
332 | {signature of Ty Coon}, 1 April 1989
333 | Ty Coon, President of Vice
334 |
335 | This General Public License does not permit incorporating your program into
336 | proprietary programs. If your program is a subroutine library, you may
337 | consider it more useful to permit linking proprietary applications with the
338 | library. If this is what you want to do, use the GNU Lesser General
339 | Public License instead of this License.
340 |
341 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # LudicrousDB
2 |
3 | LudicrousDB is an advanced database interface for WordPress that supports replication, fail-over, load balancing, and partitioning, based on Automattic's HyperDB drop-in.
4 |
5 | ## 0. Installation
6 |
7 | ### Files
8 |
9 | Copy the main `ludicrousdb` plugin folder & its contents to either:
10 |
11 | * `wp-content/plugins/ludicrousdb/`
12 | * `wp-content/mu-plugins/ludicrousdb/`
13 |
14 | It does not matter which one; LudicrousDB will figure it out. The folder name should be exactly `ludicrousdb`. Be careful when you do "Download ZIP" from github and unzip.
15 |
16 | ### Drop-ins
17 |
18 | WordPress supports a few "drop-in" style plugins, used for advanced overriding of a few specific pieces of functionality.
19 |
20 | LudicrousDB includes 3 basic database drop-ins:
21 |
22 | * `db.php` <-> `wp-content/db.php` - Bootstrap for replacement `$wpdb` object
23 | * `db-error.php` <-> `wp-content/db-error.php` - Endpoint for fatal database error output to users
24 | * `db-config.php` <-> `ABSPATH/db-config.php` - For configuring your database environment
25 |
26 | You'll probably want to copy these files to their respective locations, and modify them once you're comfortable with what they do and how they work.
27 |
28 | ## 1. Configuration
29 |
30 | LudicrousDB can manage connections to a large number of databases. Queries are distributed to appropriate servers by mapping table names to datasets.
31 |
32 | A dataset is defined as a group of tables that are located in the same database. There may be similarly-named databases containing different tables on different servers. There may also be many replicas of a database on different servers. The term "dataset" removes any ambiguity. Consider a dataset as a group of tables that can be mirrored on many servers.
33 |
34 | Configuring LudicrousDB involves defining databases and datasets. Defining a database involves specifying the server connection details, the dataset it contains, and its capabilities and priorities for reading and writing. Defining a dataset involves specifying its exact table names or registering one or more callback functions that translate table names to datasets.
35 |
36 | ### Sample Configuration 1: Default Server
37 |
38 | This is the most basic way to add a server to LudicrousDB using only the required parameters: host, user, password, name. This adds the DB defined in wp-config.php as a read/write server for the 'global' dataset. (Every table is in 'global' by default.)
39 |
40 | ```php
41 | $wpdb->add_database( array(
42 | 'host' => DB_HOST, // If port is other than 3306, use host:port.
43 | 'user' => DB_USER,
44 | 'password' => DB_PASSWORD,
45 | 'name' => DB_NAME,
46 | ) );
47 | ```
48 |
49 | This adds the same server again, only this time it is configured as a replica. The last three parameters are set to the defaults but are shown for clarity.
50 |
51 | ```php
52 | $wpdb->add_database( array(
53 | 'host' => DB_HOST, // If port is other than 3306, use host:port.
54 | 'user' => DB_USER,
55 | 'password' => DB_PASSWORD,
56 | 'name' => DB_NAME,
57 | 'write' => 0,
58 | 'read' => 1,
59 | 'dataset' => 'global',
60 | 'timeout' => 0.2,
61 | ) );
62 | ```
63 |
64 | ### Sample Configuration 2: Partitioning
65 |
66 | This example shows a setup where the multisite blog tables have been separated from the global dataset.
67 |
68 | ```php
69 | $wpdb->add_database( array(
70 | 'host' => 'global.db.example.com',
71 | 'user' => 'globaluser',
72 | 'password' => 'globalpassword',
73 | 'name' => 'globaldb',
74 | ) );
75 |
76 | $wpdb->add_database( array(
77 | 'host' => 'blog.db.example.com',
78 | 'user' => 'bloguser',
79 | 'password' => 'blogpassword',
80 | 'name' => 'blogdb',
81 | 'dataset' => 'blog',
82 | ) );
83 |
84 | $wpdb->add_callback( 'my_db_callback' );
85 |
86 | // Multisite blog tables are "{$base_prefix}{$blog_id}_*"
87 | function my_db_callback( $query, $wpdb ) {
88 | if ( preg_match("/^{$wpdb->base_prefix}\d+_/i", $wpdb->table) ) {
89 | return 'blog';
90 | }
91 | }
92 | ```
93 |
94 | ### Configuration Functions
95 |
96 | #### add_database()
97 |
98 | ```php
99 | $wpdb->add_database( $database );
100 | ```
101 |
102 | `$database` is an associative array with these parameters:
103 |
104 | ```php
105 | host (required) Hostname with optional :port. Default port is 3306.
106 | user (required) MySQL user name.
107 | password (required) MySQL user password.
108 | name (required) MySQL database name.
109 | read (optional) Whether server is readable. Default is 1 (readable).
110 | Also used to assign preference. See "Network topology".
111 | write (optional) Whether server is writable. Default is 1 (writable).
112 | Also used to assign preference in multi-primary mode.
113 | dataset (optional) Name of dataset. Default is 'global'.
114 | timeout (optional) Seconds to wait for TCP responsiveness. Default is 0.2
115 | lag_threshold (optional) The minimum lag on a replica in seconds before we consider it lagged.
116 | Set null to disable. When not set, the value of $wpdb->default_lag_threshold is used.
117 | ```
118 |
119 | #### add_table()
120 |
121 | ```php
122 | $wpdb->add_table( $dataset, $table );
123 | ```
124 |
125 | `$dataset` and `$table` are strings.
126 |
127 | #### add_callback()
128 |
129 | ```php
130 | $wpdb->add_callback( $callback, $callback_group = 'dataset' );
131 | ```
132 |
133 | `$callback` is a callable function or method. `$callback_group` is the group of callbacks, this `$callback` belongs to.
134 |
135 | Callbacks are executed in the order in which they are registered until one of them returns something other than null.
136 |
137 | The default `$callback_group` is 'dataset'. Callback in this group will be called with two arguments and expected to compute a dataset or return null.
138 |
139 | ```php
140 | $dataset = $callback($table, &$wpdb);
141 | ```
142 |
143 | Anything evaluating to false will cause the query to be aborted.
144 |
145 | For more complex setups, the callback may be used to overwrite properties of `$wpdb` or variables within `LudicrousDB::connect_db()`. If a callback returns an array, LudicrousDB will extract the array. It should be an associative array and it should include a `$dataset` value corresponding to a database added with `$wpdb->add_database()`. It may also include `$server`, which will be extracted to overwrite the parameters of each randomly selected database server prior to connection. This allows you to dynamically vary parameters such as the host, user, password, database name, lag_threshold and TCP check timeout.
146 |
147 | ## 2. Primary & Replica Databases
148 |
149 | A database definition can include 'read' and 'write' parameters. These operate as boolean switches but they are typically specified as integers. They allow or disallow use of the database for reading or writing.
150 |
151 | A primary database might be configured to allow reading and writing:
152 |
153 | ```php
154 | 'write' => 1,
155 | 'read' => 1,
156 | ```
157 |
158 | while a replica would be allowed only to read:
159 |
160 | ```php
161 | 'write' => 0,
162 | 'read' => 1,
163 | ```
164 |
165 | It might be advantageous to disallow reading from the primary, such as when there are many replicas available and the primary is very busy with writes.
166 |
167 | ```php
168 | 'write' => 1,
169 | 'read' => 0,
170 | ```
171 |
172 | LudicrousDB tracks the tables that it has written since instantiation and sending subsequent read queries to the same server that received the write query. Thus a primary set up this way will still receive read queries, but only subsequent to writes.
173 |
174 | ## 3. Network topology / Datacenter awareness
175 |
176 | When your databases are located in separate physical locations there is typically an advantage to connecting to a nearby server instead of a more distant one. The read and write parameters can be used to place servers into logical groups of more or less preferred connections. Lower numbers indicate greater preference.
177 |
178 | This configuration instructs LudicrousDB to try reading from one of the local replicas at random. If that replica is unreachable or refuses the connection, the other replica will be tried, followed by the primary, and finally the remote replicas in random order.
179 |
180 | ```php
181 | Local replica 1: 'write' => 0, 'read' => 1,
182 | Local replica 2: 'write' => 0, 'read' => 1,
183 | Local primary: 'write' => 1, 'read' => 2,
184 | Remote replica 1: 'write' => 0, 'read' => 3,
185 | Remote replica 2: 'write' => 0, 'read' => 3,
186 | ```
187 |
188 | In the other datacenter, the primary would be remote. We would take that into account while deciding where to send reads. Writes would always be sent to the primary, regardless of proximity.
189 |
190 | ```php
191 | Local replica 1: 'write' => 0, 'read' => 1,
192 | Local replica 2: 'write' => 0, 'read' => 1,
193 | Remote replica 1: 'write' => 0, 'read' => 2,
194 | Remote replica 2: 'write' => 0, 'read' => 2,
195 | Remote primary: 'write' => 1, 'read' => 3,
196 | ```
197 |
198 | There are many ways to achieve different configurations in different locations. You can deploy different config files. You can write code to discover the web server's location, such as by inspecting `$_SERVER` or `php_uname()`, and compute the read/write parameters accordingly.
199 |
200 | ## 4. Replication Lag
201 |
202 | LudicrousDB accommodates replica lag by making decisions, based on the defined lag threshold. If the lag threshold is not set, it will ignore the replica lag. Otherwise, it will try to find a non-lagged replica, before connecting to a lagged one.
203 |
204 | A replica is considered lagged, if its lag is bigger than the lag threshold you have defined in `$wpdb->default_lag_threshold` or in the per-database settings. You can also rewrite the lag threshold, by returning `$server['lag_threshold']` variable with the 'dataset' group callbacks.
205 |
206 | LudicrousDB does not check the lag on the replicas. You have to define two callbacks callbacks to do that:
207 |
208 | ```php
209 | $wpdb->add_callback( $callback, 'get_lag_cache' );
210 | ```
211 |
212 | and
213 |
214 | ```php
215 | $wpdb->add_callback( $callback, 'get_lag' );
216 | ```
217 |
218 | The first one is called before connecting to a replica, and should return either: the replication lag in seconds, or false if unknown (based on `$wpdb->lag_cache_key`).
219 |
220 | The second callback is called after a connection to a replica is established. It should return either: it's replication lag, or false if unknown (based on the connection in `$wpdb->dbhs[ $wpdb->dbhname ]`).
221 |
222 | ## Sample replication lag detection configuration
223 |
224 | To detect replication lag, try [mk-heartbeat](http://www.maatkit.org/doc/mk-heartbeat.html) or pt-heartbeat from Percona Toolkit. These tools insert a timestamp into a table on the primary and then check the lag on the replicas. The lag is the difference in seconds between the current time and the timestamp on the replica.
225 |
226 | This implementation requires the database user to have read access to the heartbeat table.
227 |
228 | The cache uses shared memory for portability. Can be modified to work with Memcached, APC and etc.
229 |
230 | ```php
231 | $wpdb->lag_cache_ttl = 30;
232 | $wpdb->shmem_key = ftok( __FILE__, "Y" );
233 | $wpdb->shmem_size = 128 * 1024;
234 |
235 | $wpdb->add_callback( 'get_lag_cache', 'get_lag_cache' );
236 | $wpdb->add_callback( 'get_lag', 'get_lag' );
237 |
238 | function get_lag_cache( $wpdb ) {
239 | $segment = shm_attach( $wpdb->shmem_key, $wpdb->shmem_size, 0600 );
240 | $lag_data = @shm_get_var( $segment, 0 );
241 |
242 | shm_detach( $segment );
243 |
244 | if ( ! is_array( $lag_data ) || !is_array( $lag_data[ $wpdb->lag_cache_key ] ) ) {
245 | return false;
246 | }
247 |
248 | if ( $wpdb->lag_cache_ttl < time() - $lag_data[ $wpdb->lag_cache_key ][ 'timestamp' ] ) {
249 | return false;
250 | }
251 |
252 | return $lag_data[ $wpdb->lag_cache_key ][ 'lag' ];
253 | }
254 |
255 | function get_lag( $wpdb ) {
256 | $dbh = $wpdb->dbhs[ $wpdb->dbhname ];
257 |
258 | if ( ! mysql_select_db( 'heartbeat', $dbh ) ) {
259 | return false;
260 | }
261 |
262 | $result = mysql_query( "SELECT UNIX_TIMESTAMP() - UNIX_TIMESTAMP(ts) AS lag FROM heartbeat LIMIT 1", $dbh );
263 |
264 | if ( ! $result || false === $row = mysql_fetch_assoc( $result ) ) {
265 | return false;
266 | }
267 |
268 | // Cache the result in shared memory with timestamp
269 | $sem_id = sem_get( $wpdb->shmem_key, 1, 0600, 1 );
270 | sem_acquire( $sem_id );
271 | $segment = shm_attach( $wpdb->shmem_key, $wpdb->shmem_size, 0600 );
272 | $lag_data = @shm_get_var( $segment, 0 );
273 |
274 | if ( ! is_array( $lag_data ) ) {
275 | $lag_data = array();
276 | }
277 |
278 | $lag_data[ $wpdb->lag_cache_key ] = array( 'timestamp' => time(), 'lag' => $row[ 'lag' ] );
279 |
280 | shm_put_var( $segment, 0, $lag_data );
281 | shm_detach( $segment );
282 | sem_release( $sem_id );
283 |
284 | return $row[ 'lag' ];
285 | }
286 | ```
287 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "stuttter/ludicrousdb",
3 | "description": "LudicrousDB is a database class that supports replication, failover, load balancing, & partitioning, based on Automattic's HyperDB drop-in.",
4 | "homepage": "https://github.com/stuttter/ludicrousdb",
5 | "type": "wordpress-plugin",
6 | "license" : "GPL-2.0-or-later",
7 | "authors": [
8 | {
9 | "name": "John James Jacoby",
10 | "email": "johnjamesjacoby@me.com"
11 | },
12 | {
13 | "name": "Jonny Harris",
14 | "email": "jon@spacedmonkey.co.uk"
15 | }
16 | ],
17 | "support": {
18 | "issues": "https://github.com/stuttter/ludicrousdb/issues",
19 | "source": "https://github.com/stuttter/ludicrousdb"
20 | },
21 | "require": {
22 | "php": ">=5.2",
23 | "composer/installers": "~1.0 || ~2.0"
24 | },
25 | "require-dev": {
26 | "wp-coding-standards/wpcs": "^3.1",
27 | "dealerdirect/phpcodesniffer-composer-installer": "^1.0",
28 | "phpcompatibility/phpcompatibility-wp": "^2.1",
29 | "sirbrillig/phpcs-variable-analysis": "^2.8",
30 | "squizlabs/php_codesniffer": "^3.10",
31 | "phpcompatibility/php-compatibility": "^9.3"
32 | },
33 | "scripts": {
34 | "format": [
35 | "vendor/bin/phpcbf . -v"
36 | ],
37 | "lint": [
38 | "vendor/bin/phpcs . -v"
39 | ]
40 | },
41 | "config": {
42 | "allow-plugins": {
43 | "composer/installers": true,
44 | "dealerdirect/phpcodesniffer-composer-installer": true
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/ludicrousdb.php:
--------------------------------------------------------------------------------
1 | charset = 'utf8mb4';
23 |
24 | /**
25 | * This sets the default column collation. For best results, investigate which
26 | * collation is recommended for your specific character set.
27 | *
28 | * Default: utf8mb4_unicode_520_ci
29 | */
30 | $wpdb->collate = 'utf8mb4_unicode_520_ci';
31 |
32 | /**
33 | * This is useful for debugging. Queries are saved in $wpdb->queries. It is not
34 | * a constant because you might want to use it momentarily.
35 | * Default: false
36 | */
37 | $wpdb->save_queries = false;
38 |
39 | /**
40 | * The amount of time to wait before trying again to ping mysql server.
41 | *
42 | * Default: 0.1 (Seconds)
43 | */
44 | $wpdb->recheck_timeout = 0.1;
45 |
46 | /**
47 | * This determines whether to use mysql_connect or mysql_pconnect. The effects
48 | * of this setting may vary and should be carefully tested.
49 | *
50 | * Default: false
51 | */
52 | $wpdb->persistent = false;
53 |
54 | /**
55 | * This determines whether to use mysql connect or mysql connect has failed and to bail loading the rest of WordPress
56 | *
57 | * Default: false
58 | */
59 | $wpdb->allow_bail = false;
60 |
61 | /**
62 | * This is the number of mysql connections to keep open. Increase if you expect
63 | * to reuse a lot of connections to different servers. This is ignored if you
64 | * enable persistent connections.
65 | *
66 | * Default: 10
67 | */
68 | $wpdb->max_connections = 10;
69 |
70 | /**
71 | * Enables checking TCP responsiveness by fsockopen prior to mysql_connect or
72 | * mysql_pconnect. This was added because PHP's mysql functions do not provide
73 | * a variable timeout setting. Disabling it may improve average performance by
74 | * a very tiny margin but lose protection against connections failing slowly.
75 | *
76 | * Default: true
77 | */
78 | $wpdb->check_tcp_responsiveness = true;
79 |
80 | /**
81 | * The cache group that is used to store TCP responsiveness.
82 | *
83 | * Default: ludicrousdb
84 | */
85 | $wpdb->cache_group = 'ludicrousdb';
86 |
87 | /**
88 | * This is the most basic way to add a server to LudicrousDB using only the
89 | * required parameters: host, user, password, name.
90 | * This adds the DB defined in wp-config.php as a read/write server for
91 | * the 'global' dataset. (Every table is in 'global' by default.)
92 | */
93 | $wpdb->add_database(
94 | array(
95 | 'host' => DB_HOST, // If port is other than 3306, use host:port.
96 | 'user' => DB_USER,
97 | 'password' => DB_PASSWORD,
98 | 'name' => DB_NAME,
99 | )
100 | );
101 |
102 | /**
103 | * This adds the same server again, only this time it is configured as a replica.
104 | * The last three parameters are set to the defaults but are shown for clarity.
105 | */
106 | $wpdb->add_database(
107 | array(
108 | 'host' => DB_HOST, // If port is other than 3306, use host:port.
109 | 'user' => DB_USER,
110 | 'password' => DB_PASSWORD,
111 | 'name' => DB_NAME,
112 | 'write' => 0,
113 | 'read' => 1,
114 | 'dataset' => 'global',
115 | 'timeout' => 0.2,
116 | )
117 | );
118 |
--------------------------------------------------------------------------------
/ludicrousdb/drop-ins/db-error.php:
--------------------------------------------------------------------------------
1 |
39 |
45 | >
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 | dbh) of established MySQL connections.
60 | *
61 | * @var array
62 | */
63 | public $dbhs = array();
64 |
65 | /**
66 | * Database servers.
67 | *
68 | * Multi-dimensional array (dataset => servers) of datasets and servers.
69 | *
70 | * @var array Default empty array.
71 | */
72 | public $ludicrous_servers = array();
73 |
74 | /**
75 | * Database tables.
76 | *
77 | * Optional directory of tables and their datasets.
78 | *
79 | * @var array Default empty array.
80 | */
81 | public $ludicrous_tables = array();
82 |
83 | /**
84 | * Callbacks.
85 | *
86 | * Optional directory of callbacks to determine datasets from queries.
87 | *
88 | * @var array Default empty array.
89 | */
90 | public $ludicrous_callbacks = array();
91 |
92 | /**
93 | * Custom callback to save debug info in $this->queries.
94 | *
95 | * @var callable Default null.
96 | */
97 | public $save_query_callback = null;
98 |
99 | /**
100 | * Whether to pass "p:" into mysqli_real_connect() to force a
101 | * persistent connection.
102 | *
103 | * @var bool Default false.
104 | */
105 | public $persistent = false;
106 |
107 | /**
108 | * Kill the application if a database connection fails.
109 | *
110 | * @var bool Default false.
111 | */
112 | public $die_on_disconnect = false;
113 |
114 | /**
115 | * The maximum number of db links to keep open. The least-recently used
116 | * link will be closed when the number of links exceeds this.
117 | *
118 | * @var int Default 10.
119 | */
120 | public $max_connections = 10;
121 |
122 | /**
123 | * Whether to check with fsockopen prior to mysqli_real_connect.
124 | *
125 | * @var bool Default true.
126 | */
127 | public $check_tcp_responsiveness = true;
128 |
129 | /**
130 | * The amount of time to wait before trying again to ping mysql server.
131 | *
132 | * @var float Default 0.1.
133 | */
134 | public $recheck_timeout = 0.1;
135 |
136 | /**
137 | * The number of times to retry reconnecting before dying
138 | *
139 | * @var int Default 3.
140 | */
141 | public $reconnect_retries = 3;
142 |
143 | /**
144 | * The amount of time to wait before trying again to connect to a mysql server.
145 | *
146 | * @var float Default 1.
147 | */
148 | public $reconnect_sleep = 1.0;
149 |
150 | /**
151 | * Whether to check for heartbeats.
152 | *
153 | * @var bool Default true.
154 | */
155 | public $check_dbh_heartbeats = true;
156 |
157 | /**
158 | * Keeps track of the dbhname usage and errors.
159 | *
160 | * @var array Default empty array.
161 | */
162 | public $dbhname_heartbeats = array();
163 |
164 | /**
165 | * The tables that have been written to.
166 | *
167 | * Disables replica connections if explicitly true.
168 | *
169 | * @var array|bool Default empty array.
170 | */
171 | public $send_reads_to_primaries = array();
172 |
173 | /**
174 | * The log of db connections made and the time each one took.
175 | *
176 | * @var array Default empty array.
177 | */
178 | public $db_connections = array();
179 |
180 | /**
181 | * The list of unclosed connections sorted by LRU.
182 | *
183 | * @var array Default empty array.
184 | */
185 | public $open_connections = array();
186 |
187 | /**
188 | * Lookup array (dbhname => host:port).
189 | *
190 | * @var array Default empty array.
191 | */
192 | public $dbh2host = array();
193 |
194 | /**
195 | * The last server used and the database name selected.
196 | *
197 | * @var array Default empty array.
198 | */
199 | public $last_used_server = array();
200 |
201 | /**
202 | * Lookup array (dbhname => (server, db name) ) for re-selecting the db
203 | * when a link is re-used.
204 | *
205 | * @var array Default empty array.
206 | */
207 | public $used_servers = array();
208 |
209 | /**
210 | * Whether to save debug_backtrace in save_query_callback. You may wish
211 | * to disable this, e.g. when tracing out-of-memory problems.
212 | *
213 | * @var bool Default true.
214 | */
215 | public $save_backtrace = true;
216 |
217 | /**
218 | * The default database attributes that are used when
219 | *
220 | * @var array Default database values.
221 | */
222 | public $database_defaults = array(
223 | 'dataset' => 'global',
224 | 'write' => 1,
225 | 'read' => 1,
226 | 'timeout' => 0.2,
227 | 'port' => 3306,
228 | 'lag_threshold' => null,
229 | );
230 |
231 | /**
232 | * Name of object TCP cache group.
233 | *
234 | * @var string Default 'ludicrousdb'.
235 | */
236 | public $tcp_cache_group = 'ludicrousdb';
237 |
238 | /**
239 | * The amount of time to wait before trying again to ping a server.
240 | *
241 | * @var float Default 0.2 seconds (I.E. 200ms).
242 | */
243 | public $tcp_timeout = 0.2;
244 |
245 | /**
246 | * In memory cache for TCP connected status.
247 | *
248 | * @var array Default empty array.
249 | */
250 | private $tcp_cache = array();
251 |
252 | /**
253 | * Whether to ignore replica lag.
254 | *
255 | * @var bool Default false.
256 | */
257 | private $ignore_replica_lag = false;
258 |
259 | /**
260 | * Number of unique servers.
261 | *
262 | * @var null|int Default null. Might be zero or more.
263 | */
264 | private $unique_servers = null;
265 |
266 | /**
267 | * Result of the last callback run.
268 | *
269 | * @var mixed Default null.
270 | */
271 | private $callback_result = null;
272 |
273 | /**
274 | * Array of renamed class variables.
275 | *
276 | * @since 5.2.0
277 | *
278 | * @var array Default key-value array of old and new class variables.
279 | */
280 | private static $renamed_vars = array(
281 | 'ignore_slave_lag' => 'ignore_replica_lag',
282 | 'srtm' => 'send_reads_to_primaries',
283 | 'allow_bail' => 'die_on_disconnect',
284 | );
285 |
286 | /**
287 | * Array of binary blob database column types.
288 | *
289 | * @since 5.2.0
290 | *
291 | * @var array Default array of binary blob column types.
292 | */
293 | private static $bin_blobs = array(
294 | 'BINARY',
295 | 'VARBINARY',
296 | 'TINYBLOB',
297 | 'MEDIUMBLOB',
298 | 'BLOB',
299 | 'LONGBLOB',
300 | );
301 |
302 | /**
303 | * Array of allowed character sets.
304 | *
305 | * @since 5.2.0
306 | *
307 | * @var array Default array of allowed character sets.
308 | */
309 | private static $allowed_charsets = array(
310 | 'utf8',
311 | 'utf8mb4',
312 | 'latin1',
313 | );
314 |
315 | /**
316 | * Gets ready to make database connections
317 | *
318 | * @since 1.0.0
319 | * @since 5.2.0 Matched parameters to parent wpdb class
320 | *
321 | * @param array|string $dbuser New class variables, or Database user.
322 | * @param string $dbpassword Database password.
323 | * @param string $dbname Database name.
324 | * @param string $dbhost Database host.
325 | */
326 | public function __construct( $dbuser = '', $dbpassword = '', $dbname = '', $dbhost = '' ) {
327 |
328 | // Show errors if debug-display mode is enabled
329 | if ( $this->is_debug_display() ) {
330 | $this->show_errors();
331 | }
332 |
333 | // Start the TCP cache
334 | $this->tcp_cache_start();
335 |
336 | // Prepare class vars
337 | $this->prepare_class_vars( $dbuser, $dbpassword, $dbname, $dbhost );
338 | }
339 |
340 | /**
341 | * Magic method to correctly get renamed attributes.
342 | *
343 | * @since 5.2.0
344 | *
345 | * @param string $name The key to set.
346 | * @return mixed
347 | */
348 | public function __get( $name ) {
349 |
350 | // Check if old var is in $class_vars_renamed
351 | if ( isset( self::$renamed_vars[ $name ] ) ) {
352 | $name = self::$renamed_vars[ $name ];
353 | }
354 |
355 | return parent::__get( $name );
356 | }
357 |
358 | /**
359 | * Magic method to correctly set renamed attributes.
360 | *
361 | * @since 5.2.0
362 | *
363 | * @param string $name The key to set.
364 | * @param mixed $value The value to set.
365 | */
366 | public function __set( $name, $value ) {
367 |
368 | // Check if old var is in $class_vars_renamed
369 | if ( isset( self::$renamed_vars[ $name ] ) ) {
370 | $name = self::$renamed_vars[ $name ];
371 | }
372 |
373 | parent::__set( $name, $value );
374 | }
375 |
376 | /**
377 | * Prepare class vars from constructor.
378 | *
379 | * @since 5.2.0
380 | *
381 | * @param array|string $dbuser New class variables, or Database user.
382 | * @param string $dbpassword Database password.
383 | * @param string $dbname Database name.
384 | * @param string $dbhost Database host.
385 | */
386 | protected function prepare_class_vars( $dbuser = '', $dbpassword = '', $dbname = '', $dbhost = '' ) {
387 |
388 | // Bail if first method parameter is empty
389 | if ( empty( $dbuser ) ) {
390 | return;
391 | }
392 |
393 | // Default class vars
394 | $class_vars = array();
395 |
396 | // Custom class vars via array of arguments
397 | if ( is_array( $dbuser ) ) {
398 | $class_vars = $dbuser;
399 |
400 | // WPDB style parameter pattern
401 | } elseif ( is_string( $dbuser ) ) {
402 |
403 | // Only compact if all params are not empty
404 | if ( ! empty( $dbpassword ) && ! empty( $dbname ) && ! empty( $dbhost ) ) {
405 | $class_vars = compact( $dbuser, $dbpassword, $dbname, $dbhost );
406 | }
407 | }
408 |
409 | // Only set vars if there are vars to set
410 | if ( ! empty( $class_vars ) ) {
411 | $this->set_class_vars( $class_vars );
412 | }
413 | }
414 |
415 | /**
416 | * Sets class vars from an array of arguments.
417 | *
418 | * @since 5.2.0
419 | * @param array $args Array of variables to set.
420 | */
421 | protected function set_class_vars( $args = array() ) {
422 |
423 | // Bail if empty arguments
424 | if ( empty( $args ) ) {
425 | return;
426 | }
427 |
428 | // Get class vars as array of keys
429 | $class_vars = get_class_vars( __CLASS__ );
430 | $class_var_keys = array_keys( $class_vars );
431 |
432 | /**
433 | * Explicit backwards compatibility for passing default_lag_threshold
434 | * in as a class argument.
435 | *
436 | * @since 5.2.0
437 | */
438 | if (
439 | isset( $args['default_lag_threshold'] )
440 | &&
441 | ! isset( $args['database_defaults']['lag_threshold'] )
442 | ) {
443 | $this->database_defaults['lag_threshold'] = $args['default_lag_threshold'];
444 | }
445 |
446 | // Loop through class vars and override if set in $args
447 | foreach ( $class_var_keys as $var ) {
448 |
449 | // Check if old var is in $args
450 | if (
451 | isset( self::$renamed_vars[ $var ] )
452 | &&
453 | isset( $args[ self::$renamed_vars[ $var ] ] )
454 | ) {
455 | $this->{$var} = $args[ self::$renamed_vars[ $var ] ];
456 | }
457 |
458 | // Check if current var is in $args
459 | if ( isset( $args[ $var ] ) ) {
460 | $this->{$var} = $args[ $var ];
461 | }
462 | }
463 | }
464 |
465 | /**
466 | * Sets $this->charset and $this->collate
467 | *
468 | * @since 1.0.0
469 | */
470 | public function init_charset() {
471 |
472 | // Defaults
473 | $charset = 'utf8mb4';
474 | $collate = 'utf8mb4_unicode_520_ci';
475 |
476 | // Use constant if defined
477 | if ( defined( 'DB_COLLATE' ) ) {
478 | $collate = DB_COLLATE;
479 | }
480 |
481 | // Use constant if defined
482 | if ( defined( 'DB_CHARSET' ) ) {
483 | $charset = DB_CHARSET;
484 | }
485 |
486 | // Determine charset and collate
487 | $charset_collate = $this->determine_charset( $charset, $collate );
488 |
489 | // Set charset and collate
490 | $this->charset = $charset_collate['charset'];
491 | $this->collate = $charset_collate['collate'];
492 | }
493 |
494 | /**
495 | * Add the connection parameters for a database
496 | *
497 | * @since 1.0.0
498 | *
499 | * @param array $db Default to empty array.
500 | */
501 | public function add_database( array $db = array() ) {
502 |
503 | // Merge using defaults
504 | $db = array_merge( $this->database_defaults, $db );
505 |
506 | // Break these apart to make code easier to understand below
507 | $dataset = $db['dataset'];
508 | $write = $db['write'];
509 | $read = $db['read'];
510 |
511 | // We do not include the dataset in the array. It's used as a key.
512 | unset( $db['dataset'] );
513 |
514 | // Maybe add database to array of write's
515 | if ( ! empty( $write ) ) {
516 | $this->ludicrous_servers[ $dataset ]['write'][ $write ][] = $db;
517 | }
518 |
519 | // Maybe add database to array of read's
520 | if ( ! empty( $read ) ) {
521 | $this->ludicrous_servers[ $dataset ]['read'][ $read ][] = $db;
522 | }
523 | }
524 |
525 | /**
526 | * Specify the dataset where a table is found
527 | *
528 | * @since 1.0.0
529 | *
530 | * @param string $dataset Database.
531 | * @param string $table Table name.
532 | */
533 | public function add_table( $dataset, $table ) {
534 | $this->ludicrous_tables[ $table ] = $dataset;
535 | }
536 |
537 | /**
538 | * Add a callback to a group of callbacks
539 | *
540 | * The default group is 'dataset', used to examine queries & determine dataset
541 | *
542 | * @since 1.0.0
543 | *
544 | * @param function $callback Callback on a dataset.
545 | * @param string $group Key name of dataset.
546 | */
547 | public function add_callback( $callback, $group = 'dataset' ) {
548 | $this->ludicrous_callbacks[ $group ][] = $callback;
549 | }
550 |
551 | /**
552 | * Determine the likelihood that this query could alter anything
553 | *
554 | * Statements are considered read-only when:
555 | * 1. not including UPDATE nor other "may-be-write" strings
556 | * 2. begin with SELECT etc.
557 | *
558 | * @since 1.0.0
559 | *
560 | * @param string $q Query.
561 | *
562 | * @return bool
563 | */
564 | public function is_write_query( $q = '' ) {
565 |
566 | // Trim potential whitespace or subquery chars
567 | $q = ltrim( $q, "\r\n\t (" );
568 |
569 | // Possible writes
570 | if ( preg_match( '/(?:^|\s)(?:ALTER|CREATE|ANALYZE|CHECK|OPTIMIZE|REPAIR|CALL|DELETE|DROP|INSERT|LOAD|REPLACE|UPDATE|FOR\s+UPDATE|SET|RENAME\s+TABLE|[a-z]+_LOCKS?\()(?:\s|$)/i', $q ) ) {
571 | return true;
572 | }
573 |
574 | // Not possible non-writes (phew!)
575 | return ! preg_match( '/^(?:SELECT|SHOW|DESCRIBE|DESC|EXPLAIN)(?:\s|$)/i', $q );
576 | }
577 |
578 | /**
579 | * Is the primary database dead?
580 | *
581 | * @since 5.2.0
582 | *
583 | * @return bool True if primary database is dead, false otherwise.
584 | */
585 | public function is_primary_dead() {
586 | return (
587 | defined( 'PRIMARY_DB_DEAD' )
588 | ||
589 | defined( 'MASTER_DB_DEAD' )
590 | );
591 | }
592 |
593 | /**
594 | * Is debug mode enabled?
595 | *
596 | * @since 5.2.0
597 | */
598 | public function is_debug() {
599 | return (
600 | ( defined( 'LDB_DEBUG' ) && LDB_DEBUG )
601 | ||
602 | ( defined( 'WP_DEBUG' ) && WP_DEBUG )
603 | );
604 | }
605 |
606 | /**
607 | * Is debug display mode enabled?
608 | *
609 | * @since 5.2.0
610 | */
611 | public function is_debug_display() {
612 | return (
613 | $this->is_debug()
614 | &&
615 | ( defined( 'WP_DEBUG_DISPLAY' ) && WP_DEBUG_DISPLAY )
616 | );
617 | }
618 |
619 | /**
620 | * Are queries being saved?
621 | *
622 | * @since 5.2.0
623 | */
624 | public function is_saving_queries() {
625 | return (
626 | ( defined( 'SAVEQUERIES' ) && SAVEQUERIES )
627 | );
628 | }
629 |
630 | /**
631 | * Set a flag to prevent reading from replicas, which might be lagging
632 | * after a write.
633 | *
634 | * @since 5.2.0
635 | */
636 | public function send_reads_to_primaries() {
637 | $this->send_reads_to_primaries = true;
638 | }
639 |
640 | /**
641 | * Callbacks are executed in the order in which they are registered until one
642 | * of them returns something other than null
643 | *
644 | * @since 1.0.0
645 | * @param string $group Group, key name in array.
646 | * @param array $args Args passed to callback. Default to null.
647 | */
648 | public function run_callbacks( $group = '', $args = null ) {
649 |
650 | // Bail if no callbacks for group
651 | if (
652 | empty( $group )
653 | ||
654 | ! isset( $this->ludicrous_callbacks[ $group ] )
655 | ||
656 | ! is_array( $this->ludicrous_callbacks[ $group ] )
657 | ) {
658 | return;
659 | }
660 |
661 | // Prepare args
662 | if ( ! isset( $args ) ) {
663 | $args = array( &$this );
664 | } elseif ( is_array( $args ) ) {
665 | $args[] = &$this;
666 | } else {
667 | $args = array( $args, &$this );
668 | }
669 |
670 | // Loop through callbacks
671 | foreach ( $this->ludicrous_callbacks[ $group ] as $func ) {
672 |
673 | // Run callback
674 | $result = call_user_func_array( $func, $args );
675 |
676 | // Return result if not null
677 | if ( isset( $result ) ) {
678 | return $result;
679 | }
680 | }
681 | }
682 |
683 | /**
684 | * Figure out which db server should handle the query, and connect to it.
685 | *
686 | * @since 1.0.0
687 | *
688 | * @param string $query Query.
689 | *
690 | * @return resource MySQL database connection
691 | */
692 | public function db_connect( $query = '' ) {
693 |
694 | // Bail if empty query
695 | if ( empty( $query ) ) {
696 | return false;
697 | }
698 |
699 | // Fix error reporting change (in PHP 8.1) causing fatal errors
700 | // See: https://php.watch/versions/8.1/mysqli-error-mode
701 | mysqli_report( MYSQLI_REPORT_OFF );
702 |
703 | // Can be empty/false if the query is e.g. "COMMIT"
704 | $this->table = $this->get_table_from_query( $query );
705 | if ( empty( $this->table ) ) {
706 | $this->table = 'no-table';
707 | }
708 | $this->last_table = $this->table;
709 |
710 | // Use current table with no callback results
711 | if ( isset( $this->ludicrous_tables[ $this->table ] ) ) {
712 | $dataset = $this->ludicrous_tables[ $this->table ];
713 | $this->callback_result = null;
714 |
715 | // Run callbacks and either extract or update dataset
716 | } else {
717 |
718 | // Run callbacks and get result
719 | $this->callback_result = $this->run_callbacks( 'dataset', $query );
720 |
721 | // Set if not null
722 | if ( ! is_null( $this->callback_result ) ) {
723 | if ( is_array( $this->callback_result ) ) {
724 | extract( $this->callback_result, EXTR_OVERWRITE );
725 | } else {
726 | $dataset = $this->callback_result;
727 | }
728 | }
729 | }
730 |
731 | if ( ! isset( $dataset ) ) {
732 | $dataset = 'global';
733 | }
734 |
735 | if ( empty( $dataset ) ) {
736 | return $this->bail( "Unable to determine which dataset to query. ({$this->table})" );
737 | } else {
738 | $this->dataset = $dataset;
739 | }
740 |
741 | $this->run_callbacks( 'dataset_found', $dataset );
742 |
743 | if ( empty( $this->ludicrous_servers ) ) {
744 |
745 | // Return early dbh if already set
746 | if ( $this->dbh_type_check( $this->dbh ) ) {
747 | return $this->dbh;
748 | }
749 |
750 | // Bail if missing database constants
751 | if (
752 | ! defined( 'DB_HOST' )
753 | ||
754 | ! defined( 'DB_USER' )
755 | ||
756 | ! defined( 'DB_PASSWORD' )
757 | ||
758 | ! defined( 'DB_NAME' )
759 | ) {
760 | return $this->bail( 'We were unable to query because there was no database defined.' );
761 | }
762 |
763 | // Fallback to wpdb::db_connect() method.
764 |
765 | $this->dbuser = DB_USER;
766 | $this->dbpassword = DB_PASSWORD;
767 | $this->dbname = DB_NAME;
768 | $this->dbhost = DB_HOST;
769 |
770 | parent::db_connect();
771 |
772 | return $this->dbh;
773 | }
774 |
775 | /**
776 | * Determine whether the query must be sent to the primary (a writable server)
777 | */
778 |
779 | // Explicitly already set to using the primary
780 | if (
781 | ! empty( $use_primary ) // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable
782 | ||
783 | ( true === $this->send_reads_to_primaries )
784 | ||
785 | isset( $this->send_reads_to_primaries[ $this->table ] )
786 | ) {
787 | $use_primary = true;
788 |
789 | // Is this a write query?
790 | } elseif ( $this->is_write_query( $query ) ) {
791 | $use_primary = true;
792 |
793 | if ( is_array( $this->send_reads_to_primaries ) ) {
794 | $this->send_reads_to_primaries[ $this->table ] = true;
795 | }
796 |
797 | // Detect queries that have a join in the send_reads_to_primaries array.
798 | } elseif (
799 | ! isset( $use_primary )
800 | &&
801 | is_array( $this->send_reads_to_primaries )
802 | &&
803 | ! empty( $this->send_reads_to_primaries )
804 | ) {
805 | $use_primary = false;
806 | $query_match = substr( $query, 0, 1000 );
807 |
808 | foreach ( $this->send_reads_to_primaries as $key => $value ) {
809 | if ( false !== stripos( $query_match, $key ) ) {
810 | $use_primary = true;
811 | break;
812 | }
813 | }
814 |
815 | // Default to false.
816 | } else {
817 | $use_primary = false;
818 | }
819 |
820 | if ( ! empty( $use_primary ) ) {
821 | $this->dbhname = $dbhname = $dataset . '__w';
822 | $operation = 'write';
823 | } else {
824 | $this->dbhname = $dbhname = $dataset . '__r';
825 | $operation = 'read';
826 | }
827 |
828 | // Try to reuse an existing connection
829 | while (
830 | isset( $this->dbhs[ $dbhname ] )
831 | &&
832 | $this->dbh_type_check( $this->dbhs[ $dbhname ] )
833 | ) {
834 |
835 | // Get the connection indexes
836 | $conns = array_keys( $this->db_connections );
837 | $conn = 0;
838 |
839 | // Loop through connections to find the matching dbhname
840 | if ( ! empty( $conns ) ) {
841 | foreach ( $conns as $i ) {
842 | if ( $this->db_connections[ $i ]['dbhname'] === $dbhname ) {
843 | $conn = (int) $i;
844 | }
845 | }
846 | }
847 |
848 | // Try to use the database name from the callback, if scalar
849 | if (
850 | ! empty( $server['name'] )
851 | &&
852 | is_scalar( $server['name'] )
853 | ) {
854 | $name = (string) $server['name'];
855 |
856 | // A callback specified a database name, but it is possible the
857 | // existing connection selected a different one.
858 | if ( $this->used_servers[ $dbhname ]['name'] !== $name ) {
859 |
860 | // If the select fails, disconnect and try again
861 | if ( ! $this->select( $name, $this->dbhs[ $dbhname ] ) ) {
862 |
863 | // This can happen when the user varies and lacks
864 | // permission on the $name database
865 | $this->increment_db_connection( $conn, 'disconnect (select failed)' );
866 | $this->disconnect( $dbhname );
867 |
868 | break;
869 | }
870 |
871 | // Update the used server name
872 | $this->used_servers[ $dbhname ]['name'] = $name;
873 | }
874 |
875 | // Otherwise, use the name from the last connection
876 | } else {
877 | $name = $this->used_servers[ $dbhname ]['name'];
878 | }
879 |
880 | $this->current_host = $this->dbh2host[ $dbhname ];
881 |
882 | // Keep this connection at the top of the stack to prevent
883 | // disconnecting from frequently-used connections
884 | $key = array_search( $dbhname, $this->open_connections, true );
885 | if ( $key !== false ) {
886 | unset( $this->open_connections[ $key ] );
887 | $this->open_connections[] = $dbhname;
888 | }
889 |
890 | $this->last_used_server = $this->used_servers[ $dbhname ];
891 | $this->last_connection = compact( 'dbhname', 'name' );
892 |
893 | // Check if the connection is still alive
894 | if (
895 | $this->should_mysql_ping( $dbhname )
896 | &&
897 | ! $this->check_connection( $this->die_on_disconnect, $this->dbhs[ $dbhname ], $query )
898 | ) {
899 | $this->increment_db_connection( $conn, 'disconnect (ping failed)' );
900 | $this->disconnect( $dbhname );
901 |
902 | break;
903 | }
904 |
905 | // Increment the connection counter
906 | $this->increment_db_connection( $conn, 'queries' );
907 |
908 | return $this->dbhs[ $dbhname ];
909 | }
910 |
911 | // Bail if trying to connect to a dead primary
912 | if (
913 | ! empty( $use_primary )
914 | &&
915 | $this->is_primary_dead()
916 | ) {
917 | return $this->bail( 'We are updating the database. Please try back in 5 minutes. If you are posting to your blog please hit the refresh button on your browser in a few minutes to post the data again. It will be posted as soon as the database is back online.' );
918 | }
919 |
920 | // Bail if no servers available for table/dataset/operation
921 | if ( empty( $this->ludicrous_servers[ $dataset ][ $operation ] ) ) {
922 | return $this->bail( "No databases available with {$this->table} ({$dataset})" );
923 | }
924 |
925 | // Put the operations in order by key
926 | ksort( $this->ludicrous_servers[ $dataset ][ $operation ] );
927 |
928 | // Make a list of at least $this->reconnect_retries connections to try,
929 | // repeating as necessary.
930 | $servers = array();
931 | do {
932 | foreach ( $this->ludicrous_servers[ $dataset ][ $operation ] as $group => $items ) {
933 | $keys = array_keys( $items );
934 |
935 | shuffle( $keys );
936 |
937 | foreach ( $keys as $key ) {
938 | $servers[] = compact( 'group', 'key' );
939 | }
940 | }
941 |
942 | $tries_remaining = count( $servers );
943 | if ( 0 === $tries_remaining ) {
944 | return $this->bail( "No database servers were found to match the query. ({$this->table}, {$dataset})" );
945 | }
946 |
947 | if ( is_null( $this->unique_servers ) ) {
948 | $this->unique_servers = $tries_remaining;
949 | }
950 | } while ( $tries_remaining < $this->reconnect_retries );
951 |
952 | // Connect to a database server
953 | do {
954 | $unique_lagged_replicas = array();
955 | $success = false;
956 |
957 | foreach ( $servers as $group_key ) {
958 | --$tries_remaining;
959 |
960 | // If all servers are lagged, we need to start ignoring the lag and retry
961 | if ( count( $unique_lagged_replicas ) === (int) $this->unique_servers ) {
962 | break;
963 | }
964 |
965 | // $group, $key
966 | $group = $group_key['group'];
967 | $key = $group_key['key'];
968 |
969 | // $host, $port, $user, $password, $name, $write, $read, $timeout, $lag_threshold
970 | $db_config = $this->ludicrous_servers[ $dataset ][ $operation ][ $group ][ $key ];
971 | $host = $db_config['host'];
972 | $port = $db_config['port'];
973 | $user = $db_config['user'];
974 | $password = $db_config['password'];
975 | $name = $db_config['name'];
976 | $write = $db_config['write'];
977 | $read = $db_config['read'];
978 | $timeout = $db_config['timeout'];
979 | $lag_threshold = $db_config['lag_threshold'];
980 |
981 | // Overwrite vars from $server (if it was extracted from a callback)
982 | if ( ! empty( $server ) && is_array( $server ) ) {
983 | extract( $server, EXTR_OVERWRITE );
984 |
985 | // Otherwise, set $server to an empty array
986 | } else {
987 | $server = array();
988 | }
989 |
990 | // Maybe split host:port into $host and $port
991 | if ( strpos( $host, ':' ) ) {
992 | list( $host, $port ) = explode( ':', $host );
993 | }
994 |
995 | // Maybe use the default port number (usually: 3306)
996 | if ( empty( $port ) ) {
997 | $port = (int) $this->database_defaults['port'];
998 | }
999 |
1000 | // Maybe use the default timeout (usually: 200ms)
1001 | if ( ! isset( $timeout ) ) {
1002 | $timeout = (float) $this->tcp_timeout;
1003 | }
1004 |
1005 | // Get the minimum group here, in case $server rewrites it
1006 | if ( ! isset( $min_group ) || ( $min_group > $group ) ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable
1007 | $min_group = $group;
1008 | }
1009 |
1010 | // Format the cache key using the extracted host and port
1011 | $host_and_port = $this->tcp_get_cache_key( $host, $port );
1012 |
1013 | // Can be used by the lag callbacks
1014 | $this->lag_cache_key = $host_and_port;
1015 | $this->lag_threshold = isset( $lag_threshold )
1016 | ? $lag_threshold
1017 | : $this->database_defaults['lag_threshold'];
1018 |
1019 | // Check for a lagged replica, if applicable
1020 | if (
1021 | empty( $use_primary )
1022 | &&
1023 | empty( $write )
1024 | &&
1025 | empty( $this->ignore_replica_lag )
1026 | &&
1027 | isset( $this->lag_threshold )
1028 | &&
1029 | ! isset( $server['host'] )
1030 | &&
1031 | ( $lagged_status = $this->get_lag_cache() ) === DB_LAG_BEHIND // phpcs:ignore Generic.CodeAnalysis.AssignmentInCondition
1032 | ) {
1033 |
1034 | // If it is the last lagged replica. and it is with the best
1035 | // preference, we will ignore its lag
1036 | if (
1037 | ! isset( $unique_lagged_replicas[ $host_and_port ] )
1038 | &&
1039 | ( ( count( $unique_lagged_replicas ) + 1 ) === (int) $this->unique_servers )
1040 | &&
1041 | ( $group === $min_group )
1042 | ) {
1043 | $this->lag_threshold = null;
1044 |
1045 | // Otherwise, log the lag and continue on
1046 | } else {
1047 | $unique_lagged_replicas[ $host_and_port ] = $this->lag;
1048 |
1049 | continue;
1050 | }
1051 | }
1052 |
1053 | $this->timer_start();
1054 |
1055 | // Maybe check TCP responsiveness
1056 | $tcp = ! empty( $this->check_tcp_responsiveness )
1057 | ? $this->check_tcp_responsiveness( $host, $port, $timeout )
1058 | : null;
1059 |
1060 | // Connect if necessary or possible
1061 | if (
1062 | ! empty( $use_primary )
1063 | ||
1064 | empty( $tries_remaining )
1065 | ||
1066 | ( true === $tcp )
1067 | ) {
1068 | $this->single_db_connect( $dbhname, $host_and_port, $user, $password );
1069 | } else {
1070 | $this->dbhs[ $dbhname ] = false;
1071 | }
1072 |
1073 | $elapsed = $this->timer_stop();
1074 |
1075 | if ( $this->dbh_type_check( $this->dbhs[ $dbhname ] ) ) {
1076 | /**
1077 | * If we care about lag, disconnect lagged replicas and try
1078 | * to find others. We don't disconnect if it is the last
1079 | * lagged replica and it is with the best preference.
1080 | */
1081 | if (
1082 | empty( $use_primary )
1083 | &&
1084 | empty( $write )
1085 | &&
1086 | empty( $this->ignore_replica_lag )
1087 | &&
1088 | isset( $this->lag_threshold )
1089 | &&
1090 | ! isset( $server['host'] )
1091 | &&
1092 | ( $lagged_status !== DB_LAG_OK )
1093 | &&
1094 | ( $lagged_status = $this->get_lag() ) === DB_LAG_BEHIND // phpcs:ignore Generic.CodeAnalysis.AssignmentInCondition
1095 | &&
1096 | ! (
1097 | ! isset( $unique_lagged_replicas[ $host_and_port ] )
1098 | &&
1099 | ( (int) $this->unique_servers === ( count( $unique_lagged_replicas ) + 1 ) )
1100 | &&
1101 | ( $group === $min_group )
1102 | )
1103 | ) {
1104 | $unique_lagged_replicas[ $host_and_port ] = $this->lag;
1105 | $this->disconnect( $dbhname );
1106 |
1107 | $this->dbhs[ $dbhname ] = false;
1108 | $success = false;
1109 | $msg = "Replication lag of {$this->lag}s on {$host_and_port} ({$dbhname})";
1110 |
1111 | $this->print_error( $msg );
1112 |
1113 | continue;
1114 |
1115 | } else {
1116 | $this->set_sql_mode( array(), $this->dbhs[ $dbhname ] );
1117 |
1118 | if ( $this->select( $name, $this->dbhs[ $dbhname ] ) ) {
1119 | $this->current_host = $host_and_port;
1120 | $this->dbh2host[ $dbhname ] = $host_and_port;
1121 |
1122 | // Define these to avoid undefined variable notices
1123 | $queries = isset( $queries ) ? $queries : 1; // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable
1124 | $lag = isset( $this->lag ) ? $this->lag : 0;
1125 |
1126 | $this->last_connection = compact( 'dbhname', 'host', 'port', 'user', 'name', 'tcp', 'elapsed', 'success', 'queries', 'lag' );
1127 | $this->db_connections[] = $this->last_connection;
1128 | $this->open_connections[] = $dbhname;
1129 | $success = true;
1130 |
1131 | break;
1132 | }
1133 | }
1134 | }
1135 |
1136 | $success = false;
1137 | $this->last_connection = compact( 'dbhname', 'host', 'port', 'user', 'name', 'tcp', 'elapsed', 'success' );
1138 | $this->db_connections[] = $this->last_connection;
1139 |
1140 | if ( $this->dbh_type_check( $this->dbhs[ $dbhname ] ) ) {
1141 | $error = mysqli_error( $this->dbhs[ $dbhname ] );
1142 | $errno = mysqli_errno( $this->dbhs[ $dbhname ] );
1143 | }
1144 |
1145 | $msg = date( 'Y-m-d H:i:s' ) . " Can't select {$dbhname} - \n"; // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
1146 | $msg .= "'referrer' => '{$_SERVER['HTTP_HOST']}{$_SERVER['REQUEST_URI']}',\n";
1147 | $msg .= "'host' => {$host},\n";
1148 |
1149 | if ( ! empty( $error ) ) {
1150 | $msg .= "'error' => {$error},\n";
1151 | }
1152 |
1153 | if ( ! empty( $errno ) ) {
1154 | $msg .= "'errno' => {$errno},\n";
1155 |
1156 | // Maybe log the error to heartbeats
1157 | if ( ! empty( $this->check_dbh_heartbeats ) ) {
1158 | $this->dbhname_heartbeats[ $dbhname ]['last_errno'] = $errno;
1159 | }
1160 | }
1161 |
1162 | $msg .= "'tcp_responsive' => " . ( $tcp === true
1163 | ? 'true'
1164 | : $tcp ) . ",\n";
1165 |
1166 | $msg .= "'lagged_status' => " . ( isset( $lagged_status )
1167 | ? $lagged_status
1168 | : DB_LAG_UNKNOWN );
1169 |
1170 | $this->print_error( $msg );
1171 | }
1172 |
1173 | // Maybe bail if we have tried all the servers and none of them
1174 | // worked.
1175 | if (
1176 | empty( $success )
1177 | ||
1178 | ! isset( $this->dbhs[ $dbhname ] )
1179 | ||
1180 | ! $this->dbh_type_check( $this->dbhs[ $dbhname ] )
1181 | ) {
1182 |
1183 | // Lagged replicas were not used. Ignore the lag for this
1184 | // connection attempt and retry.
1185 | if (
1186 | empty( $this->ignore_replica_lag )
1187 | &&
1188 | count( $unique_lagged_replicas )
1189 | ) {
1190 | $this->ignore_replica_lag = true;
1191 | $tries_remaining = count( $servers );
1192 |
1193 | continue;
1194 | }
1195 |
1196 | // Setup the callback data
1197 | $callback_data = array(
1198 | 'host' => $host,
1199 | 'port' => $port,
1200 | 'operation' => $operation,
1201 | 'table' => $this->table,
1202 | 'dataset' => $dataset,
1203 | 'dbhname' => $dbhname,
1204 | );
1205 |
1206 | $this->run_callbacks( 'db_connection_error', $callback_data );
1207 |
1208 | return $this->bail( "Unable to connect to {$host}:{$port} to {$operation} table '{$this->table}' ({$dataset})" );
1209 | }
1210 |
1211 | break;
1212 | } while ( true );
1213 |
1214 | $this->set_charset( $this->dbhs[ $dbhname ] );
1215 |
1216 | $this->dbh = $this->dbhs[ $dbhname ]; // needed by $wpdb->_real_escape()
1217 | $this->last_used_server = compact( 'host', 'user', 'name', 'write', 'read' );
1218 | $this->used_servers[ $dbhname ] = $this->last_used_server;
1219 |
1220 | while (
1221 | ( false === $this->persistent )
1222 | &&
1223 | ( count( $this->open_connections ) > $this->max_connections ) // phpcs:ignore Squiz.PHP.DisallowSizeFunctionsInLoops.Found
1224 | ) {
1225 | $oldest_connection = array_shift( $this->open_connections );
1226 |
1227 | if ( $this->dbhs[ $oldest_connection ] !== $this->dbhs[ $dbhname ] ) {
1228 | $this->disconnect( $oldest_connection );
1229 | }
1230 | }
1231 |
1232 | return $this->dbhs[ $dbhname ];
1233 | }
1234 |
1235 | /**
1236 | * Increment a database connection counter.
1237 | *
1238 | * @since 5.2.0
1239 | * @param int $connection Connection index.
1240 | * @param string $name Connection name.
1241 | */
1242 | protected function increment_db_connection( $connection = 0, $name = '' ) {
1243 |
1244 | // Bail if name is empty
1245 | if ( empty( $name ) ) {
1246 | return;
1247 | }
1248 |
1249 | // Initialize the connection counter
1250 | if ( ! isset( $this->db_connections[ $connection ] ) ) {
1251 | $this->db_connections[ $connection ] = array();
1252 | }
1253 |
1254 | // Increment the connection counter
1255 | if ( ! isset( $this->db_connections[ $connection ][ $name ] ) ) {
1256 | $this->db_connections[ $connection ][ $name ] = 1;
1257 | } else {
1258 | ++$this->db_connections[ $connection ][ $name ];
1259 | }
1260 | }
1261 |
1262 | /**
1263 | * Connect selected database
1264 | *
1265 | * @since 1.0.0
1266 | *
1267 | * @param string $dbhname Database name.
1268 | * @param string $host Internet address: host:port of server on internet.
1269 | * @param string $user Database user.
1270 | * @param string $password Database password.
1271 | *
1272 | * @return bool|mysqli|resource
1273 | */
1274 | protected function single_db_connect( $dbhname, $host, $user, $password ) {
1275 | $this->is_mysql = true;
1276 |
1277 | // Check client flags
1278 | $client_flags = defined( 'MYSQL_CLIENT_FLAGS' )
1279 | ? MYSQL_CLIENT_FLAGS
1280 | : 0;
1281 |
1282 | // Initialize the database handle
1283 | $this->dbhs[ $dbhname ] = mysqli_init();
1284 |
1285 | /**
1286 | * mysqli_real_connect doesn't support the "host" param including a port
1287 | * or socket like mysql_connect does. This duplicates how mysql_connect
1288 | * detects a port and/or socket file.
1289 | */
1290 | $port = 0;
1291 | $socket = '';
1292 | $port_or_socket = strstr( $host, ':' );
1293 |
1294 | if ( ! empty( $port_or_socket ) ) {
1295 | $host = substr( $host, 0, strpos( $host, ':' ) );
1296 | $port_or_socket = substr( $port_or_socket, 1 );
1297 |
1298 | if ( 0 !== strpos( $port_or_socket, '/' ) ) {
1299 | $port = intval( $port_or_socket );
1300 | $maybe_socket = strstr( $port_or_socket, ':' );
1301 |
1302 | if ( ! empty( $maybe_socket ) ) {
1303 | $socket = substr( $maybe_socket, 1 );
1304 | }
1305 | } else {
1306 | $socket = $port_or_socket;
1307 | }
1308 | }
1309 |
1310 | /**
1311 | * If DB_HOST begins with a 'p:', allow it to be passed to
1312 | * mysqli_real_connect(). mysqli supports persistent connections
1313 | * starting with PHP 5.3.0.
1314 | */
1315 | if (
1316 | ( true === $this->persistent )
1317 | &&
1318 | version_compare( phpversion(), '5.3.0', '>=' )
1319 | ) {
1320 | $pre_host = 'p:';
1321 | } else {
1322 | $pre_host = '';
1323 | }
1324 |
1325 | // Connect to the database
1326 | mysqli_real_connect(
1327 | $this->dbhs[ $dbhname ],
1328 | $pre_host . $host,
1329 | $user,
1330 | $password,
1331 | '',
1332 | $port,
1333 | $socket,
1334 | $client_flags
1335 | );
1336 |
1337 | // Bail if connection failed
1338 | if ( ! empty( $this->dbhs[ $dbhname ]->connect_errno ) ) {
1339 | $this->dbhs[ $dbhname ] = false;
1340 |
1341 | return false;
1342 | }
1343 | }
1344 |
1345 | /**
1346 | * Change the current SQL mode, and ensure its WordPress compatibility.
1347 | *
1348 | * If no modes are passed, it will ensure the current MySQL server
1349 | * modes are compatible.
1350 | *
1351 | * @since 1.0.0
1352 | *
1353 | * @param array $modes Optional. A list of SQL modes to set.
1354 | * @param false|string|mysqli|resource $dbh_or_table Optional. The database. One of:
1355 | * - the current database
1356 | * - the database housing the specified table
1357 | * - the database of the MySQL resource
1358 | * @return void
1359 | */
1360 | public function set_sql_mode( $modes = array(), $dbh_or_table = false ) {
1361 | $dbh = $this->get_db_object( $dbh_or_table );
1362 |
1363 | if ( ! $this->dbh_type_check( $dbh ) ) {
1364 | return;
1365 | }
1366 |
1367 | if ( empty( $modes ) ) {
1368 | $res = mysqli_query( $dbh, 'SELECT @@SESSION.sql_mode' );
1369 |
1370 | if ( empty( $res ) ) {
1371 | return;
1372 | }
1373 |
1374 | $modes_array = mysqli_fetch_array( $res );
1375 | if ( empty( $modes_array[0] ) ) {
1376 | return;
1377 | }
1378 |
1379 | $modes_str = $modes_array[0];
1380 |
1381 | if ( empty( $modes_str ) ) {
1382 | return;
1383 | }
1384 |
1385 | $modes = explode( ',', $modes_str );
1386 | }
1387 |
1388 | $modes = array_change_key_case( $modes, CASE_UPPER );
1389 |
1390 | /**
1391 | * Filter the list of incompatible SQL modes to exclude.
1392 | *
1393 | * @param array $incompatible_modes An array of incompatible modes.
1394 | */
1395 | $incompatible_modes = (array) apply_filters( 'incompatible_sql_modes', $this->incompatible_modes );
1396 | foreach ( $modes as $i => $mode ) {
1397 | if ( in_array( $mode, $incompatible_modes, true ) ) {
1398 | unset( $modes[ $i ] );
1399 | }
1400 | }
1401 |
1402 | $modes_str = implode( ',', $modes );
1403 |
1404 | mysqli_query( $dbh, "SET SESSION sql_mode='{$modes_str}'" );
1405 | }
1406 |
1407 | /**
1408 | * Selects a database using the current database connection.
1409 | *
1410 | * The database name will be changed based on the current database
1411 | * connection. On failure, the execution will bail and display an DB error.
1412 | *
1413 | * @since 1.0.0
1414 | *
1415 | * @param string $db MySQL database name.
1416 | * @param false|string|mysqli|resource $dbh_or_table Optional. The database. One of:
1417 | * - the current database
1418 | * - the database housing the specified table
1419 | * - the database of the MySQL resource
1420 | */
1421 | public function select( $db, $dbh_or_table = false ) {
1422 | $dbh = $this->get_db_object( $dbh_or_table );
1423 |
1424 | if ( ! $this->dbh_type_check( $dbh ) ) {
1425 | return false;
1426 | }
1427 |
1428 | $success = mysqli_select_db( $dbh, $db );
1429 |
1430 | return $success;
1431 | }
1432 |
1433 | /**
1434 | * Load the column metadata from the last query.
1435 | *
1436 | * @since 1.0.0
1437 | *
1438 | * @access protected
1439 | */
1440 | protected function load_col_info() {
1441 |
1442 | // Bail if not enough info
1443 | if (
1444 | ! empty( $this->col_info )
1445 | ||
1446 | ( false === $this->result )
1447 | ) {
1448 | return;
1449 | }
1450 |
1451 | $this->col_info = array();
1452 |
1453 | $num_fields = mysqli_num_fields( $this->result );
1454 |
1455 | for ( $i = 0; $i < $num_fields; $i++ ) {
1456 | $this->col_info[ $i ] = mysqli_fetch_field( $this->result );
1457 | }
1458 | }
1459 |
1460 | /**
1461 | * Force addslashes() for the escapes
1462 | *
1463 | * LudicrousDB makes connections when a query is made which is why we can't
1464 | * use mysql_real_escape_string() for escapes
1465 | *
1466 | * This is also the reason why we don't allow certain charsets.
1467 | *
1468 | * See set_charset().
1469 | *
1470 | * @since 1.0.0
1471 | * @param string $to_escape String to escape.
1472 | */
1473 | public function _real_escape( $to_escape = '' ) { // phpcs:ignore PSR2.Methods.MethodDeclaration.Underscore
1474 |
1475 | // Bail if not a scalar
1476 | if ( ! is_scalar( $to_escape ) ) {
1477 | return '';
1478 | }
1479 |
1480 | // Slash the query part
1481 | $escaped = addslashes( $to_escape );
1482 |
1483 | // Maybe use WordPress core placeholder method
1484 | if ( method_exists( $this, 'add_placeholder_escape' ) ) {
1485 | $escaped = $this->add_placeholder_escape( $escaped );
1486 | }
1487 |
1488 | return $escaped;
1489 | }
1490 |
1491 | /**
1492 | * Sets the connection's character set
1493 | *
1494 | * @since 1.0.0
1495 | *
1496 | * @param mysqli|resource $dbh The resource given by mysqli_real_connect
1497 | * @param string $charset Optional. The character set.
1498 | * @param string $collate Optional. The collation.
1499 | */
1500 | public function set_charset( $dbh, $charset = null, $collate = null ) {
1501 |
1502 | // Default charset
1503 | if ( ! isset( $charset ) ) {
1504 | $charset = $this->charset;
1505 | }
1506 |
1507 | // Default collation
1508 | if ( ! isset( $collate ) ) {
1509 | $collate = $this->collate;
1510 | }
1511 |
1512 | // Exit if charset or collation are empty
1513 | if ( empty( $charset ) || empty( $collate ) ) {
1514 | wp_die( "{$charset} {$collate}" );
1515 | }
1516 |
1517 | // Exit if charset is not allowed
1518 | if ( ! in_array( strtolower( $charset ), self::$allowed_charsets, true ) ) {
1519 | wp_die( "{$charset} charset isn't supported in LudicrousDB for security reasons" );
1520 | }
1521 |
1522 | // Bail if cannot set collation
1523 | if ( ! $this->has_cap( 'collation', $dbh ) ) {
1524 | return;
1525 | }
1526 |
1527 | // Attempt to set the character set
1528 | $do_set_names_query = $this->has_cap( 'set_charset', $dbh )
1529 | ? mysqli_set_charset( $dbh, $charset )
1530 | : true;
1531 |
1532 | // Bail if client charset could not be set
1533 | if ( false === $do_set_names_query ) {
1534 | return;
1535 | }
1536 |
1537 | // Start the query with charset
1538 | $query = $this->prepare( 'SET NAMES %s', $charset );
1539 |
1540 | // Maybe add collation to query
1541 | if ( ! empty( $collate ) ) {
1542 | $query .= $this->prepare( ' COLLATE %s', $collate );
1543 | }
1544 |
1545 | // Do the query
1546 | $this->_do_query( $query, $dbh );
1547 | }
1548 |
1549 | /**
1550 | * Disconnect and remove connection from open connections list
1551 | *
1552 | * @since 1.0.0
1553 | *
1554 | * @param string $dbhname Database name.
1555 | */
1556 | public function disconnect( $dbhname ) {
1557 | $key = array_search( $dbhname, $this->open_connections, true );
1558 |
1559 | if ( $key !== false ) {
1560 | unset( $this->open_connections[ $key ] );
1561 | }
1562 |
1563 | if ( $this->dbh_type_check( $this->dbhs[ $dbhname ] ) ) {
1564 | $this->close( $this->dbhs[ $dbhname ] );
1565 | }
1566 |
1567 | unset( $this->dbhs[ $dbhname ] );
1568 | }
1569 |
1570 | /**
1571 | * Kill cached query results
1572 | *
1573 | * @since 1.0.0
1574 | */
1575 | public function flush() {
1576 | $this->last_error = '';
1577 | $this->num_rows = 0;
1578 | parent::flush();
1579 | }
1580 |
1581 | /**
1582 | * Check that the connection to the database is still up. If not, try
1583 | * to reconnect.
1584 | *
1585 | * This function is called internally by LudicrousDB when a database
1586 | * connection
1587 | *
1588 | * If this function is unable to reconnect, it will forcibly die, or if
1589 | * after the "template_redirect" hook has been fired, return false instead.
1590 | *
1591 | * If $die_on_disconnect is false, the lack of database connection will need
1592 | * to be handled manually.
1593 | *
1594 | * @since 1.0.0
1595 | *
1596 | * @param bool $die_on_disconnect Optional. Allows the function to die. Default true.
1597 | * @param bool $dbh_or_table Optional.
1598 | * @param string $query Optional. Query string passed db_connect
1599 | *
1600 | * @return bool|void True if the connection is up.
1601 | */
1602 | public function check_connection( $die_on_disconnect = true, $dbh_or_table = false, $query = '' ) {
1603 | $dbh = $this->get_db_object( $dbh_or_table );
1604 |
1605 | // Return true if ping is successful. This is the most common case.
1606 | if (
1607 | $this->dbh_type_check( $dbh )
1608 | &&
1609 | mysqli_ping( $dbh )
1610 | ) {
1611 | return true;
1612 | }
1613 |
1614 | // Default to false
1615 | $error_reporting = false;
1616 |
1617 | // Disable warnings, as we don't want to see a multitude of "unable to connect" messages
1618 | if ( $this->is_debug() ) {
1619 | $error_reporting = error_reporting();
1620 | error_reporting( $error_reporting & ~E_WARNING );
1621 | }
1622 |
1623 | // Ping failed, so try to reconnect manually
1624 | for ( $tries = 1; $tries <= $this->reconnect_retries; $tries++ ) {
1625 |
1626 | // Try to reconnect
1627 | $retry = $this->db_connect( $query );
1628 |
1629 | // Return true if the connection is up
1630 | if ( false !== $retry ) {
1631 | return true;
1632 |
1633 | // On the last try, re-enable warnings. We want to see a single
1634 | // instance of the "unable to connect" message on the bail()
1635 | // screen, if it appears.
1636 | } elseif (
1637 | ( $this->reconnect_retries === $tries )
1638 | &&
1639 | $this->is_debug()
1640 | ) {
1641 | error_reporting( $error_reporting );
1642 | }
1643 |
1644 | // Sleep before retrying
1645 | sleep( $this->reconnect_sleep );
1646 | }
1647 |
1648 | // Bail here if not allowed to call $this->bail()
1649 | if ( false === $die_on_disconnect ) {
1650 | return false;
1651 | }
1652 |
1653 | // Bail if template_redirect has already happened, because it's too
1654 | // late for wp_die()/dead_db()
1655 | if ( did_action( 'template_redirect' ) ) {
1656 | return false;
1657 | }
1658 |
1659 | // Load translations early so that the error message can be translated
1660 | wp_load_translations_early();
1661 |
1662 | $message = '' . __( 'Error reconnecting to the database', 'ludicrousdb' ) . "
\n";
1663 | $message .= '' . sprintf(
1664 | /* translators: %s: database host */
1665 | __( 'This means that we lost contact with the database server at %s. This could mean your host’s database server is down.', 'ludicrousdb' ),
1666 | '' . htmlspecialchars( $this->dbhost, ENT_QUOTES ) . '
'
1667 | ) . "
\n";
1668 | $message .= "\n";
1669 | $message .= '- ' . __( 'Are you sure that the database server is running?', 'ludicrousdb' ) . "
\n";
1670 | $message .= '- ' . __( 'Are you sure that the database server is not under particularly heavy load?', 'ludicrousdb' ) . "
\n";
1671 | $message .= "
\n";
1672 | $message .= '' . sprintf(
1673 | /* translators: %s: support forums URL */
1674 | __( 'If you’re unsure what these terms mean you should probably contact your host. If you still need help you can always visit the WordPress Support Forums.', 'ludicrousdb' ),
1675 | __( 'https://wordpress.org/support/', 'ludicrousdb' )
1676 | ) . "
\n";
1677 |
1678 | // We weren't able to reconnect, so we better bail.
1679 | $this->bail( $message, 'db_connect_fail' );
1680 |
1681 | // Call dead_db() if bail didn't die, because this database is no more.
1682 | // It has ceased to be (at least temporarily).
1683 | dead_db();
1684 | }
1685 |
1686 | /**
1687 | * Basic query. See documentation for more details.
1688 | *
1689 | * @since 1.0.0
1690 | * @since 5.2.0 Added support for SELECT modifiers (e.g. DISTINCT, HIGH_PRIORITY, etc...)
1691 | *
1692 | * @param string $query Query.
1693 | *
1694 | * @return int number of rows
1695 | */
1696 | public function query( $query ) {
1697 |
1698 | // Default return value
1699 | $retval = 0;
1700 |
1701 | $this->flush();
1702 |
1703 | // Some queries are made before plugins are loaded
1704 | if ( function_exists( 'apply_filters' ) ) {
1705 |
1706 | /**
1707 | * Filter the database query.
1708 | *
1709 | * Some queries are made before the plugins have been loaded,
1710 | * and thus cannot be filtered with this method.
1711 | *
1712 | * @param string $query Database query.
1713 | */
1714 | $query = apply_filters( 'query', $query );
1715 | }
1716 |
1717 | // Some queries are made before plugins are loaded
1718 | if ( function_exists( 'apply_filters_ref_array' ) ) {
1719 |
1720 | /**
1721 | * Filter the return value before the query is run.
1722 | *
1723 | * Passing a non-null value to the filter will effectively short-circuit
1724 | * the DB query, stopping it from running, then returning this value instead.
1725 | *
1726 | * This uses apply_filters_ref_array() to allow $this to be manipulated, so
1727 | * values can be set just-in-time to match your particular use-case.
1728 | *
1729 | * You probably will never need to use this filter, but if you do, there's
1730 | * no other way to do what you're trying to do, so here you go!
1731 | *
1732 | * @since 4.0.0
1733 | *
1734 | * @param string null The filtered return value. Default is null.
1735 | * @param string $query Database query.
1736 | * @param LudicrousDB &$this Current instance of LudicrousDB, passed by reference.
1737 | */
1738 | $retval = apply_filters_ref_array( 'pre_query', array( null, $query, &$this ) );
1739 | if ( null !== $retval ) {
1740 | $this->run_query_log_callbacks( $query, $retval );
1741 |
1742 | return $retval;
1743 | }
1744 | }
1745 |
1746 | // Bail if query is empty (via application error or 'query' filter)
1747 | if ( empty( $query ) ) {
1748 | $this->run_query_log_callbacks( $query, $retval );
1749 |
1750 | return $retval;
1751 | }
1752 |
1753 | // Log how the function was called
1754 | $this->func_call = "\$db->query(\"{$query}\")";
1755 |
1756 | // If we're writing to the database, make sure the query will write safely.
1757 | if (
1758 | $this->check_current_query
1759 | &&
1760 | ! $this->check_ascii( $query )
1761 | ) {
1762 | $stripped_query = $this->strip_invalid_text_from_query( $query );
1763 |
1764 | // strip_invalid_text_from_query() may perform queries, so
1765 | // flush again to make sure everything is clear.
1766 | $this->flush();
1767 |
1768 | if ( $stripped_query !== $query ) {
1769 | $this->insert_id = 0;
1770 | $retval = false;
1771 |
1772 | $this->run_query_log_callbacks( $query, $retval );
1773 |
1774 | return $retval;
1775 | }
1776 | }
1777 |
1778 | $this->check_current_query = true;
1779 |
1780 | // Keep track of the last query for debug..
1781 | $this->last_query = $query;
1782 |
1783 | if (
1784 | preg_match( '/^\s*SELECT\s+FOUND_ROWS(\s*)/i', $query )
1785 | &&
1786 | ( $this->last_found_rows_result instanceof mysqli_result )
1787 | ) {
1788 | $this->result = $this->last_found_rows_result;
1789 | $elapsed = 0;
1790 |
1791 | } else {
1792 | $this->dbh = $this->db_connect( $query );
1793 |
1794 | if ( ! $this->dbh_type_check( $this->dbh ) ) {
1795 | $this->run_query_log_callbacks( $query, $retval );
1796 |
1797 | return false;
1798 | }
1799 |
1800 | $this->timer_start();
1801 | $this->result = $this->_do_query( $query, $this->dbh );
1802 | $elapsed = $this->timer_stop();
1803 |
1804 | ++$this->num_queries;
1805 |
1806 | if ( preg_match( '/^\s*SELECT\s+([A-Z_]+\s+)*SQL_CALC_FOUND_ROWS\s/i', $query ) ) {
1807 | if ( false === strpos( $query, 'NO_SELECT_FOUND_ROWS' ) ) {
1808 | $this->timer_start();
1809 | $this->last_found_rows_result = $this->_do_query( 'SELECT FOUND_ROWS()', $this->dbh );
1810 | $elapsed += $this->timer_stop();
1811 | ++$this->num_queries;
1812 | $query .= '; SELECT FOUND_ROWS()';
1813 | }
1814 | } else {
1815 | $this->last_found_rows_result = null;
1816 | }
1817 |
1818 | if (
1819 | ! empty( $this->save_queries )
1820 | ||
1821 | $this->is_saving_queries()
1822 | ) {
1823 | $this->log_query(
1824 | $query,
1825 | $elapsed,
1826 | $this->get_caller(),
1827 | $this->time_start,
1828 | array()
1829 | );
1830 | }
1831 | }
1832 |
1833 | // If there is an error then take note of it
1834 | if ( $this->dbh_type_check( $this->dbh ) ) {
1835 | $this->last_error = mysqli_error( $this->dbh );
1836 | }
1837 |
1838 | if ( ! empty( $this->last_error ) ) {
1839 | $this->print_error( $this->last_error );
1840 | $retval = false;
1841 |
1842 | $this->run_query_log_callbacks( $query, $retval );
1843 |
1844 | return $retval;
1845 | }
1846 |
1847 | if ( preg_match( '/^\s*(create|alter|truncate|drop)\s/i', $query ) ) {
1848 | $retval = $this->result;
1849 |
1850 | } elseif ( preg_match( '/^\\s*(insert|delete|update|replace|alter) /i', $query ) ) {
1851 | $this->rows_affected = mysqli_affected_rows( $this->dbh );
1852 |
1853 | // Take note of the insert_id
1854 | if ( preg_match( '/^\s*(insert|replace)\s/i', $query ) ) {
1855 | $this->insert_id = mysqli_insert_id( $this->dbh );
1856 | }
1857 |
1858 | // Return number of rows affected
1859 | $retval = $this->rows_affected;
1860 |
1861 | } else {
1862 | $num_rows = 0;
1863 | $this->last_result = array();
1864 |
1865 | if ( $this->result instanceof mysqli_result ) {
1866 | $this->load_col_info();
1867 |
1868 | while ( $row = mysqli_fetch_object( $this->result ) ) { // phpcs:ignore Generic.CodeAnalysis.AssignmentInCondition
1869 | $this->last_result[ $num_rows ] = $row;
1870 |
1871 | // phpcs:ignore
1872 | $num_rows++;
1873 | }
1874 | }
1875 |
1876 | // Log number of rows the query returned
1877 | // and return number of rows selected
1878 | $this->num_rows = $num_rows;
1879 | $retval = $num_rows;
1880 | }
1881 |
1882 | $this->run_query_log_callbacks( $query, $retval );
1883 |
1884 | // Some queries are made before plugins are loaded
1885 | if ( function_exists( 'do_action_ref_array' ) ) {
1886 |
1887 | /**
1888 | * Runs after a query is finished.
1889 | *
1890 | * @since 4.0.0
1891 | *
1892 | * @param string $query Database query.
1893 | * @param LudicrousDB &$this Current instance of LudicrousDB, passed by reference.
1894 | */
1895 | do_action_ref_array( 'queried', array( $query, &$this ) );
1896 | }
1897 |
1898 | // Return number of rows
1899 | return $retval;
1900 | }
1901 |
1902 | /**
1903 | * Internal function to perform the mysqli_query() call
1904 | *
1905 | * @since 1.0.0
1906 | *
1907 | * @access protected
1908 | * @see wpdb::query()
1909 | *
1910 | * @param string $query The query to run.
1911 | * @param bool $dbh_or_table Database or table name. Defaults to false.
1912 | * @throws Throwable If the query fails.
1913 | */
1914 | protected function _do_query( $query, $dbh_or_table = false ) { // phpcs:ignore PSR2.Methods.MethodDeclaration.Underscore
1915 | $dbh = $this->get_db_object( $dbh_or_table );
1916 |
1917 | if ( ! $this->dbh_type_check( $dbh ) ) {
1918 | return false;
1919 | }
1920 |
1921 | // Try to execute the query
1922 | try {
1923 | $result = mysqli_query( $dbh, $query );
1924 |
1925 | // Catch any exceptions
1926 | } catch ( Throwable $exception ) {
1927 | if ( true === $this->suppress_errors ) {
1928 | $result = false;
1929 | } else {
1930 | throw $exception;
1931 | }
1932 | }
1933 |
1934 | // Maybe log last used to heartbeats
1935 | if ( ! empty( $this->check_dbh_heartbeats ) ) {
1936 |
1937 | // Lookup name
1938 | $name = $this->lookup_dbhs_name( $dbh );
1939 |
1940 | // Set last used for this dbh
1941 | if ( ! empty( $name ) ) {
1942 | $this->dbhname_heartbeats[ $name ]['last_used'] = microtime( true );
1943 | }
1944 | }
1945 |
1946 | return $result;
1947 | }
1948 |
1949 | /**
1950 | * Closes the current database connection.
1951 | *
1952 | * @since 1.0.0
1953 | *
1954 | * @param false|string|mysqli|resource $dbh_or_table Optional. The database. One of:
1955 | * - the current database
1956 | * - the database housing the specified table
1957 | * - the database of the MySQL resource
1958 | *
1959 | * @return bool True if the connection was successfully closed. False if it wasn't
1960 | * or the connection doesn't exist.
1961 | */
1962 | public function close( $dbh_or_table = false ) {
1963 | $dbh = $this->get_db_object( $dbh_or_table );
1964 |
1965 | if ( ! $this->dbh_type_check( $dbh ) ) {
1966 | return false;
1967 | }
1968 |
1969 | $closed = mysqli_close( $dbh );
1970 |
1971 | if ( ! empty( $closed ) ) {
1972 | $this->dbh = null;
1973 | }
1974 |
1975 | return $closed;
1976 | }
1977 |
1978 | /**
1979 | * Whether or not MySQL database is at least the required minimum version.
1980 | * The additional argument allows the caller to check a specific database.
1981 | *
1982 | * @since 1.0.0
1983 | *
1984 | * @global $wp_version
1985 | * @global $required_mysql_version
1986 | *
1987 | * @param false|string|mysqli|resource $dbh_or_table Optional. The database. One of:
1988 | * - the current database
1989 | * - the database housing the specified table
1990 | * - the database of the MySQL resource
1991 | *
1992 | * @return WP_Error
1993 | */
1994 | public function check_database_version( $dbh_or_table = false ) {
1995 | global $wp_version, $required_mysql_version;
1996 |
1997 | // Make sure the server has the required MySQL version
1998 | $mysql_version = preg_replace( '|[^0-9\.]|', '', $this->db_version( $dbh_or_table ) );
1999 | if ( version_compare( $mysql_version, $required_mysql_version, '<' ) ) {
2000 | // translators: 1. WordPress version, 2. MySql Version.
2001 | return new WP_Error( 'database_version', sprintf( __( 'ERROR: WordPress %1$s requires MySQL %2$s or higher', 'ludicrousdb' ), $wp_version, $required_mysql_version ) );
2002 | }
2003 | }
2004 |
2005 | /**
2006 | * This function is called when WordPress is generating the table schema to
2007 | * determine whether or not the current database supports or needs the
2008 | * collation statements.
2009 | *
2010 | * The additional argument allows the caller to check a specific database.
2011 | *
2012 | * @since 1.0.0
2013 | *
2014 | * @param false|string|mysqli|resource $dbh_or_table Optional. The database. One of:
2015 | * - the current database
2016 | * - the database housing the specified table
2017 | * - the database of the MySQL resource
2018 | *
2019 | * @return bool
2020 | */
2021 | public function supports_collation( $dbh_or_table = false ) {
2022 | _deprecated_function( __FUNCTION__, '3.5', 'wpdb::has_cap( \'collation\' )' );
2023 |
2024 | return $this->has_cap( 'collation', $dbh_or_table );
2025 | }
2026 |
2027 | /**
2028 | * Generic function to determine if a database supports a particular feature.
2029 | * The additional argument allows the caller to check a specific database.
2030 | *
2031 | * @since 1.0.0
2032 | *
2033 | * @param string $db_cap The feature.
2034 | * @param false|string|mysqli|resource $dbh_or_table Optional. The database. One of:
2035 | * - the current database
2036 | * - the database housing the specified table
2037 | * - the database of the MySQL resource
2038 | *
2039 | * @return bool
2040 | */
2041 | public function has_cap( $db_cap, $dbh_or_table = false ) {
2042 | $db_version = $this->db_version( $dbh_or_table );
2043 | $db_server_info = $this->db_server_info( $dbh_or_table );
2044 |
2045 | // Account for MariaDB version being prefixed with '5.5.5-' on older PHP versions.
2046 | // See: https://github.com/Automattic/HyperDB/pull/143
2047 | if (
2048 | ( '5.5.5' === $db_version )
2049 | &&
2050 | ( false !== strpos( $db_server_info, 'MariaDB' ) )
2051 | &&
2052 | version_compare( phpversion(), '8.0.16', '<' ) // PHP 8.0.15 or older.
2053 | ) {
2054 | // Strip the '5.5.5-' prefix and set the version to the correct value.
2055 | $db_server_info = preg_replace( '/^5\.5\.5-(.*)/', '$1', $db_server_info );
2056 | $db_version = preg_replace( '/[^0-9.].*/', '', $db_server_info );
2057 | }
2058 |
2059 | switch ( strtolower( $db_cap ) ) {
2060 | case 'collation': // @since WP 2.5.0
2061 | case 'group_concat': // @since WP 2.7.0
2062 | case 'subqueries': // @since WP 2.7.0
2063 | return version_compare( $db_version, '4.1', '>=' );
2064 | case 'set_charset':
2065 | return version_compare( $db_version, '5.0.7', '>=' );
2066 | case 'utf8mb4': // @since WP 4.1.0
2067 | if ( version_compare( $db_version, '5.5.3', '<' ) ) {
2068 | return false;
2069 | }
2070 |
2071 | $dbh = $this->get_db_object( $dbh_or_table );
2072 |
2073 | if ( $this->dbh_type_check( $dbh ) ) {
2074 | $client_version = mysqli_get_client_info();
2075 |
2076 | /*
2077 | * libmysql has supported utf8mb4 since 5.5.3, same as the MySQL server.
2078 | * mysqlnd has supported utf8mb4 since 5.0.9.
2079 | */
2080 | if ( false !== strpos( $client_version, 'mysqlnd' ) ) {
2081 | $client_version = preg_replace( '/^\D+([\d.]+).*/', '$1', $client_version );
2082 |
2083 | return version_compare( $client_version, '5.0.9', '>=' );
2084 | } else {
2085 | return version_compare( $client_version, '5.5.3', '>=' );
2086 | }
2087 | }
2088 | break;
2089 | case 'utf8mb4_520': // @since WP 4.6.0
2090 | return version_compare( $db_version, '5.6', '>=' );
2091 | }
2092 |
2093 | return false;
2094 | }
2095 |
2096 | /**
2097 | * Retrieve the name of the function that called wpdb.
2098 | *
2099 | * Searches up the list of functions until it reaches
2100 | * the one that would most logically had called this method.
2101 | *
2102 | * @since 2.5.0
2103 | *
2104 | * @return string Comma separated list of the calling functions.
2105 | */
2106 | public function get_caller() {
2107 | return ( true === $this->save_backtrace )
2108 | ? wp_debug_backtrace_summary( __CLASS__ )
2109 | : null;
2110 | }
2111 |
2112 | /**
2113 | * The database version number.
2114 | *
2115 | * @since 1.0.0
2116 | *
2117 | * @param false|string|mysqli|resource $dbh_or_table Optional. The database. One of:
2118 | * - the current database
2119 | * - the database housing the specified table
2120 | * - the database of the MySQL resource
2121 | *
2122 | * @return false|string False on failure. Version number on success.
2123 | */
2124 | public function db_version( $dbh_or_table = false ) {
2125 | return preg_replace( '/[^0-9.].*/', '', $this->db_server_info( $dbh_or_table ) );
2126 | }
2127 |
2128 | /**
2129 | * Retrieves full MySQL server information.
2130 | *
2131 | * @since 5.0.0
2132 | *
2133 | * @param false|string|mysqli|resource $dbh_or_table Optional. The database. One of:
2134 | * - the current database
2135 | * - the database housing the specified table
2136 | * - the database of the MySQL resource
2137 | *
2138 | * @return string|false Server info on success, false on failure.
2139 | */
2140 | public function db_server_info( $dbh_or_table = false ) {
2141 | $dbh = $this->get_db_object( $dbh_or_table );
2142 |
2143 | if ( ! $this->dbh_type_check( $dbh ) ) {
2144 | return false;
2145 | }
2146 |
2147 | $server_info = mysqli_get_server_info( $dbh );
2148 |
2149 | return $server_info;
2150 | }
2151 |
2152 | /**
2153 | * Get the db connection object
2154 | *
2155 | * @since 1.0.0
2156 | *
2157 | * @param false|string|mysqli|resource $dbh_or_table Optional. The database. One of:
2158 | * - the current database
2159 | * - the database housing the specified table
2160 | * - the database of the MySQL resource
2161 | * @return bool|mysqli|resource
2162 | */
2163 | private function get_db_object( $dbh_or_table = false ) {
2164 |
2165 | // No database
2166 | $dbh = false;
2167 |
2168 | // Database
2169 | if ( $this->dbh_type_check( $dbh_or_table ) ) {
2170 | $dbh = &$dbh_or_table;
2171 |
2172 | // Database
2173 | } elseif (
2174 | ( false === $dbh_or_table )
2175 | &&
2176 | $this->dbh_type_check( $this->dbh )
2177 | ) {
2178 | $dbh = &$this->dbh;
2179 |
2180 | // Table name
2181 | } elseif ( is_string( $dbh_or_table ) ) {
2182 | $dbh = $this->db_connect( "SELECT FROM {$dbh_or_table} {$this->users}" );
2183 | }
2184 |
2185 | return $dbh;
2186 | }
2187 |
2188 | /**
2189 | * Check database object type.
2190 | *
2191 | * @since 1.0.0
2192 | *
2193 | * @param mysqli|resource $dbh Database resource.
2194 | *
2195 | * @return bool
2196 | */
2197 | private function dbh_type_check( $dbh ) {
2198 | if ( $dbh instanceof mysqli ) {
2199 | return true;
2200 | } elseif ( is_resource( $dbh ) ) {
2201 | return true;
2202 | }
2203 |
2204 | return false;
2205 | }
2206 |
2207 | /**
2208 | * Logs query data.
2209 | *
2210 | * @since 4.3.0
2211 | *
2212 | * @param string $query The query's SQL.
2213 | * @param float $query_time Total time spent on the query, in seconds.
2214 | * @param string $query_callstack Comma separated list of the calling functions.
2215 | * @param float $query_start Unix timestamp of the time at the start of the query.
2216 | * @param array $query_data Custom query data.
2217 | * }
2218 | */
2219 | public function log_query( $query = '', $query_time = 0, $query_callstack = '', $query_start = 0, $query_data = array() ) {
2220 |
2221 | /**
2222 | * Filters the custom query data being logged.
2223 | *
2224 | * Caution should be used when modifying any of this data, it is recommended that any additional
2225 | * information you need to store about a query be added as a new associative entry to the fourth
2226 | * element $query_data.
2227 | *
2228 | * @since 5.3.0
2229 | *
2230 | * @param array $query_data Custom query data.
2231 | * @param string $query The query's SQL.
2232 | * @param float $query_time Total time spent on the query, in seconds.
2233 | * @param string $query_callstack Comma separated list of the calling functions.
2234 | * @param float $query_start Unix timestamp of the time at the start of the query.
2235 | */
2236 | $query_data = apply_filters( 'log_query_custom_data', $query_data, $query, $query_time, $query_callstack, $query_start );
2237 |
2238 | // Pass to custom callback...
2239 | if ( is_callable( $this->save_query_callback ) ) {
2240 | $this->queries[] = call_user_func_array(
2241 | $this->save_query_callback,
2242 | array(
2243 | $query,
2244 | $query_time,
2245 | $query_callstack,
2246 | $query_start,
2247 | $query_data,
2248 | &$this,
2249 | )
2250 | );
2251 |
2252 | // ...or save it to the queries array
2253 | } else {
2254 | $this->queries[] = array(
2255 | $query,
2256 | $query_time,
2257 | $query_callstack,
2258 | $query_start,
2259 | $query_data,
2260 | );
2261 | }
2262 | }
2263 |
2264 | /**
2265 | * Check the responsiveness of a TCP/IP daemon
2266 | *
2267 | * @since 1.0.0
2268 | *
2269 | * @param string $host Host.
2270 | * @param int $port Port or socket.
2271 | * @param float $float_timeout Timeout in seconds, as float number ().
2272 | *
2273 | * @return bool true when $host:$post responds within $float_timeout seconds, else false
2274 | */
2275 | public function check_tcp_responsiveness( $host, $port, $float_timeout ) {
2276 |
2277 | // Get the cache key
2278 | $cache_key = $this->tcp_get_cache_key( $host, $port );
2279 |
2280 | // Persistent cached value exists
2281 | $cached_value = $this->tcp_cache_get( $cache_key );
2282 |
2283 | // Confirmed up or down response
2284 | if ( 'up' === $cached_value ) {
2285 | $this->tcp_responsive = true;
2286 |
2287 | return true;
2288 | } elseif ( 'down' === $cached_value ) {
2289 | $this->tcp_responsive = false;
2290 |
2291 | return false;
2292 | }
2293 |
2294 | // Defaults
2295 | $errno = 0;
2296 | $errstr = '';
2297 |
2298 | // Try to get a new socket
2299 | // phpcs:disable
2300 | $socket = $this->is_debug()
2301 | ? fsockopen( $host, $port, $errno, $errstr, $float_timeout )
2302 | : @fsockopen( $host, $port, $errno, $errstr, $float_timeout );
2303 | // phpcs:enable
2304 |
2305 | // No socket
2306 | if ( false === $socket ) {
2307 | $this->tcp_cache_set( $cache_key, 'down' );
2308 |
2309 | return "[ > {$float_timeout} ] ({$errno}) '{$errstr}'";
2310 | }
2311 |
2312 | // Close the socket
2313 | // phpcs:ignore
2314 | fclose( $socket );
2315 |
2316 | // Using API
2317 | $this->tcp_cache_set( $cache_key, 'up' );
2318 |
2319 | return true;
2320 | }
2321 |
2322 | /**
2323 | * Run lag cache callbacks and return current lag
2324 | *
2325 | * @since 2.1.0
2326 | *
2327 | * @return int
2328 | */
2329 | public function get_lag_cache() {
2330 | $this->lag = $this->run_callbacks( 'get_lag_cache' );
2331 |
2332 | return $this->check_lag();
2333 | }
2334 |
2335 | /**
2336 | * Run query log callbacks and return the return value.
2337 | *
2338 | * @since 5.2.0
2339 | *
2340 | * @param string $query The query's SQL.
2341 | * @param mixed $retval The return value of the query.
2342 | *
2343 | * @return void
2344 | */
2345 | public function run_query_log_callbacks( $query = '', $retval = null ) {
2346 |
2347 | // Setup the callback data
2348 | $callback_data = array(
2349 | $query,
2350 | $retval,
2351 | $this->last_error,
2352 | );
2353 |
2354 | // Run the callbacks
2355 | $this->run_callbacks( 'sql_query_log', $callback_data );
2356 | }
2357 |
2358 | /**
2359 | * Should we try to ping the MySQL host?
2360 | *
2361 | * @since 4.1.0
2362 | *
2363 | * @param string $dbhname Database name.
2364 | *
2365 | * @return bool True if we should try to ping the MySQL host, false otherwise.
2366 | */
2367 | public function should_mysql_ping( $dbhname = '' ) {
2368 |
2369 | // Return false if empty handle or checks are disabled
2370 | if (
2371 | empty( $dbhname )
2372 | ||
2373 | empty( $this->check_dbh_heartbeats )
2374 | ) {
2375 | return false;
2376 | }
2377 |
2378 | // Return true if no heartbeat yet
2379 | if ( empty( $this->dbhname_heartbeats[ $dbhname ] ) ) {
2380 | return true;
2381 | }
2382 |
2383 | // Return true if last error is a down server
2384 | if (
2385 | ! empty( $this->dbhname_heartbeats[ $dbhname ]['last_errno'] )
2386 | &&
2387 | ( DB_SERVER_GONE_ERROR === $this->dbhname_heartbeats[ $dbhname ]['last_errno'] )
2388 | ) {
2389 |
2390 | // Also clear the last error
2391 | unset( $this->dbhname_heartbeats[ $dbhname ]['last_errno'] );
2392 |
2393 | return true;
2394 | }
2395 |
2396 | // Return true if last used is older than recheck timeout
2397 | if ( ( microtime( true ) - $this->dbhname_heartbeats[ $dbhname ]['last_used'] ) > $this->recheck_timeout ) {
2398 | return true;
2399 | }
2400 |
2401 | // Default to false
2402 | return false;
2403 | }
2404 |
2405 | /**
2406 | * Run lag callbacks and return current lag
2407 | *
2408 | * @since 2.1.0
2409 | *
2410 | * @return int
2411 | */
2412 | public function get_lag() {
2413 | $this->lag = $this->run_callbacks( 'get_lag' );
2414 |
2415 | return $this->check_lag();
2416 | }
2417 |
2418 | /**
2419 | * Check lag
2420 | *
2421 | * @since 2.1.0
2422 | *
2423 | * @return int
2424 | */
2425 | public function check_lag() {
2426 | if ( false === $this->lag ) {
2427 | return DB_LAG_UNKNOWN;
2428 | }
2429 |
2430 | if ( $this->lag > $this->lag_threshold ) {
2431 | return DB_LAG_BEHIND;
2432 | }
2433 |
2434 | return DB_LAG_OK;
2435 | }
2436 |
2437 | /**
2438 | * Retrieves the character set for a database table.
2439 | *
2440 | * NOTE: This must be called after LudicrousDB::db_connect, so
2441 | * that wpdb::dbh is set correctly.
2442 | *
2443 | * @param string $table Table name
2444 | *
2445 | * @return mixed The table character set, or WP_Error if we couldn't find it
2446 | */
2447 | protected function get_table_charset( $table ) {
2448 | $tablekey = strtolower( $table );
2449 |
2450 | /**
2451 | * Filter the table charset value before the DB is checked.
2452 | *
2453 | * Passing a non-null value to the filter will effectively short-circuit
2454 | * checking the DB for the charset, returning that value instead.
2455 | *
2456 | * @param string $charset The character set to use. Default null.
2457 | * @param string $table The name of the table being checked.
2458 | */
2459 | $charset = apply_filters( 'pre_get_table_charset', null, $table );
2460 | if ( null !== $charset ) {
2461 | return $charset;
2462 | }
2463 |
2464 | if ( isset( $this->table_charset[ $tablekey ] ) ) {
2465 | return $this->table_charset[ $tablekey ];
2466 | }
2467 |
2468 | $charsets = $columns = array();
2469 |
2470 | $table_parts = explode( '.', $table );
2471 | $table = '`' . implode( '`.`', $table_parts ) . '`';
2472 | $results = $this->get_results( "SHOW FULL COLUMNS FROM {$table}" );
2473 |
2474 | if ( empty( $results ) ) {
2475 | return new WP_Error( 'wpdb_get_table_charset_failure' );
2476 | }
2477 |
2478 | foreach ( $results as $column ) {
2479 | $columns[ strtolower( $column->Field ) ] = $column; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
2480 | }
2481 |
2482 | $this->col_meta[ $tablekey ] = $columns;
2483 |
2484 | foreach ( $columns as $column ) {
2485 | if ( ! empty( $column->Collation ) ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
2486 | list( $charset ) = explode( '_', $column->Collation ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
2487 |
2488 | // If the current connection can't support utf8mb4 characters, let's only send 3-byte utf8 characters.
2489 | if (
2490 | ( 'utf8mb4' === $charset )
2491 | &&
2492 | ! $this->has_cap( 'utf8mb4' )
2493 | ) {
2494 | $charset = 'utf8';
2495 | }
2496 |
2497 | $charsets[ strtolower( $charset ) ] = true;
2498 | }
2499 |
2500 | list( $type ) = explode( '(', $column->Type ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
2501 |
2502 | // A binary/blob means the whole query gets treated like this.
2503 | if ( in_array( strtoupper( $type ), self::$bin_blobs, true ) ) {
2504 | $this->table_charset[ $tablekey ] = 'binary';
2505 |
2506 | return 'binary';
2507 | }
2508 | }
2509 |
2510 | // utf8mb3 is an alias for utf8.
2511 | if ( isset( $charsets['utf8mb3'] ) ) {
2512 | $charsets['utf8'] = true;
2513 | unset( $charsets['utf8mb3'] );
2514 | }
2515 |
2516 | // Check if there is more than one charset in play.
2517 | $count = count( $charsets );
2518 | if ( 1 === $count ) {
2519 | $charset = key( $charsets );
2520 |
2521 | // No charsets, assume this table can store whatever.
2522 | } elseif ( 0 === $count ) {
2523 | $charset = false;
2524 |
2525 | // More than one charset. Remove latin1 if present and recalculate.
2526 | } else {
2527 | unset( $charsets['latin1'] );
2528 | $count = count( $charsets );
2529 |
2530 | // Only one charset (besides latin1).
2531 | if ( 1 === $count ) {
2532 | $charset = key( $charsets );
2533 |
2534 | // Two charsets, but they're utf8 and utf8mb4, use utf8.
2535 | } elseif ( 2 === $count && isset( $charsets['utf8'], $charsets['utf8mb4'] ) ) {
2536 | $charset = 'utf8';
2537 |
2538 | // Two mixed character sets. ascii.
2539 | } else {
2540 | $charset = 'ascii';
2541 | }
2542 | }
2543 |
2544 | $this->table_charset[ $tablekey ] = $charset;
2545 |
2546 | return $charset;
2547 | }
2548 |
2549 | /**
2550 | * Given a string, a character set and a table, ask the DB to check the
2551 | * string encoding. Classes that extend wpdb can override this function
2552 | * without needing to copy/paste all of wpdb::strip_invalid_text().
2553 | *
2554 | * NOTE: This must be called after LudicrousDB::db_connect, so
2555 | * that wpdb::dbh is set correctly.
2556 | *
2557 | * @since 1.0.0
2558 | *
2559 | * @param string $to_strip String to convert
2560 | * @param string $charset Character set to test against (uses MySQL character set names)
2561 | *
2562 | * @return mixed The converted string, or a WP_Error if the conversion fails
2563 | */
2564 | protected function strip_invalid_text_using_db( $to_strip, $charset ) {
2565 | $sql = "SELECT CONVERT( %s USING {$charset} )";
2566 | $query = $this->prepare( $sql, $to_strip );
2567 | $result = $this->_do_query( $query, $this->dbh );
2568 |
2569 | // Bail with error if no result
2570 | if ( empty( $result ) ) {
2571 | return new WP_Error( 'wpdb_convert_text_failure' );
2572 | }
2573 |
2574 | // Fetch row
2575 | $row = mysqli_fetch_row( $result );
2576 |
2577 | // Bail with error if no rows
2578 | if ( ! is_array( $row ) || count( $row ) < 1 ) {
2579 | return new WP_Error( 'wpdb_convert_text_failure' );
2580 | }
2581 |
2582 | return $row[0];
2583 | }
2584 |
2585 | /** TCP Cache *************************************************************/
2586 |
2587 | /**
2588 | * Start the TCP cache
2589 | *
2590 | * Only runs once. Subsequent calls will bail early.
2591 | *
2592 | * @since 5.2.0
2593 | *
2594 | * @see https://github.com/stuttter/ludicrousdb/issues/126
2595 | * @uses wp_start_object_cache() If available, to start the object cache.
2596 | * @static var bool $started True if started. False if not.
2597 | */
2598 | protected function tcp_cache_start() {
2599 | static $started = null;
2600 |
2601 | // Bail if added or caching not available yet
2602 | if ( true === $started ) {
2603 | return;
2604 | }
2605 |
2606 | // Maybe start object cache
2607 | if ( function_exists( 'wp_start_object_cache' ) ) {
2608 | wp_start_object_cache();
2609 |
2610 | // Make sure the global group is added
2611 | $this->tcp_cache_add_global_group();
2612 | }
2613 |
2614 | // Set started
2615 | $started = true;
2616 | }
2617 |
2618 | /**
2619 | * Add global TCP cache group.
2620 | *
2621 | * Only runs if object cache is available and the necessary WordPress
2622 | * function (wp_cache_add_global_groups) exists.
2623 | *
2624 | * @since 5.2.0
2625 | */
2626 | protected function tcp_cache_add_global_group() {
2627 |
2628 | // Add the cache group
2629 | if ( function_exists( 'wp_cache_add_global_groups' ) ) {
2630 | wp_cache_add_global_groups( $this->tcp_cache_group );
2631 | }
2632 | }
2633 |
2634 | /**
2635 | * Get the cache key used for TCP responses
2636 | *
2637 | * @since 3.0.0
2638 | *
2639 | * @param string $host Host
2640 | * @param string $port Port or socket.
2641 | *
2642 | * @return string
2643 | */
2644 | protected function tcp_get_cache_key( $host, $port ) {
2645 | return "{$host}:{$port}";
2646 | }
2647 |
2648 | /**
2649 | * Get the number of seconds TCP response is good for
2650 | *
2651 | * @since 3.0.0
2652 | *
2653 | * @return int
2654 | */
2655 | protected function tcp_get_cache_expiration() {
2656 | return 10;
2657 | }
2658 |
2659 | /**
2660 | * Check if TCP is using a persistent cache or not.
2661 | *
2662 | * @since 5.1.0
2663 | *
2664 | * @return bool True if yes. False if no.
2665 | */
2666 | protected function tcp_is_cache_persistent() {
2667 |
2668 | // Check if using external object cache
2669 | if ( wp_using_ext_object_cache() ) {
2670 |
2671 | // Cache is persistent
2672 | return true;
2673 | }
2674 |
2675 | // Cache is not persistent
2676 | return false;
2677 | }
2678 |
2679 | /**
2680 | * Get cached up/down value of previous TCP response.
2681 | *
2682 | * Falls back to local cache if persistent cache is not available.
2683 | *
2684 | * @since 3.0.0
2685 | *
2686 | * @param string $key Results of tcp_get_cache_key()
2687 | *
2688 | * @return mixed Results of wp_cache_get()
2689 | */
2690 | protected function tcp_cache_get( $key = '' ) {
2691 |
2692 | // Bail for invalid key
2693 | if ( empty( $key ) ) {
2694 | return false;
2695 | }
2696 |
2697 | // Get from persistent cache
2698 | if ( $this->tcp_is_cache_persistent() ) {
2699 | return wp_cache_get( $key, $this->tcp_cache_group );
2700 |
2701 | // Fallback to local cache
2702 | } elseif ( ! empty( $this->tcp_cache[ $key ] ) ) {
2703 |
2704 | // Not expired
2705 | if (
2706 | ! empty( $this->tcp_cache[ $key ]['expiration'] )
2707 | &&
2708 | ( time() < $this->tcp_cache[ $key ]['expiration'] )
2709 | ) {
2710 |
2711 | // Return value or false if empty
2712 | return ! empty( $this->tcp_cache[ $key ]['value'] )
2713 | ? $this->tcp_cache[ $key ]['value']
2714 | : false;
2715 |
2716 | // Expired, so delete and proceed
2717 | } else {
2718 | $this->tcp_cache_delete( $key );
2719 | }
2720 | }
2721 |
2722 | return false;
2723 | }
2724 |
2725 | /**
2726 | * Set cached up/down value of current TCP response.
2727 | *
2728 | * Falls back to local cache if persistent cache is not available.
2729 | *
2730 | * @since 3.0.0
2731 | *
2732 | * @param string $key Results of tcp_get_cache_key()
2733 | * @param string $value "up" or "down" based on TCP response
2734 | *
2735 | * @return bool Results of wp_cache_set() or true
2736 | */
2737 | protected function tcp_cache_set( $key = '', $value = '' ) {
2738 |
2739 | // Bail if invalid values were passed
2740 | if ( empty( $key ) || empty( $value ) ) {
2741 | return false;
2742 | }
2743 |
2744 | // Get expiration
2745 | $expires = $this->tcp_get_cache_expiration();
2746 |
2747 | // Add to persistent cache
2748 | if ( $this->tcp_is_cache_persistent() ) {
2749 | return wp_cache_set( $key, $value, $this->tcp_cache_group, $expires );
2750 |
2751 | // Fallback to local cache
2752 | } else {
2753 | $this->tcp_cache[ $key ] = array(
2754 | 'value' => $value,
2755 | 'expiration' => time() + $expires,
2756 | );
2757 | }
2758 |
2759 | return true;
2760 | }
2761 |
2762 | /**
2763 | * Delete cached up/down value of current TCP response.
2764 | *
2765 | * Falls back to local cache if persistent cache is not available.
2766 | *
2767 | * @since 5.1.0
2768 | *
2769 | * @param string $key Results of tcp_get_cache_key()
2770 | *
2771 | * @return bool Results of wp_cache_delete() or true
2772 | */
2773 | protected function tcp_cache_delete( $key = '' ) {
2774 |
2775 | // Bail if invalid key
2776 | if ( empty( $key ) ) {
2777 | return false;
2778 | }
2779 |
2780 | // Delete from persistent cache
2781 | if ( $this->tcp_is_cache_persistent() ) {
2782 | return wp_cache_delete( $key, $this->tcp_cache_group );
2783 |
2784 | // Fallback to local cache
2785 | } else {
2786 | unset( $this->tcp_cache[ $key ] );
2787 | }
2788 |
2789 | return true;
2790 | }
2791 |
2792 | /**
2793 | * Find a dbh name value for a given $dbh object.
2794 | *
2795 | * @since 5.0.0
2796 | *
2797 | * @param object $dbh The dbh object for which to find the dbh name
2798 | *
2799 | * @return string The dbh name
2800 | */
2801 | private function lookup_dbhs_name( $dbh = false ) {
2802 |
2803 | // Loop through database hosts and look for this one
2804 | foreach ( $this->dbhs as $dbhname => $other_dbh ) {
2805 |
2806 | // Match found so return the key
2807 | if ( $dbh === $other_dbh ) {
2808 | return $dbhname;
2809 | }
2810 | }
2811 |
2812 | // No match
2813 | return false;
2814 | }
2815 |
2816 | /** Deprecated ************************************************************/
2817 |
2818 | /**
2819 | * Set a flag to prevent reading from replicas which might be lagging after
2820 | * a write.
2821 | *
2822 | * @since 1.0.0
2823 | * @deprecated 5.2.0 Use send_reads_to_primaries() instead.
2824 | */
2825 | public function send_reads_to_masters() {
2826 | _deprecated_function( __FUNCTION__, '5.2.0', 'wpdb::send_reads_to_primaries()' );
2827 | $this->send_reads_to_primaries();
2828 | }
2829 | }
2830 |
--------------------------------------------------------------------------------
/ludicrousdb/includes/functions.php:
--------------------------------------------------------------------------------
1 | add_table( $dataset, $table );
54 | }
55 |
56 | /**
57 | * Convenience function to add a database server.
58 | *
59 | * This is backwards-compatible with a procedural config style.
60 | *
61 | * lhost, part, and dc were removed from LudicrousDB because the read and write
62 | * parameters provide enough power to achieve the desired effects via config.
63 | *
64 | * @param string $dataset Dataset: The name of the dataset. Just use "global" if you don't need horizontal partitioning.
65 | * @param int $part Partition: The vertical partition number (1, 2, 3, etc.). Use "0" if you don't need vertical partitioning.
66 | * @param string $dc Datacenter: Where the database server is located. Airport codes are convenient. Use whatever.
67 | * @param int $read Read group: Tries all servers in lowest number group before trying higher number group. Typical: 1 for replicas, 2 for primary. This will cause reads to go to replicas.
68 | * @param bool $write Write flag: Is this server writable? Works the same as $read. Typical: 1 for primary, 0 for replicas.
69 | * @param string $host Internet address: host:port of server on Internet.
70 | * @param string $lhost Local address: host:port of server for use when in same datacenter. Leave empty if no local address exists.
71 | * @param string $name Database name.
72 | * @param string $user Database user.
73 | * @param string $password Database password.
74 | * @param int $timeout Timeout.
75 | */
76 | function ldb_add_db_server( $dataset, $part, $dc, $read, $write, $host, $lhost, $name, $user, $password, $timeout = 0.2 ) {
77 |
78 | // dc is not used in LudicrousDB. This produces the desired effect of
79 | // trying to connect to local servers before remote servers. Also
80 | // increases time allowed for TCP responsiveness check.
81 | if ( ! empty( $dc ) && ( defined( 'DATACENTER' ) && ( DATACENTER !== $dc ) ) ) {
82 | $read += 10000;
83 | $write += 10000;
84 | $timeout = 0.7;
85 | }
86 |
87 | // Maybe add part to dataset
88 | if ( ! empty( $part ) ) {
89 | $dataset = "{$dataset}_{$part}";
90 | }
91 |
92 | // Put variables into array
93 | $database = compact( 'dataset', 'read', 'write', 'host', 'name', 'user', 'password', 'timeout' );
94 |
95 | // Add database array to main object
96 | $GLOBALS['wpdb']->add_database( $database );
97 |
98 | // Current datacenter
99 | if ( defined( 'DATACENTER' ) && ( DATACENTER === $dc ) ) {
100 |
101 | // lhost is not used in LudicrousDB. This configures LudicrousDB with an
102 | // additional server to represent the local hostname so it tries to
103 | // connect over the private interface before the public one.
104 | if ( ! empty( $lhost ) ) {
105 |
106 | $database['host'] = $lhost;
107 |
108 | if ( ! empty( $read ) ) {
109 | $database['read'] = $read - 0.5;
110 | }
111 |
112 | if ( ! empty( $write ) ) {
113 | $database['write'] = $write - 0.5;
114 | }
115 |
116 | $GLOBALS['wpdb']->add_database( $database );
117 | }
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/readme.txt:
--------------------------------------------------------------------------------
1 | === LudicrousDB ===
2 | Contributors: johnjamesjacoby, spacedmonkey
3 | Author: Triple J Software, Inc.
4 | Author URI: https://jjj.software
5 | Plugin URI: https://github.com/stuttter/ludicrousdb/
6 | License URI: https://www.gnu.org/licenses/gpl-2.0.html
7 | License: GPLv2 or later
8 | Tags: database, mysql, performance, scaling, wpdb
9 | Requires PHP: 7.0
10 | Requires at least: 5.0
11 | Tested up to: 6.5
12 | Stable tag: 5.2.0
13 |
14 | LudicrousDB is an advanced database interface for WordPress that supports replication, fail-over, load balancing, and partitioning
15 |
16 | == Description ==
17 |
18 | LudicrousDB is an advanced database interface for WordPress that replaces much of its built-in database functionality.
19 |
20 | The main differences are:
21 |
22 | * Connects to an arbitrary number of database servers
23 | * Inspects each query to determine the appropriate database
24 | * Designed specifically for large, multi-site, high-availability environments
25 |
26 | It supports:
27 |
28 | * Read and write servers (replication)
29 | * Configurable priority for reading and writing
30 | * Local and remote data-centers
31 | * Private and public networks
32 | * Different tables on different databases/hosts
33 | * Smart post-write primary reads
34 | * Fail-over for downed host
35 | * Advanced statistics for profiling
36 |
37 | It is based on the code currently used in production on WordPress.com & WordPress.org, with many database servers spanning multiple data-centers.
38 |
39 | It is a fork of Automattic's HyperDB.
40 |
41 | = Previous Contributors =
42 |
43 | HyperDB's original contributors: matt, andy, ryan, mdawaffe, vnsavage, automattic
44 |
45 | == Installation ==
46 |
47 | See https://github.com/stuttter/ludicrousdb/wiki/0.-Installation
48 |
49 | == Frequently Asked Questions ==
50 |
51 | = What can I do with LudicrousDB that I can't do with WPDB? =
52 |
53 | WordPress.org, the most complex LudicrousDB installation, manages millions of tables spanning thousands of databases. Dynamic configuration logic allows LudicrousDB to compute the location of tables by querying a central database. Custom scripts constantly balance database server resources by migrating tables and updating their locations in the central database.
54 |
55 | Stretch your imagination. You could create a dynamic configuration using persistent caching to gather intelligence about the state of the network. The only constant is the name of the configuration file. The rest, as they say, is PHP.
56 |
57 | = How does LudicrousDB support replication? =
58 |
59 | LudicrousDB does not provide replication services. That is done by configuring MySQL servers for replication. LudicrousDB can then be configured to use these servers appropriately, e.g. by connecting to primary servers to perform write queries.
60 |
61 | = How does LudicrousDB support load balancing? =
62 |
63 | LudicrousDB randomly selects database connections from priority groups that you configure. The most advantageous connections are tried first. Thus you can optimize your configuration for network topology, hardware capability, or any other scheme you invent.
64 |
65 | = How does LudicrousDB support failover? =
66 |
67 | Failover describes how LudicrousDB deals with connection failures. When LudicrousDB fails to connect to one database, it tries to connect to another database that holds the same data. If replication hasn't been set up, LudicrousDB tries reconnecting a few times before giving up.
68 |
69 | = How does LudicrousDB support partitioning? =
70 |
71 | LudicrousDB allows tables to be placed in arbitrary databases. It can use callbacks you write to compute the appropriate database for a given query. Thus you can partition your site's data according to your own scheme and configure LudicrousDB accordingly.
72 |
73 | = Is there any advantage to using LudicrousDB with just one database server? =
74 |
75 | None that has been measured. LudicrousDB does at least try again before giving up connecting, so it might help in cases where the web server is momentarily unable to connect to the database server.
76 |
77 | One way LudicrousDB differs from WPDB is that LudicrousDB does not attempt to connect to a database until a query is made. Thus a site with sufficiently aggressive persistent caching could remain read-only accessible despite the database becoming unreachable.
78 |
79 | = What if all database servers for a dataset go down? =
80 |
81 | Since LudicrousDB attempts a connection only when a query is made, your WordPress installation will not kill the site with a database error, but will let the code decide what to do next on an unsuccessful query. If you want to do something different, like setting a custom error page or kill the site, you need to define the 'db_connection_error' callback in your db-config.php.
82 |
83 | == Changelog ==
84 |
85 | = 5.2.0 =
86 | * PHP 8.3 compatibility
87 | * Update default collation to utf8mb4_unicode_520_ci
88 | * Fix a few PHP warnings under some configurations
89 |
90 | = 5.0.0 =
91 | * PHP 7.3 compatibility
92 | * Update default collation to unicode_520_ci
93 | * Global cache group, and add caching to TCP connection statuses
94 | * Fix a few PHP warnings under some configurations
95 | * Fix bug causing timeout for ping under some configurations
96 |
97 | = 4.1.0 =
98 | * Fix WordPress 4.8.3 SQLi vulnerability
99 |
100 | = 4.0.0 =
101 | * Support for custom mu / plugin paths
102 | * Improved WordPress 4.6 capability
103 | * New filter - pre_query
104 | * New action - queried
105 | * Allow db-config.php file to stored in wp-content directory.
106 |
107 | = 3.0.0 =
108 | * Improved support for mu-plugins installation location
109 | * Remove APC support for TCP responses (use core caching functions instead)
110 | * Fix typos in documentation
111 |
112 | = 2.1.0 =
113 | * Fixed wrong return value for `CREATE`, `ALTER`, `TRUNCATE` and `DROP` queries
114 | * Merge improvements from WordPress core WPDB
115 | * Undefined variable fixes
116 |
117 | = 2.0.0 =
118 | * Fork from HyperDB
119 | * Include utf8mb4 support (for WordPress 4.2 compatibility)
120 | * Remove support for WPDB_PATH as require_wp_db() prevents it
121 |
122 | = 1.1.0 =
123 | * Extended callbacks functionality
124 | * Added connection error callback
125 | * Added replication lag detection support
126 |
127 | = 1.0.0 =
128 | * Removed support for WPMU and BackPress.
129 | * New class with inheritance: hyperdb extends wpdb.
130 | * New instantiation scheme: $wpdb = new hyperdb(); then include config. No more $db_* globals.
131 | * New configuration file name (db-config.php) and logic for locating it. (ABSPATH, dirname(ABSPATH), or pre-defined)
132 | * Added fallback to wpdb in case database config not found.
133 | * Renamed servers to databases in config in an attempt to reduce ambiguity.
134 | * Added config interface functions to hyperdb: add_database, add_table, add_callback.
135 | * Refactored db_server array to simplify finding a server.
136 | * Removed native support for datacenters and partitions. The same effects are accomplished by read/write parameters and dataset names.
137 | * Removed preg pattern support from $db_tables. Use callbacks instead.
138 | * Removed delay between connection retries and avoid immediate retry of same server when others are available to try.
139 | * Added connection stats.
140 | * Added save_query_callback for custom debug logging.
141 | * Refined SRTM granularity. Now only send reads to primaries when the written table is involved.
142 | * Improved connection reuse logic and added mysql_ping to recover from "server has gone away".
143 | * Added min_tries to configure the minimum number of connection attempts before bailing.
144 | * Added WPDB_PATH constant. Define this if you'd rather not use ABSPATH . WPINC . '/wp-db.php'.
145 |
--------------------------------------------------------------------------------