├── .gitignore ├── .travis.yml ├── LICENSE.txt ├── README.md ├── composer.json ├── phpunit.xml.dist ├── src ├── Exception │ └── PeerNotReachableException.php ├── README ├── Replication.php ├── ReplicationTask.php └── Replicator.php └── tests ├── Functional └── ReplicatorTest.php ├── ReplicationTest.php ├── ReplicatorFunctionalTestBase.php └── TestUtil.php /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | .idea 3 | nbproject 4 | vendor 5 | composer.lock 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | services: 4 | - couchdb 5 | 6 | php: 7 | - 5.6 8 | - 7.0 9 | - 7.1 10 | - 7.2 11 | 12 | notifications: 13 | slack: 14 | rooms: 15 | - det:YiOiwfzUBtt9aTnWvocK8uDI 16 | on_success: change 17 | on_failure: always 18 | on_start: never 19 | 20 | before_install: 21 | - composer selfupdate 22 | 23 | install: 24 | - composer install 25 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/relaxedws/couchdb-replicator.svg?branch=master)](https://travis-ci.org/relaxedws/couchdb-replicator) 2 | 3 | # couchdb-replicator 4 | CouchDB Replicator implemented with PHP 5 | 6 | ## Example usage 7 | ```php 8 | require __DIR__ . '/vendor/autoload.php'; 9 | 10 | use Doctrine\CouchDB\CouchDBClient; 11 | use Relaxed\Replicator\ReplicationTask; 12 | use Relaxed\Replicator\Replicator; 13 | 14 | $source = CouchDBClient::create(['dbname' => 'source']); 15 | $target = CouchDBClient::create(['dbname' => 'target']); 16 | 17 | $task = new ReplicationTask(); 18 | $replicator = new Replicator($source, $target, $task); 19 | 20 | $response = $replicator->startReplication(); 21 | ``` 22 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "relaxedws/replicator", 3 | "description": "couchdb-replicator", 4 | "license": "GPL-2.0+", 5 | "authors": [ 6 | { 7 | "name": "abhishek kumar", 8 | "email": "abhi170893@gmail.com" 9 | } 10 | ], 11 | "require-dev": { 12 | "phpunit/phpunit": "4.8.*" 13 | }, 14 | "require": { 15 | "relaxedws/couchdb": "~2" 16 | }, 17 | "autoload": { 18 | "psr-4": { 19 | "Relaxed\\Replicator\\": "src" 20 | } 21 | }, 22 | "autoload-dev": { 23 | "psr-4": { 24 | "Relaxed\\Replicator\\Test\\": "tests" 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 12 | 14 | 15 | 16 | 17 | 18 | tests 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/Exception/PeerNotReachableException.php: -------------------------------------------------------------------------------- 1 | source = $source; 53 | $this->target = $target; 54 | $this->task = $task; 55 | } 56 | 57 | /** 58 | * Starts the replication process. $printStatus can be used to print the 59 | * status of the continuous replication to the STDOUT. The $getFinalReport 60 | * can be used to enable/disable returning of an array containing the 61 | * replication report in case of continuous replication. This is useful 62 | * when there are large number of documents. So when the replication is 63 | * continuous,to see the status set $printStatus to true and $getFinalReport 64 | * to false. 65 | * 66 | * @param bool $printStatus 67 | * @param bool $getFinalReport 68 | * @return array 69 | * @throws \Doctrine\CouchDB\HTTP\HTTPException 70 | * @throws \Exception 71 | */ 72 | 73 | public function start($printStatus = true, $getFinalReport = true) 74 | { 75 | $this->startTime = new \DateTime(); 76 | // DB info (via GET /{db}) for source and target. 77 | $this->verifyPeers(); 78 | $this->task->setRepId($this->generateReplicationId()); 79 | // Replication log (via GET /{db}/_local/{docid}) for source and target. 80 | list($sourceLog, $targetLog) = $this->getReplicationLog(); 81 | 82 | $this->task->setSinceSeq($this->compareReplicationLogs($sourceLog, $targetLog)); 83 | 84 | // Main replication processing 85 | $response = $this->locateChangedDocumentsAndReplicate($printStatus, $getFinalReport); 86 | 87 | $this->endTime = new \DateTime(); 88 | $replicationLog = $this->putReplicationLog($response); 89 | 90 | $this->ensureFullCommit(); 91 | 92 | unset($replicationLog['_id']); 93 | unset($replicationLog['_rev']); 94 | unset($replicationLog['_revisions']); 95 | 96 | return $replicationLog; 97 | } 98 | 99 | 100 | /** 101 | * @return array 102 | * @throws HTTPException 103 | * @throws \Exception 104 | */ 105 | public function verifyPeers() 106 | { 107 | $sourceInfo = null; 108 | try { 109 | $sourceInfo = $this->source->getDatabaseInfo($this->source->getDatabase()); 110 | } catch (HTTPException $e) { 111 | throw new PeerNotReachableException('Source not reachable.'); 112 | } 113 | 114 | $targetInfo = null; 115 | try { 116 | $targetInfo = $this->target->getDatabaseInfo($this->target->getDatabase()); 117 | } catch (HTTPException $e) { 118 | if ($e->getCode() == 404 && $this->task->getCreateTarget()) { 119 | $this->target->createDatabase($this->target->getDatabase()); 120 | $targetInfo = $this->target->getDatabaseInfo($this->target->getDatabase()); 121 | } elseif ($e->getCode() == 404) { 122 | throw new PeerNotReachableException('Target does not exist.'); 123 | } else { 124 | throw new PeerNotReachableException($e->getMessage()); 125 | } 126 | } 127 | return array($sourceInfo, $targetInfo); 128 | } 129 | 130 | /** 131 | * @return string 132 | * @throws HTTPException 133 | */ 134 | public function generateReplicationId() 135 | { 136 | $filterCode = ''; 137 | $filter = $this->task->getFilter(); 138 | $parameters = $this->task->getParameters(); 139 | if ($filter != null && empty($parameters)) { 140 | if ($filter[0] !== '_') { 141 | list($designDoc, $functionName) = explode('/', $filter); 142 | $designDocName = '_design/' . $designDoc; 143 | $response = $this->source->findDocument($designDocName); 144 | if ($response->status != 200) { 145 | throw HTTPException::fromResponse('/' . $this->source->getDatabase() . '/' . $designDocName, $response); 146 | } 147 | $filterCode = $response->body['filters'][$functionName]; 148 | } 149 | } 150 | return \md5( 151 | $this->source->getUuid() . 152 | $this->source->getDatabase() . 153 | $this->target->getDatabase() . 154 | \var_export($this->task->getDocIds(), true) . 155 | ($this->task->getCreateTarget() ? '1' : '0') . 156 | ($this->task->getContinuous() ? '1' : '0') . 157 | $filter . 158 | $filterCode . 159 | $this->task->getStyle() . 160 | \var_export($this->task->getHeartbeat(), true) 161 | ); 162 | } 163 | 164 | /** 165 | * @return array 166 | * @throws HTTPException 167 | * @throws \Exception 168 | */ 169 | public function getReplicationLog() 170 | { 171 | $replicationDocId = '_local' . '/' . $this->task->getRepId(); 172 | $sourceResponse = $this->source->findDocument($replicationDocId); 173 | $targetResponse = $this->target->findDocument($replicationDocId); 174 | 175 | if ($sourceResponse->status == 200) { 176 | $this->sourceLog = $sourceResponse->body; 177 | } elseif ($sourceResponse->status != 404) { 178 | throw HTTPException::fromResponse('/' . $this->source->getDatabase() . '/' .$replicationDocId, $sourceResponse); 179 | } 180 | if ($targetResponse->status == 200) { 181 | $this->targetLog = $targetResponse->body; 182 | } elseif ($targetResponse->status != 404) { 183 | throw HTTPException::fromResponse('/' . $this->target->getDatabase() . '/' .$replicationDocId, $targetResponse); 184 | } 185 | return array($this->sourceLog, $this->targetLog); 186 | } 187 | 188 | /** 189 | * @param array $response 190 | * @return array 191 | * @throws \Doctrine\CouchDB\HTTP\HTTPException 192 | */ 193 | public function putReplicationLog(array $response) { 194 | $sessionId = \md5((\microtime(true) * 1000000)); 195 | $sourceInfo = $this->source->getDatabaseInfo($this->source->getDatabase()); 196 | $data = [ 197 | '_id' => '_local/' . $this->task->getRepId(), 198 | 'history' => [ 199 | 'recorded_seq' => $sourceInfo['update_seq'], 200 | 'session_id' => $sessionId, 201 | 'start_time' => $this->startTime->format('D, d M Y H:i:s e'), 202 | 'end_time' => $this->endTime->format('D, d M Y H:i:s e'), 203 | ], 204 | 'replication_id_version' => 3, 205 | 'session_id' => $sessionId, 206 | 'source_last_seq' => $sourceInfo['update_seq'] 207 | ]; 208 | 209 | if (isset($response['doc_write_failures'])) { 210 | $data['history']['doc_write_failures'] = $response['doc_write_failures']; 211 | } 212 | if (isset($response['docs_read'])) { 213 | $data['history']['docs_read'] = $response['docs_read']; 214 | } 215 | if (isset($response['missing_checked'])) { 216 | $data['history']['missing_checked'] = $response['missing_checked']; 217 | } 218 | if (isset($response['missing_found'])) { 219 | $data['history']['missing_found'] = $response['missing_found']; 220 | } 221 | if (isset($response['start_last_seq'])) { 222 | $data['history']['start_last_seq'] = $response['start_last_seq']; 223 | } 224 | if (isset($response['end_last_seq'])) { 225 | $data['history']['end_last_seq'] = $response['end_last_seq']; 226 | } 227 | if (isset($response['docs_written'])) { 228 | $data['history']['docs_written'] = $response['docs_written']; 229 | } 230 | 231 | // Creating dedicated source and target data arrays. 232 | $sourceData = $data; 233 | $targetData = $data; 234 | // Adding _rev to data array if it was in original replication log 235 | if (isset($this->sourceLog['_rev'])) { 236 | $sourceData['_rev'] = $this->sourceLog['_rev']; 237 | } 238 | if (isset($this->targetLog['_rev'])) { 239 | $targetData['_rev'] = $this->targetLog['_rev']; 240 | } 241 | 242 | // Having to work around CouchDBClient not supporting _local. 243 | $sourceResponse = $this->source->getHttpClient()->request('PUT', '/' . $this->source->getDatabase() . '/' . $data['_id'], json_encode($sourceData)); 244 | $targetResponse = $this->target->getHttpClient()->request('PUT', '/' . $this->target->getDatabase() . '/' . $data['_id'], json_encode($targetData)); 245 | 246 | if ($sourceResponse->status != 201) { 247 | throw HTTPException::fromResponse('/' . $this->source->getDatabase() . '/' . $data['_id'], $sourceResponse); 248 | } 249 | 250 | if ($targetResponse->status != 201) { 251 | throw HTTPException::fromResponse('/' . $this->target->getDatabase() . '/' . $data['_id'], $targetResponse); 252 | } 253 | 254 | return $data; 255 | } 256 | 257 | /** 258 | * @param $sourceLog 259 | * @param $targetLog 260 | * @return int|mixed 261 | */ 262 | public function compareReplicationLogs(&$sourceLog, &$targetLog) 263 | { 264 | $sinceSeq = 0; 265 | if ($sourceLog == null || $targetLog == null) { 266 | $sinceSeq = $this->task->getSinceSeq(); 267 | } elseif ($sourceLog['session_id'] === $targetLog['session_id']) { 268 | $sinceSeq = $sourceLog['source_last_seq']; 269 | } else { 270 | foreach ($sourceLog['history'] as &$sDoc) { 271 | $matchFound = 0; 272 | foreach ($targetLog['history'] as &$tDoc) { 273 | if ($sDoc['session_id'] === $tDoc['session_id']) { 274 | $sinceSeq = $sDoc['recorded_seq']; 275 | $matchFound = 1; 276 | break; 277 | } 278 | } 279 | unset($tDoc); 280 | if ($matchFound === 1) { 281 | break; 282 | } 283 | } 284 | unset($sDoc); 285 | } 286 | return $sinceSeq; 287 | } 288 | 289 | /** 290 | * @param $changes 291 | * @return array 292 | */ 293 | public function getMapping($changes) 294 | { 295 | $rows = array(); 296 | if ($this->task->getContinuous() == false) { 297 | $rows = is_array($changes['results']) ? $changes['results'] : []; 298 | } else { 299 | $arr = \explode("\n",$changes); 300 | foreach ($arr as $line) { 301 | if (\strlen($line) > 0) { 302 | $rows[] = json_decode($line, true); 303 | } 304 | } 305 | 306 | } 307 | // To be sent to target/_revs_diff. 308 | $mapping = array(); 309 | foreach ($rows as $row) { 310 | $mapping[$row['id']] = array(); 311 | foreach ($row['changes'] as $revision) { 312 | $mapping[$row['id']][] = $revision['rev']; 313 | } 314 | } 315 | return $mapping; 316 | } 317 | 318 | /** 319 | * When $printStatus is true, the replication details are written to the 320 | * STDOUT. When $getFinalReport is true, detailed replication report is 321 | * returned and if false, only the success and failure counts are returned. 322 | * Both $printStatus and $getFinalReport are used only when the 323 | * replication is continuous and are ignored in case of normal replication. 324 | * 325 | * @param bool $printStatus 326 | * @param bool $getFinalReport 327 | * @return array 328 | * @throws HTTPException 329 | */ 330 | public function locateChangedDocumentsAndReplicate($printStatus, $getFinalReport) 331 | { 332 | $finalResponse = array( 333 | 'multipartResponse' => array(), 334 | 'bulkResponse' => array(), 335 | 'errorResponse' => array(), 336 | 'missing_found' => 0, 337 | ); 338 | // Filtered changes stream is not supported. So Don't use the doc_ids 339 | // to specify the specific document ids. 340 | if ($this->task->getContinuous()) { 341 | $options = array( 342 | 'feed' => 'continuous', 343 | 'style' => $this->task->getStyle(), 344 | 'heartbeat' => $this->task->getHeartbeat(), 345 | 'since' => $this->task->getSinceSeq(), 346 | 'filter' => $this->task->getFilter(), 347 | 'parameters' => $this->task->getParameters(), 348 | //'doc_ids' => $this->task->getDocIds(), // Not supported. 349 | //'limit' => 10000 //taking large value for now, needs optimisation 350 | ); 351 | if ($this->task->getHeartbeat() != null) { 352 | $options['heartbeat'] = $this->task->getHeartbeat(); 353 | } else { 354 | $options['timeout'] = ($this->task->getTimeout() != null ? $this->task->getTimeout() : 10000); 355 | } 356 | $changesStream = $this->source->getChangesAsStream($options); 357 | $failureCount = 0; 358 | 359 | while (!feof($changesStream)) { 360 | $changes = fgets($changesStream); 361 | if ($changes == false || trim($changes) == '' || strpos($changes,'last_seq') !==false) { 362 | sleep(2); 363 | continue; 364 | } 365 | $mapping = $this->getMapping($changes); 366 | $docId = array_keys($mapping)[0]; 367 | try { 368 | // getRevisionDifference throws bad request when JSON is 369 | // empty. So check before sending. 370 | $revDiff = (count($mapping) > 0 ? $this->target->getRevisionDifference($mapping) : array()); 371 | $response = $this->replicateChanges($revDiff); 372 | $finalResponse['doc_write_failures'] = 0; 373 | $finalResponse['docs_written'] = 0; 374 | $finalResponse['docs_read'] = $response['docs_read']; 375 | $finalResponse['missing_checked'] = $response['missing_checked']; 376 | if (isset($changes['results'][0]['seq'])) { 377 | $finalResponse['start_last_seq'] = $changes['results'][0]['seq']; 378 | } 379 | if (isset($changes['last_seq'])) { 380 | $finalResponse['end_last_seq'] = $changes['last_seq']; 381 | } 382 | if ($getFinalReport == true) { 383 | foreach ($response['multipartResponse'] as $docID => $res) { 384 | // Add the response of posting each revision of the 385 | // doc that had attachments. 386 | foreach ($res as $singleRevisionResponse) { 387 | // An Exception. 388 | if (is_a($singleRevisionResponse, 'Exception')) { 389 | $finalResponse['errorResponse'][$docID][] = $singleRevisionResponse; 390 | } else { 391 | $finalResponse['missing_found']++; 392 | $finalResponse['multipartResponse'][$docID][] = $singleRevisionResponse; 393 | } 394 | } 395 | } 396 | foreach ($response['bulkResponse'] as $doc_post_result) { 397 | if (!empty($doc_post_result['ok'])) { 398 | $finalResponse['docs_written']++; 399 | } 400 | elseif (!empty($doc_post_result['error'])) { 401 | $finalResponse['doc_write_failures']++; 402 | } 403 | } 404 | $finalResponse['bulkResponse'] = $response['bulkResponse']; 405 | } 406 | 407 | if ($printStatus == true) { 408 | echo 'Document with id = ' . $docId . ' successfully replicated.'. "\n"; 409 | } 410 | 411 | } catch (\Exception $e) { 412 | if ($getFinalReport == true) { 413 | $finalResponse['errorResponse'][$docID][] = $e; 414 | } 415 | 416 | if ($printStatus == true) { 417 | echo 'Replication of document with id = ' . $docId . ' failed with code: ' . $e->getCode() . ".\n"; 418 | } 419 | 420 | $failureCount++; 421 | } 422 | } 423 | $finalResponse['failureCount'] = $failureCount; 424 | // The final response in case of continuous replication. 425 | // In case where $getFinalReport is true, response has five keys: 426 | // (i)multipartResponse, (ii) bulkResponse, (iii)errorResponse, 427 | // (iv) successCount, (v) failureCount. The errorResponse has the 428 | // responses from the failed replication attempt of docs having 429 | // attachments. To check failures related to bulk posting, the 430 | // returned status codes can be used. 431 | // When $getFinalReport is false, the returned response has only the 432 | // successCount and failureCount. 433 | return $finalResponse; 434 | 435 | } else { 436 | $revDiff = []; 437 | $since = $this->task->getSinceSeq(); 438 | $style = $this->task->getStyle(); 439 | while (1) { 440 | $changes = $this->source->getChanges( 441 | array( 442 | 'feed' => 'normal', 443 | 'style' => $style, 444 | 'since' => $since, 445 | 'filter' => $this->task->getFilter(), 446 | 'parameters' => $this->task->getParameters(), 447 | 'doc_ids' => $this->task->getDocIds(), 448 | 'limit' => $this->task->getLimit(), 449 | ) 450 | ); 451 | if (empty($changes['results']) || empty($changes['last_seq'])) { 452 | break; 453 | } 454 | $mapping = $this->getMapping($changes); 455 | $diff = count($mapping) > 0 ? $this->target->getRevisionDifference($mapping) : []; 456 | if ($style == 'all_docs') { 457 | $revDiff = array_merge_recursive($revDiff, $diff); 458 | } 459 | else { 460 | $revDiff = array_merge($revDiff, $diff); 461 | } 462 | if (!in_array($changes['last_seq'], array_column($changes['results'], 'seq'))) { 463 | break; 464 | } 465 | $since = $changes['last_seq']; 466 | } 467 | 468 | $response = $this->replicateChanges($revDiff); 469 | $finalResponse['doc_write_failures'] = 0; 470 | $finalResponse['docs_written'] = 0; 471 | $finalResponse['docs_read'] = $response['docs_read']; 472 | $finalResponse['missing_checked'] = $response['missing_checked']; 473 | if (isset($changes['results'][0]['seq'])) { 474 | $finalResponse['start_last_seq'] = $changes['results'][0]['seq']; 475 | } 476 | if (isset($changes['last_seq'])) { 477 | $finalResponse['end_last_seq'] = $changes['last_seq']; 478 | } 479 | foreach ($response['multipartResponse'] as $docID => $res) { 480 | // Add the response of posting each revision of the 481 | // doc that had attachments. 482 | foreach ($res as $singleRevisionResponse) { 483 | // An Exception. 484 | if (is_a($singleRevisionResponse, 'Exception')) { 485 | $finalResponse['errorResponse'][$docID][] = $singleRevisionResponse; 486 | } else { 487 | $finalResponse['missing_found']++; 488 | $finalResponse['multipartResponse'][$docID][] = $singleRevisionResponse; 489 | } 490 | } 491 | } 492 | foreach ($response['bulkResponse'] as $doc_post_result) { 493 | if (!empty($doc_post_result['ok'])) { 494 | $finalResponse['docs_written']++; 495 | } 496 | elseif (!empty($doc_post_result['error'])) { 497 | $finalResponse['doc_write_failures']++; 498 | } 499 | } 500 | $finalResponse['bulkResponse'] = $response['bulkResponse']; 501 | // In case of normal replication the $finalResponse has three 502 | // keys: (i) multipartResponse, (ii) bulkResponse, (iii)= 503 | // errorResponse. 504 | return $finalResponse; 505 | } 506 | } 507 | 508 | /** 509 | * @param array $revDiff 510 | * @return array 511 | * @throws HTTPException|\Exception 512 | */ 513 | public function replicateChanges(array &$revDiff) 514 | { 515 | $allResponse = array( 516 | 'multipartResponse' => array(), 517 | 'bulkResponse' => array(), 518 | 'docs_read' => 0, 519 | 'missing_checked' => 0 520 | ); 521 | 522 | $bulkUpdater = $this->target->createBulkUpdater(); 523 | $bulkUpdater->setNewEdits(false); 524 | $bulkDocsLimit = $this->task->getBulkDocsLimit(); 525 | while (!empty($revDiff)) { 526 | $processRevs = array_splice($revDiff, 0, $bulkDocsLimit); 527 | foreach ($processRevs as $docId => $revMisses) { 528 | $allResponse['docs_read']++; 529 | $allResponse['missing_checked'] += count($revMisses['missing']); 530 | try { 531 | $path = '/' . $this->source->getDatabase() . '/'. $docId; 532 | $params = ['revs' => true, 'latest' => true, 'open_revs' => json_encode($revMisses['missing'])]; 533 | $query = http_build_query($params); 534 | $path .= '?' . $query; 535 | $response = $this->source->transferChangedDocuments($docId, $revMisses['missing'], $this->target); 536 | if ($response instanceof ErrorResponse) { 537 | throw HTTPException::fromResponse($path, $response); 538 | } 539 | list($docStack, $multipartResponse) = $response; 540 | } catch (\Exception $e) { 541 | throw new \Exception($e->getMessage(), $e->getCode()); 542 | } 543 | $bulkUpdater->updateDocuments($docStack); 544 | // $multipartResponse is an empty array in case there was no 545 | // transferred revision that had attachment in the current doc. 546 | $allResponse['multipartResponse'][$docId] = $multipartResponse; 547 | } 548 | $allResponse['bulkResponse'] += $bulkUpdater->executeByLimit($bulkDocsLimit); 549 | $bulkUpdater->emptyDocuments(); 550 | } 551 | return $allResponse; 552 | } 553 | 554 | /** 555 | * @throws \Doctrine\CouchDB\HTTP\HTTPException 556 | */ 557 | public function ensureFullCommit() 558 | { 559 | $this->target->ensureFullCommit(); 560 | } 561 | 562 | } 563 | -------------------------------------------------------------------------------- /src/ReplicationTask.php: -------------------------------------------------------------------------------- 1 | repId = $repId; 105 | $this->continuous = $continuous; 106 | $this->filter = $filter; 107 | $this->parameters = $parameters; 108 | $this->createTarget = $createTarget; 109 | $this->docIds = $docIds; 110 | $this->heartbeat = $heartbeat; 111 | $this->timeout = $timeout; 112 | $this->cancel = $cancel; 113 | $this->style = $style; 114 | $this->sinceSeq = $sinceSeq; 115 | $this->limit = $limit; 116 | $this->bulkDocsLimit = $bulkDocsLimit; 117 | 118 | if ($docIds != null) { 119 | \sort($this->docIds); 120 | if ($filter == null) { 121 | $this->filter = '_doc_ids'; 122 | } 123 | elseif ($filter !== '_doc_ids') { 124 | throw new \InvalidArgumentException('If docIds is specified, 125 | the filter should be set as _doc_ids'); 126 | } 127 | } 128 | } 129 | 130 | /** 131 | * @return mixed 132 | */ 133 | public function getRepId() 134 | { 135 | return $this->repId; 136 | } 137 | 138 | /** 139 | * @param mixed $repId 140 | */ 141 | public function setRepId($repId) 142 | { 143 | $this->repId = $repId; 144 | } 145 | 146 | /** 147 | * @return bool 148 | */ 149 | public function getContinuous() 150 | { 151 | return $this->continuous; 152 | } 153 | 154 | /** 155 | * @param bool $continuous 156 | */ 157 | public function setContinuous($continuous) 158 | { 159 | $this->continuous = $continuous; 160 | } 161 | 162 | /** 163 | * @return string 164 | */ 165 | public function getFilter() 166 | { 167 | return $this->filter; 168 | } 169 | 170 | /** 171 | * @param string $filter 172 | */ 173 | public function setFilter($filter) 174 | { 175 | $this->filter = $filter; 176 | } 177 | 178 | /** 179 | * @return array 180 | */ 181 | public function getParameters() 182 | { 183 | return $this->parameters; 184 | } 185 | 186 | /** 187 | * @param array|NULL $parameters 188 | * An associative array of name-value parameters. 189 | */ 190 | public function setParameters(array $parameters = NULL) { 191 | if ($parameters == NULL) { 192 | $parameters = []; 193 | } 194 | $this->parameters = $parameters; 195 | } 196 | 197 | /** 198 | * @param string $name 199 | * The parameter name to set. 200 | * @param string $value 201 | * The value for the parameter. 202 | */ 203 | public function setParameter($name, $value) 204 | { 205 | if (!is_array($this->parameters)) { 206 | $this->setParameters([]); 207 | } 208 | $this->parameters[$name] = $value; 209 | } 210 | 211 | /** 212 | * @param int 213 | */ 214 | public function setLimit($limit) 215 | { 216 | $this->limit = $limit; 217 | } 218 | 219 | /** 220 | * @param int 221 | */ 222 | public function setBulkDocsLimit($bulkDocsLimit) 223 | { 224 | $this->bulkDocsLimit = $bulkDocsLimit; 225 | } 226 | 227 | /** 228 | * @return boolean 229 | */ 230 | public function getCreateTarget() 231 | { 232 | return $this->createTarget; 233 | } 234 | 235 | /** 236 | * @param boolean $createTarget 237 | */ 238 | public function setCreateTarget($createTarget) 239 | { 240 | $this->createTarget = $createTarget; 241 | } 242 | 243 | /** 244 | * @return array 245 | */ 246 | public function getDocIds() 247 | { 248 | return $this->docIds; 249 | } 250 | 251 | /** 252 | * @return int 253 | */ 254 | public function getLimit() 255 | { 256 | return $this->limit; 257 | } 258 | 259 | /** 260 | * @return int 261 | */ 262 | public function getBulkDocsLimit() 263 | { 264 | return $this->bulkDocsLimit; 265 | } 266 | 267 | /** 268 | * @param array $docIds 269 | */ 270 | public function setDocIds($docIds) 271 | { 272 | if ($docIds != null) { 273 | \sort($docIds); 274 | if ($this->filter == null) { 275 | $this->filter = '_doc_ids'; 276 | } 277 | elseif ($this->filter !== '_doc_ids') { 278 | throw new \InvalidArgumentException('If docIds is specified, 279 | the filter should be set as _doc_ids'); 280 | } 281 | } 282 | $this->docIds = $docIds; 283 | } 284 | 285 | /** 286 | * @return int 287 | */ 288 | public function getHeartbeat() 289 | { 290 | return $this->heartbeat; 291 | } 292 | 293 | /** 294 | * @param int $heartbeat 295 | */ 296 | public function setHeartbeat($heartbeat) 297 | { 298 | $this->heartbeat = $heartbeat; 299 | } 300 | 301 | /** 302 | * @return int 303 | */ 304 | public function getTimeout() 305 | { 306 | return $this->timeout; 307 | } 308 | 309 | /** 310 | * @param int $timeout 311 | */ 312 | public function setTimeout($timeout) 313 | { 314 | $this->timeout = $timeout; 315 | } 316 | 317 | /** 318 | * @return mixed 319 | */ 320 | public function getCancel() 321 | { 322 | return $this->cancel; 323 | } 324 | 325 | /** 326 | * @param mixed $cancel 327 | */ 328 | public function setCancel($cancel) 329 | { 330 | $this->cancel = $cancel; 331 | } 332 | 333 | /** 334 | * @return mixed 335 | */ 336 | public function getStyle() 337 | { 338 | return $this->style; 339 | } 340 | 341 | /** 342 | * @param mixed $style 343 | */ 344 | public function setStyle($style) 345 | { 346 | $this->style = $style; 347 | } 348 | 349 | /** 350 | * @return mixed 351 | */ 352 | public function getSinceSeq() 353 | { 354 | return $this->sinceSeq; 355 | } 356 | 357 | /** 358 | * @param mixed $sinceSeq 359 | */ 360 | public function setSinceSeq($sinceSeq) 361 | { 362 | $this->sinceSeq = $sinceSeq; 363 | } 364 | 365 | } 366 | -------------------------------------------------------------------------------- /src/Replicator.php: -------------------------------------------------------------------------------- 1 | source = $source; 39 | $this->target = $target; 40 | $this->task = $task; 41 | } 42 | 43 | /** 44 | * Start the replicator. $printStatus can be used to print the status of 45 | * the continuous replication to the STDOUT. The $getFinalReport can be 46 | * used to enable/disable returning of an array containing the 47 | * replication report in case of continuous replication. 48 | * 49 | * @param bool $printStatus 50 | * @param bool $getFinalReport 51 | * @return array 52 | */ 53 | public function startReplication($printStatus = true, $getFinalReport = false) 54 | { 55 | if ($this->source == null) { 56 | throw new \UnexpectedValueException('Source is Null.'); 57 | } 58 | if ($this->target == null) { 59 | throw new \UnexpectedValueException('Target is Null.'); 60 | } 61 | if ($this->task == null) { 62 | throw new \UnexpectedValueException('Task is Null.'); 63 | } 64 | 65 | $replication = new Replication($this->source, $this-> target, $this->task); 66 | 67 | // Start and return the details of the replication. 68 | return $replication->start($printStatus, $getFinalReport); 69 | } 70 | 71 | /** 72 | * @throws Exception 73 | */ 74 | public function cancelReplication() 75 | { 76 | throw new \Exception('Not defined'); 77 | } 78 | 79 | /** 80 | * @return CouchDBClient 81 | */ 82 | public function getSource() 83 | { 84 | return $this->source; 85 | } 86 | 87 | /** 88 | * @param CouchDBClient $source 89 | */ 90 | public function setSource(CouchDBClient $source) 91 | { 92 | $this->source = $source; 93 | } 94 | 95 | /** 96 | * @return CouchDBClient 97 | */ 98 | public function getTarget() 99 | { 100 | return $this->target; 101 | } 102 | 103 | /** 104 | * @param CouchDBClient $target 105 | */ 106 | public function setTarget(CouchDBClient $target) 107 | { 108 | $this->target = $target; 109 | } 110 | 111 | /** 112 | * @return ReplicationTask 113 | */ 114 | public function getTask() 115 | { 116 | return $this->task; 117 | } 118 | 119 | /** 120 | * @param ReplicationTask $task 121 | */ 122 | public function setTask(ReplicationTask $task) 123 | { 124 | $this->task = $task; 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /tests/Functional/ReplicatorTest.php: -------------------------------------------------------------------------------- 1 | sourceClient = $this->getSourceCouchDBClient(); 18 | $this->targetClient = $this->getTargetCouchDBClient(); 19 | $this->replicationTask = $this->getReplicationTask(); 20 | // Disable default Heartbeat and use timeout. This is to make the 21 | // connection terminate quickly when there are no changes happening on 22 | // the source in case of the continuous replication. 23 | $this->replicationTask->setHeartbeat(null); 24 | // Timeout to be used in the case of continuous replication. It's in 25 | // milliseconds. 26 | $this->replicationTask->setTimeout(100); 27 | 28 | // Create the source and the target databases. 29 | $this->sourceClient->createDatabase($this->getSourceTestDatabase()); 30 | $this->sourceClient->createDatabase($this->getTargetTestDatabase()); 31 | 32 | $this->replicator = new Replicator( 33 | $this->sourceClient, 34 | $this->targetClient, 35 | $this->replicationTask 36 | ); 37 | } 38 | 39 | 40 | /** 41 | * @expectedException \UnexpectedValueException 42 | * @expectedExceptionMessage Source is Null. 43 | */ 44 | public function testStartReplicationThrowsExceptionOnNullSource() 45 | { 46 | $this->replicator = new Replicator( 47 | null, 48 | $this->targetClient, 49 | $this->replicationTask 50 | ); 51 | $this->replicator->startReplication(); 52 | } 53 | 54 | /** 55 | * @expectedException \UnexpectedValueException 56 | * @expectedExceptionMessage Target is Null. 57 | */ 58 | public function testStartReplicationThrowsExceptionOnNullTarget() 59 | { 60 | $this->replicator = new Replicator( 61 | $this->sourceClient, 62 | null, 63 | $this->replicationTask 64 | ); 65 | $this->replicator->startReplication(); 66 | } 67 | 68 | /** 69 | * @expectedException \UnexpectedValueException 70 | * @expectedExceptionMessage Task is Null. 71 | */ 72 | public function testStartReplicationThrowsExceptionOnNullTask() 73 | { 74 | $this->replicator = new Replicator( 75 | $this->sourceClient, 76 | $this->targetClient, 77 | null 78 | ); 79 | $this->replicator->startReplication(); 80 | } 81 | 82 | /** 83 | * @expectedException \Exception 84 | * @expectedExceptionMessage Source not reachable. 85 | */ 86 | public function testStartReplicationThrowsExceptionWhenSourceDoesNotExist() 87 | { 88 | // Delete the source database. 89 | $this->sourceClient->deleteDatabase($this->getSourceTestDatabase()); 90 | try { 91 | $this->replicator->startReplication(); 92 | } catch (\Exception $e) { 93 | // Restore state before throwing the raised exception. 94 | $this->sourceClient->createDatabase($this->getSourceTestDatabase()); 95 | throw $e; 96 | } 97 | 98 | } 99 | 100 | /** 101 | * @expectedException \Exception 102 | * @expectedExceptionMessage Target does not exist. 103 | */ 104 | public function testStartReplicationThrowsExceptionWhenTargetDoesNotExist() 105 | { 106 | // Delete the target database. 107 | $this->targetClient->deleteDatabase($this->getTargetTestDatabase()); 108 | try { 109 | $this->replicator->startReplication(); 110 | } catch (\Exception $e) { 111 | // Restore state before throwing the raised exception. 112 | $this->targetClient->createDatabase($this->getTargetTestDatabase()); 113 | throw $e; 114 | } 115 | 116 | } 117 | 118 | public function testTargetCreation() 119 | { 120 | // Delete the target database. 121 | $this->targetClient->deleteDatabase($this->getTargetTestDatabase()); 122 | // Enable target creation. 123 | $this->replicationTask->setCreateTarget(true); 124 | $this->replicator->setTask($this->replicationTask); 125 | // Start the replication. 126 | $this->replicator->startReplication(); 127 | 128 | $data = $this->targetClient->getDatabaseInfo(); 129 | 130 | // The target should have been created. 131 | $this->assertInternalType('array', $data); 132 | $this->assertArrayHasKey('db_name', $data); 133 | $this->assertEquals($this->getTargetTestDatabase(), $data['db_name']); 134 | 135 | } 136 | 137 | public function isContinuousReplicationProvider() 138 | { 139 | return array( 140 | // Normal replication. 141 | array(false), 142 | // Continuous replication. 143 | array(true) 144 | ); 145 | } 146 | 147 | /** 148 | * @dataProvider isContinuousReplicationProvider 149 | */ 150 | public function testReplicationWithoutAttachments($isContinuous) 151 | { // Set the replication type. 152 | $this->replicationTask->setContinuous($isContinuous); 153 | 154 | // Add three docs to the source db. 155 | for ($i = 0; $i < 3; $i++) { 156 | list($id, $rev) = $this->sourceClient->putDocument( 157 | array("foo" => "bar" . var_export($i, true)), 158 | 'id' . var_export($i, true) 159 | ); 160 | } 161 | $this->replicator->startReplication(true, true); 162 | // Fetch the documents. 163 | $response = $this->targetClient->findDocuments( 164 | array('id0', 'id1', 'id2') 165 | ); 166 | $this->assertInternalType('array', $response->body); 167 | $body = $response->body['rows']; 168 | $this->assertEquals(3, count($body)); 169 | $this->assertArrayHasKey('id', $body[0]); 170 | $this->assertEquals('id0', $body[0]['id']); 171 | $this->assertEquals('id1', $body[1]['id']); 172 | $this->assertEquals('id2', $body[2]['id']); 173 | } 174 | 175 | public function testFilteredReplication() 176 | { 177 | // Add four docs to the source db. Replicate only id1 and id3 for 178 | // checking filtered Replication. 179 | for ($i = 1; $i <= 4; $i++) { 180 | $this->sourceClient->putDocument(array("foo" => "bar$i"), "id$i"); 181 | } 182 | // Specify docs to be replicated. id2 and id4 should not be replicated. 183 | $this->replicationTask->setDocIds(array('id1', 'id3')); 184 | $this->replicator->setTask($this->replicationTask); 185 | $this->replicator->startReplication(); 186 | $response = $this->targetClient->findDocuments( 187 | array('id1', 'id2', 'id3', 'id4') 188 | ); 189 | $this->assertInternalType('array', $response->body); 190 | $body = $response->body['rows']; 191 | $this->assertEquals(4, count($body)); 192 | $this->assertArrayHasKey('id', $body[0]); 193 | $this->assertEquals('id1', $body[0]['id']); 194 | $this->assertArrayHasKey('error', $body[1]); 195 | $this->assertEquals('not_found', $body[1]['error']); 196 | $this->assertEquals('id3', $body[2]['id']); 197 | $this->assertArrayHasKey('error', $body[3]); 198 | $this->assertEquals('not_found', $body[3]['error']); 199 | 200 | } 201 | 202 | public function testChangesLimitReplication() 203 | { 204 | // Replicate 9 docs. 205 | $docs_count = 9; 206 | for ($i = 1; $i <= $docs_count; $i++) { 207 | $this->sourceClient->putDocument(array("foo" => "bar$i"), "id$i"); 208 | } 209 | // Set changes limit to 2. 210 | $this->replicationTask->setLimit(2); 211 | $this->replicator->setTask($this->replicationTask); 212 | $this->replicator->startReplication(); 213 | $response = $this->targetClient->allDocs(); 214 | $this->assertInternalType('array', $response->body); 215 | $body = $response->body['rows']; 216 | $this->assertEquals($docs_count, count($body)); 217 | } 218 | 219 | public function testBulkDocsLimitReplication() 220 | { 221 | // Replicate 9 docs. 222 | $docs_count = 9; 223 | for ($i = 1; $i <= $docs_count; $i++) { 224 | $this->sourceClient->putDocument(array("foo" => "bar$i"), "id$i"); 225 | } 226 | // Set BulkDocs limit to 2. 227 | $this->replicationTask->setBulkDocsLimit(2); 228 | $this->replicator->setTask($this->replicationTask); 229 | $this->replicator->startReplication(); 230 | $response = $this->targetClient->allDocs(); 231 | $this->assertInternalType('array', $response->body); 232 | $body = $response->body['rows']; 233 | $this->assertEquals($docs_count, count($body)); 234 | } 235 | 236 | /** 237 | * @dataProvider isContinuousReplicationProvider 238 | */ 239 | public function testReplicationWithAttachments($isContinuous) 240 | { 241 | // Set the replication type. 242 | $this->replicationTask->setContinuous($isContinuous); 243 | // Test replication with attachments. 244 | // Doc id. 245 | $id = 'multiple_attachments'; 246 | // Document with attachments. 247 | $docWithAttachment1 = array ( 248 | '_id' => $id, 249 | '_rev' => '1-abc', 250 | '_attachments' => 251 | array ( 252 | 'foo.txt' => 253 | array ( 254 | 'content_type' => 'text/plain', 255 | 'data' => 'VGhpcyBpcyBhIGJhc2U2NCBlbmNvZGVkIHRleHQ=', 256 | ), 257 | 'bar.txt' => 258 | array ( 259 | 'content_type' => 'text/plain', 260 | 'data' => 'VGhpcyBpcyBhIGJhc2U2NCBlbmNvZGVkIHRleHQ=', 261 | ), 262 | ), 263 | ); 264 | // Doc without any attachment. The id of both the docs is same. 265 | // So we will get two leaf revisions. 266 | $doc = array('_id' => $id, '_rev' => '1-bcd', 'foo' => 'bar'); 267 | // Another document with attachments. 268 | $docWithAttachment2 = array ( 269 | '_id' => $id . '2', 270 | '_rev' => '1-lala', 271 | '_attachments' => 272 | array ( 273 | 'abhi.txt' => 274 | array ( 275 | 'content_type' => 'text/plain', 276 | 'data' => 'VGhpcyBpcyBhIGJhc2U2NCBlbmNvZGVkIHRleHQ=', 277 | ), 278 | 'dixon.txt' => 279 | array ( 280 | 'content_type' => 'text/plain', 281 | 'data' => 'VGhpcyBpcyBhIGJhc2U2NCBlbmNvZGVkIHRleHQ=', 282 | ), 283 | ), 284 | ); 285 | 286 | // Add the documents to the test db using Bulk API. 287 | $updater = $this->sourceClient->createBulkUpdater(); 288 | $updater->updateDocument($docWithAttachment1); 289 | $updater->updateDocument($docWithAttachment2); 290 | $updater->updateDocument($doc); 291 | // Set newedits to false to use the supplied _rev instead of assigning 292 | // new ones. 293 | $updater->setNewEdits(false); 294 | $updater->execute(); 295 | 296 | // Start the replication. Print the status to STDOUT and also get the 297 | // details in an array. 298 | $repDetails = $this->replicator->startReplication(true, true); 299 | 300 | // Test the replication. 301 | // Fetch all the revisions of the first doc. 302 | $response = $this->targetClient->findRevisions($id, true); 303 | $this->assertObjectHasAttribute('body', $response); 304 | $this->assertInternalType('array', $response->body); 305 | $this->assertEquals(2, count($response->body)); 306 | // Doc with _rev = 1-bcd 307 | $this->assertEquals(array('ok' => $doc), $response->body[0]); 308 | // Doc with _rev = 1-abc 309 | $this->assertEquals(3, count($response->body[1]['ok'])); 310 | $this->assertEquals($id, $response->body[1]['ok']['_id']); 311 | $this->assertEquals('1-abc', $response->body[1]['ok']['_rev']); 312 | $this->assertEquals( 313 | 2, 314 | count($response->body[1]['ok']['_attachments']) 315 | ); 316 | $this->assertArrayHasKey( 317 | 'foo.txt', 318 | $response->body[1]['ok']['_attachments'] 319 | ); 320 | $this->assertArrayHasKey( 321 | 'bar.txt', 322 | $response->body[1]['ok']['_attachments'] 323 | ); 324 | // Fetch the second document. This has only one revision. 325 | $response = $this->targetClient->findDocument($id . '2'); 326 | $this->assertObjectHasAttribute('body', $response); 327 | $this->assertInternalType('array', $response->body); 328 | $this->assertEquals(3, count($response->body)); 329 | $this->assertEquals($id . '2', $response->body['_id']); 330 | $this->assertEquals('1-lala', $response->body['_rev']); 331 | $this->assertEquals( 332 | 2, 333 | count($response->body['_attachments']) 334 | ); 335 | $this->assertArrayHasKey( 336 | 'abhi.txt', 337 | $response->body['_attachments'] 338 | ); 339 | $this->assertArrayHasKey( 340 | 'dixon.txt', 341 | $response->body['_attachments'] 342 | ); 343 | } 344 | 345 | public function tearDown() 346 | { 347 | $this->sourceClient->deleteDatabase($this->getSourceTestDatabase()); 348 | $this->sourceClient->deleteDatabase($this->getTargetTestDatabase()); 349 | } 350 | 351 | } -------------------------------------------------------------------------------- /tests/ReplicationTest.php: -------------------------------------------------------------------------------- 1 | source = $this->getMockBuilder('Doctrine\CouchDB\CouchDBClient') 20 | ->disableOriginalConstructor() 21 | ->getMock(); 22 | $this->target = $this->getMockBuilder('Doctrine\CouchDB\CouchDBClient') 23 | ->disableOriginalConstructor() 24 | ->getMock(); 25 | 26 | $this->response=new Response(200, array(), array('reason' => 'someReasonAsIAmTesting'), true); 27 | } 28 | 29 | /** 30 | * @expectedException \Exception 31 | * @expectedExceptionMessage Source not reachable. 32 | */ 33 | Public function testVerifyPeersRaisesExceptionWhenSourceIsNotReachable() 34 | { 35 | $this->source->expects($this->once()) 36 | ->method('getDatabase') 37 | ->willReturn('test_source_database'); 38 | $this->source->expects($this->once()) 39 | ->method('getDatabaseInfo') 40 | ->willThrowException(new HTTPException()); 41 | $task = new ReplicationTask(); 42 | 43 | $replication = new Replication($this->source, $this->target, $task); 44 | $replication->verifyPeers(); 45 | 46 | } 47 | 48 | /** 49 | * @expectedException \Exception 50 | * @expectedExceptionMessage Target does not exist. 51 | */ 52 | Public function testVerifyPeersRaisesExceptionWhenTargetDoesNotExistAndIsNotToBeCreated() 53 | { 54 | $this->source->expects($this->once()) 55 | ->method('getDatabase') 56 | ->willReturn('test_source_database'); 57 | $this->source->expects($this->once()) 58 | ->method('getDatabaseInfo') 59 | ->willReturn(array( 60 | 'db_name' => 'test_source_database', 61 | 'instance_start_time' => '123', 62 | 'update_seq' => '456' 63 | )); 64 | $this->target->expects($this->once()) 65 | ->method('getDatabase') 66 | ->willReturn('test_target_database'); 67 | $this->response->status = 404; 68 | $this->target->expects($this->once()) 69 | ->method('getDatabaseInfo') 70 | ->willThrowException(HTTPException::fromResponse(null, $this->response)); 71 | $task = new ReplicationTask(); 72 | 73 | $replication = new Replication($this->source, $this->target, $task); 74 | $replication->verifyPeers(); 75 | 76 | } 77 | 78 | Public function testVerifyPeersWhenWhenTargetDoesNotExistAndIsToBeCreated() 79 | { 80 | $this->source->expects($this->once()) 81 | ->method('getDatabase') 82 | ->willReturn('test_source_database'); 83 | $this->source->expects($this->once()) 84 | ->method('getDatabaseInfo') 85 | ->willReturn(array( 86 | 'db_name' => 'test_source_database', 87 | 'instance_start_time' => '123', 88 | 'update_seq' => '456' 89 | )); 90 | $this->target->expects($this->exactly(3)) 91 | ->method('getDatabase') 92 | ->willReturn('test_target_database'); 93 | $this->response->status = 404; 94 | $this->target->expects($this->exactly(2)) 95 | ->method('getDatabaseInfo') 96 | ->will($this->onConsecutiveCalls( 97 | $this->throwException(HTTPException::fromResponse('path', $this->response)), 98 | array( 99 | 'db_name' => 'test_target_database', 100 | 'instance_start_time' => '123', 101 | 'update_seq' => '456' 102 | ))); 103 | $this->target->expects($this->once()) 104 | ->method('createDatabase') 105 | ->willReturn(''); 106 | $task = new ReplicationTask(); 107 | $task->setCreateTarget(true); 108 | 109 | $replication = new Replication($this->source, $this->target, $task); 110 | $response = $replication->verifyPeers(); 111 | $this->assertEquals(\count($response), 2, 112 | 'Source and target info not correctly returned.'); 113 | } 114 | 115 | Public function testVerifyPeersWhenSourceAndTargetAlreadyExist() 116 | { 117 | $this->source->expects($this->once()) 118 | ->method('getDatabase') 119 | ->willReturn('test_source_database'); 120 | $this->source->expects($this->once()) 121 | ->method('getDatabaseInfo') 122 | ->willReturn(array( 123 | 'db_name' => 'test_source_database', 124 | 'instance_start_time' => '123', 125 | 'update_seq' => '456' 126 | )); 127 | $this->target->expects($this->once()) 128 | ->method('getDatabase') 129 | ->willReturn('test_target_database'); 130 | $this->target->expects($this->once()) 131 | ->method('getDatabaseInfo') 132 | ->willReturn(array( 133 | 'db_name' => 'test_target_database', 134 | 'instance_start_time' => '123', 135 | 'update_seq' => '456' 136 | )); 137 | $task = new ReplicationTask(); 138 | 139 | $replication = new Replication($this->source, $this->target, $task); 140 | list($sourceInfo, $targetInfo) = $replication->verifyPeers(); 141 | 142 | $this->assertArrayHasKey("update_seq", $sourceInfo, 'Source info not correctly returned.'); 143 | $this->assertArrayHasKey("instance_start_time", $sourceInfo, 'Source info not correctly returned.'); 144 | $this->assertArrayHasKey("update_seq", $targetInfo, 'Target info not correctly returned.'); 145 | $this->assertArrayHasKey("instance_start_time", $targetInfo, 'Target info not correctly returned.'); 146 | } 147 | 148 | public function testGenerateReplicationId() 149 | { 150 | $this->source->expects($this->once()) 151 | ->method('getDatabase') 152 | ->willReturn('test_source_database'); 153 | $this->target->expects($this->once()) 154 | ->method('getDatabase') 155 | ->willReturn('test_target_database'); 156 | $task = new ReplicationTask(); 157 | $expectedId = md5( 158 | 'test_source_database' . 159 | 'test_target_database' . 160 | \var_export(null, true) . 161 | '0' . 162 | '0' . 163 | null . 164 | null . 165 | 'all_docs' . 166 | '10000' 167 | ); 168 | $replication = new Replication($this->source, $this->target, $task); 169 | $this->assertEquals($expectedId, $replication->generateReplicationId(), 'Incorrect Replication Id Generation.'); 170 | } 171 | 172 | public function testGenerateReplicationIdWithFilter() 173 | { 174 | $filterCode = "function(doc, req) { if (doc._deleted) { return true; } if(!doc.clientId) { return false; } }"; 175 | $this->source->expects($this->once()) 176 | ->method('getDatabase') 177 | ->willReturn('test_source_database'); 178 | $this->response->status = 200; 179 | $this->response->body = array('filters' => array('testFilterFunction' => $filterCode)); 180 | $this->source->expects($this->once()) 181 | ->method('findDocument') 182 | ->willReturn($this->response); 183 | $this->target->expects($this->once()) 184 | ->method('getDatabase') 185 | ->willReturn('test_target_database'); 186 | $task = new ReplicationTask( 187 | null,false,'test/testFilterFunction', [], true, 188 | null, 10000, 10000, false, 'all_docs', 0 189 | ); 190 | $expectedId = md5( 191 | 'test_source_database' . 192 | 'test_target_database' . 193 | \var_export(null, true) . 194 | '1' . 195 | '0' . 196 | 'test/testFilterFunction' . 197 | $filterCode . 198 | 'all_docs' . 199 | '10000' 200 | ); 201 | $replication = new Replication($this->source, $this->target, $task); 202 | $this->assertEquals($expectedId, $replication->generateReplicationId(), 'Incorrect Replication Id Generation.'); 203 | } 204 | 205 | public function testGenerateReplicationIdWithDocIds() 206 | { 207 | $this->source->expects($this->once()) 208 | ->method('getDatabase') 209 | ->willReturn('test_source_database'); 210 | $this->target->expects($this->once()) 211 | ->method('getDatabase') 212 | ->willReturn('test_target_database'); 213 | $task = new ReplicationTask( 214 | null, false, '_doc_ids', [], true, 215 | array(1, 3, 2, 'jfajs57s868'), 216 | 10000, 10000, false, 'all_docs', 0 217 | ); 218 | $expectedId = md5( 219 | 'test_source_database' . 220 | 'test_target_database' . 221 | \var_export(array('jfajs57s868', 1, 2, 3), true) . 222 | '1' . 223 | '0' . 224 | '_doc_ids' . 225 | '' . 226 | 'all_docs' . 227 | '10000' 228 | ); 229 | $replication = new Replication($this->source, $this->target, $task); 230 | $this->assertEquals($expectedId, $replication->generateReplicationId(), 'Incorrect Replication Id Generation.'); 231 | } 232 | 233 | public function testGetReplicationLog() 234 | { 235 | $sourceResponse = $this->response; 236 | $sourceResponse->body = array("log" => "source_replication_log"); 237 | $sourceResponse->status = 200; 238 | $this->source->expects($this->once()) 239 | ->method('findDocument') 240 | ->willReturn($sourceResponse); 241 | 242 | $targetResponse = clone $this->response; 243 | $targetResponse->status = 404; 244 | 245 | $this->target->expects($this->once()) 246 | ->method('findDocument') 247 | ->willReturn($targetResponse); 248 | 249 | $task = new ReplicationTask(); 250 | $replication = new Replication($this->source, $this->target, $task); 251 | list($sourceLog, $targetLog) = $replication->getReplicationLog(); 252 | $this->assertEquals($sourceLog, array("log" => "source_replication_log")); 253 | $this->assertEquals($targetLog, null); 254 | 255 | } 256 | 257 | /** 258 | * @expectedException \Doctrine\CouchDB\HTTP\HTTPException 259 | */ 260 | public function testGetReplicationLogRaisesExceptionWhenPeerNotReachable() 261 | { 262 | $this->response->status = 500; 263 | $this->source->expects($this->once()) 264 | ->method('findDocument') 265 | ->willThrowException(HTTPException::fromResponse(null, $this->response)); 266 | 267 | $task = new ReplicationTask(); 268 | $replication = new Replication($this->source, $this->target, $task); 269 | list($sourceLog, $targetLog) = $replication->getReplicationLog(); 270 | $this->assertEquals($targetLog, array("log" => "source_replication_log")); 271 | $this->assertEquals($sourceLog, null); 272 | 273 | } 274 | 275 | /** 276 | * @dataProvider replicationLogsProvider 277 | */ 278 | public function testCompareReplicationLogs($sourceLog, $targetLog, $expectedSequence) 279 | { 280 | $task = new ReplicationTask(); 281 | $replication = new Replication($this->source, $this->target, $task); 282 | $this->assertEquals($expectedSequence, $replication->compareReplicationLogs($sourceLog,$targetLog)); 283 | 284 | } 285 | 286 | public function replicationLogsProvider() 287 | { 288 | return array( 289 | array( 290 | array ( 291 | '_id' => '_local/b3e44b920ee2951cb2e123b63044427a', 292 | '_rev' => '0-8', 293 | 'history' => 294 | array ( 295 | 0 => 296 | array ( 297 | 'doc_write_failures' => 0, 298 | 'docs_read' => 2, 299 | 'docs_written' => 2, 300 | 'end_last_seq' => 5, 301 | 'end_time' => 'Thu, 10 Oct 2013 05:56:38 GMT', 302 | 'missing_checked' => 2, 303 | 'missing_found' => 2, 304 | 'recorded_seq' => 5, 305 | 'session_id' => 'd5a34cbbdafa70e0db5cb57d02a6b955', 306 | 'start_last_seq' => 3, 307 | 'start_time' => 'Thu, 10 Oct 2013 05:56:38 GMT', 308 | ), 309 | 1 => 310 | array ( 311 | 'doc_write_failures' => 0, 312 | 'docs_read' => 1, 313 | 'docs_written' => 1, 314 | 'end_last_seq' => 3, 315 | 'end_time' => 'Thu, 10 Oct 2013 05:56:12 GMT', 316 | 'missing_checked' => 1, 317 | 'missing_found' => 1, 318 | 'recorded_seq' => 3, 319 | 'session_id' => '11a79cdae1719c362e9857cd1ddff09d', 320 | 'start_last_seq' => 2, 321 | 'start_time' => 'Thu, 10 Oct 2013 05:56:12 GMT', 322 | ), 323 | 2 => 324 | array ( 325 | 'doc_write_failures' => 0, 326 | 'docs_read' => 2, 327 | 'docs_written' => 2, 328 | 'end_last_seq' => 2, 329 | 'end_time' => 'Thu, 10 Oct 2013 05:56:04 GMT', 330 | 'missing_checked' => 2, 331 | 'missing_found' => 2, 332 | 'recorded_seq' => 2, 333 | 'session_id' => '77cdf93cde05f15fcb710f320c37c155', 334 | 'start_last_seq' => 0, 335 | 'start_time' => 'Thu, 10 Oct 2013 05:56:04 GMT', 336 | ), 337 | ), 338 | 'replication_id_version' => 3, 339 | 'session_id' => 'd5a34cbbdafa70e0db5cb57d02a6b955', 340 | 'source_last_seq' => 5, 341 | ), 342 | array ( 343 | '_id' => '_local/b3e44b920ee2951cb2e123b63044427a', 344 | '_rev' => '0-8', 345 | 'history' => 346 | array ( 347 | 0 => 348 | array ( 349 | 'doc_write_failures' => 0, 350 | 'docs_read' => 2, 351 | 'docs_written' => 2, 352 | 'end_last_seq' => 5, 353 | 'end_time' => 'Thu, 10 Oct 2013 05:56:38 GMT', 354 | 'missing_checked' => 2, 355 | 'missing_found' => 2, 356 | 'recorded_seq' => 5, 357 | 'session_id' => 'd5a34cbbdafa70e0db5cb57d02a6b955', 358 | 'start_last_seq' => 3, 359 | 'start_time' => 'Thu, 10 Oct 2013 05:56:38 GMT', 360 | ), 361 | 1 => 362 | array ( 363 | 'doc_write_failures' => 0, 364 | 'docs_read' => 1, 365 | 'docs_written' => 1, 366 | 'end_last_seq' => 3, 367 | 'end_time' => 'Thu, 10 Oct 2013 05:56:12 GMT', 368 | 'missing_checked' => 1, 369 | 'missing_found' => 1, 370 | 'recorded_seq' => 3, 371 | 'session_id' => '11a79cdae1719c362e9857cd1ddff09d', 372 | 'start_last_seq' => 2, 373 | 'start_time' => 'Thu, 10 Oct 2013 05:56:12 GMT', 374 | ), 375 | 2 => 376 | array ( 377 | 'doc_write_failures' => 0, 378 | 'docs_read' => 2, 379 | 'docs_written' => 2, 380 | 'end_last_seq' => 2, 381 | 'end_time' => 'Thu, 10 Oct 2013 05:56:04 GMT', 382 | 'missing_checked' => 2, 383 | 'missing_found' => 2, 384 | 'recorded_seq' => 2, 385 | 'session_id' => '77cdf93cde05f15fcb710f320c37c155', 386 | 'start_last_seq' => 0, 387 | 'start_time' => 'Thu, 10 Oct 2013 05:56:04 GMT', 388 | ), 389 | ), 390 | 'replication_id_version' => 3, 391 | 'session_id' => 'd5a34cbbdafa70e0db5cb57d02a6b955', 392 | 'source_last_seq' => 5, 393 | ), 394 | 5 395 | ), 396 | array( 397 | array ( 398 | '_id' => '_local/b3e44b920ee2951cb2e123b63044427a', 399 | '_rev' => '0-8', 400 | 'history' => 401 | array ( 402 | 0 => 403 | array ( 404 | 'doc_write_failures' => 0, 405 | 'docs_read' => 2, 406 | 'docs_written' => 2, 407 | 'end_last_seq' => 5, 408 | 'end_time' => 'Thu, 10 Oct 2013 05:56:38 GMT', 409 | 'missing_checked' => 2, 410 | 'missing_found' => 2, 411 | 'recorded_seq' => 5, 412 | 'session_id' => 'd5a34cbbdafa70e0db5cb57d02a6b955', 413 | 'start_last_seq' => 3, 414 | 'start_time' => 'Thu, 10 Oct 2013 05:56:38 GMT', 415 | ), 416 | 1 => 417 | array ( 418 | 'doc_write_failures' => 0, 419 | 'docs_read' => 1, 420 | 'docs_written' => 1, 421 | 'end_last_seq' => 3, 422 | 'end_time' => 'Thu, 10 Oct 2013 05:56:12 GMT', 423 | 'missing_checked' => 1, 424 | 'missing_found' => 1, 425 | 'recorded_seq' => 3, 426 | 'session_id' => '11a79cdae1719c362e9857cd1ddff09d', 427 | 'start_last_seq' => 2, 428 | 'start_time' => 'Thu, 10 Oct 2013 05:56:12 GMT', 429 | ), 430 | 2 => 431 | array ( 432 | 'doc_write_failures' => 0, 433 | 'docs_read' => 2, 434 | 'docs_written' => 2, 435 | 'end_last_seq' => 2, 436 | 'end_time' => 'Thu, 10 Oct 2013 05:56:04 GMT', 437 | 'missing_checked' => 2, 438 | 'missing_found' => 2, 439 | 'recorded_seq' => 2, 440 | 'session_id' => '77cdf93cde05f15fcb710f320c37c155', 441 | 'start_last_seq' => 0, 442 | 'start_time' => 'Thu, 10 Oct 2013 05:56:04 GMT', 443 | ), 444 | ), 445 | 'replication_id_version' => 3, 446 | 'session_id' => 'd5a34cbbdafa70e0db5cb57d02a6b955', 447 | 'source_last_seq' => 5, 448 | ), 449 | array ( 450 | '_id' => '_local/b3e44b920ee2951cb2e123b63044427a', 451 | '_rev' => '0-8', 452 | 'history' => 453 | array ( 454 | 0 => 455 | array ( 456 | 'doc_write_failures' => 0, 457 | 'docs_read' => 2, 458 | 'docs_written' => 2, 459 | 'end_last_seq' => 5, 460 | 'end_time' => 'Thu, 10 Oct 2013 05:56:38 GMT', 461 | 'missing_checked' => 2, 462 | 'missing_found' => 2, 463 | 'recorded_seq' => 5, 464 | 'session_id' => 'cbbdafa70e0db5cb57d02a6b955', 465 | 'start_last_seq' => 3, 466 | 'start_time' => 'Thu, 10 Oct 2013 05:56:38 GMT', 467 | ), 468 | 1 => 469 | array ( 470 | 'doc_write_failures' => 0, 471 | 'docs_read' => 1, 472 | 'docs_written' => 1, 473 | 'end_last_seq' => 3, 474 | 'end_time' => 'Thu, 10 Oct 2013 05:56:12 GMT', 475 | 'missing_checked' => 1, 476 | 'missing_found' => 1, 477 | 'recorded_seq' => 3, 478 | 'session_id' => '11a79cdae1719c362e9857cd1ddff09d', 479 | 'start_last_seq' => 2, 480 | 'start_time' => 'Thu, 10 Oct 2013 05:56:12 GMT', 481 | ), 482 | 2 => 483 | array ( 484 | 'doc_write_failures' => 0, 485 | 'docs_read' => 2, 486 | 'docs_written' => 2, 487 | 'end_last_seq' => 2, 488 | 'end_time' => 'Thu, 10 Oct 2013 05:56:04 GMT', 489 | 'missing_checked' => 2, 490 | 'missing_found' => 2, 491 | 'recorded_seq' => 2, 492 | 'session_id' => '77cdf93cde05f15fcb710f320c37c155', 493 | 'start_last_seq' => 0, 494 | 'start_time' => 'Thu, 10 Oct 2013 05:56:04 GMT', 495 | ), 496 | ), 497 | 'replication_id_version' => 3, 498 | 'session_id' => 'zzz34cbbdafa70e0db5cb57d02a6b955', 499 | 'source_last_seq' => 5, 500 | ), 501 | 3, 502 | 503 | ), 504 | array( 505 | array ( 506 | '_id' => '_local/b3e44b920ee2951cb2e123b63044427a', 507 | '_rev' => '0-8', 508 | 'history' => 509 | array ( 510 | 0 => 511 | array ( 512 | 'doc_write_failures' => 0, 513 | 'docs_read' => 2, 514 | 'docs_written' => 2, 515 | 'end_last_seq' => 5, 516 | 'end_time' => 'Thu, 10 Oct 2013 05:56:38 GMT', 517 | 'missing_checked' => 2, 518 | 'missing_found' => 2, 519 | 'recorded_seq' => 5, 520 | 'session_id' => 'd5a34cbbdafa70e0db5cb57d02a6b955', 521 | 'start_last_seq' => 3, 522 | 'start_time' => 'Thu, 10 Oct 2013 05:56:38 GMT', 523 | ), 524 | 1 => 525 | array ( 526 | 'doc_write_failures' => 0, 527 | 'docs_read' => 1, 528 | 'docs_written' => 1, 529 | 'end_last_seq' => 3, 530 | 'end_time' => 'Thu, 10 Oct 2013 05:56:12 GMT', 531 | 'missing_checked' => 1, 532 | 'missing_found' => 1, 533 | 'recorded_seq' => 3, 534 | 'session_id' => '11a79cdae1719c362e9857cd1ddff09d', 535 | 'start_last_seq' => 2, 536 | 'start_time' => 'Thu, 10 Oct 2013 05:56:12 GMT', 537 | ), 538 | 2 => 539 | array ( 540 | 'doc_write_failures' => 0, 541 | 'docs_read' => 2, 542 | 'docs_written' => 2, 543 | 'end_last_seq' => 2, 544 | 'end_time' => 'Thu, 10 Oct 2013 05:56:04 GMT', 545 | 'missing_checked' => 2, 546 | 'missing_found' => 2, 547 | 'recorded_seq' => 2, 548 | 'session_id' => '77cdf93cde05f15fcb710f320c37c155', 549 | 'start_last_seq' => 0, 550 | 'start_time' => 'Thu, 10 Oct 2013 05:56:04 GMT', 551 | ), 552 | ), 553 | 'replication_id_version' => 3, 554 | 'session_id' => 'd5a34cbbdafa70e0db5cb57d02a6b955', 555 | 'source_last_seq' => 5, 556 | ), 557 | null, 558 | 0 559 | ), 560 | array( 561 | null, 562 | null, 563 | 0 564 | ) 565 | ); 566 | } 567 | 568 | /** 569 | * Test the mapping done in getMapping 570 | * 571 | * @dataProvider changesFeedProvider 572 | */ 573 | public function testGetMapping($changes, $continuous, $expected) 574 | { 575 | $task = new ReplicationTask(); 576 | $task->setContinuous($continuous); 577 | $replication = new Replication($this->source, $this->target, $task); 578 | $mapping = $replication->getMapping($changes); 579 | $this->assertEquals($expected, $mapping, 'Incorrect mapping in getMapping.'); 580 | 581 | } 582 | 583 | public function changesFeedProvider() 584 | { 585 | $normal = array ( 586 | 'results' => 587 | array ( 588 | 0 => 589 | array ( 590 | 'seq' => 14, 591 | 'id' => 'f957f41e', 592 | 'changes' => 593 | array ( 594 | 0 => 595 | array ( 596 | 'rev' => '3-46a3', 597 | ), 598 | ), 599 | 'deleted' => true, 600 | ), 601 | 1 => 602 | array ( 603 | 'seq' => 29, 604 | 'id' => 'ddf339dd', 605 | 'changes' => 606 | array ( 607 | 0 => 608 | array ( 609 | 'rev' => '10-304b', 610 | ), 611 | ), 612 | ), 613 | 2 => 614 | array ( 615 | 'seq' => 39, 616 | 'id' => 'f13bd08b', 617 | 'changes' => 618 | array ( 619 | 0 => 620 | array ( 621 | 'rev' => '1-b35d', 622 | ), 623 | array( 624 | 'rev' => '1-535d', 625 | ) 626 | ), 627 | ), 628 | ), 629 | 'last_seq' => 78, 630 | ); 631 | $continuous = '{"seq":14,"id":"f957f41e","changes":[{"rev":"3-46a3"}],"deleted":true} 632 | {"seq":29,"id":"ddf339dd","changes":[{"rev":"10-304b"}]} 633 | {"seq":39,"id":"f13bd08b","changes":[{"rev":"1-b35d"},{"rev":"1-535d"}]}'; 634 | 635 | $expected = array ( 636 | 'f957f41e' => 637 | array ( 638 | 0 => '3-46a3', 639 | ), 640 | 'ddf339dd' => 641 | array ( 642 | 0 => '10-304b', 643 | ), 644 | 'f13bd08b' => 645 | array ( 646 | 0 => '1-b35d', 647 | 1 => '1-535d', 648 | ), 649 | ); 650 | return array( 651 | array( 652 | $normal, 653 | false, 654 | $expected 655 | ), 656 | array( 657 | $continuous, 658 | true, 659 | $expected 660 | ) 661 | ); 662 | } 663 | 664 | protected function tearDown() 665 | { 666 | } 667 | } 668 | -------------------------------------------------------------------------------- /tests/ReplicatorFunctionalTestBase.php: -------------------------------------------------------------------------------- 1 | getSourceTestDatabase() 26 | ); 27 | } 28 | 29 | public function getTargetCouchDBClient() 30 | { 31 | return new CouchDBClient( 32 | new SocketClient(), 33 | $this->getTargetTestDatabase() 34 | ); 35 | } 36 | 37 | public function getReplicationTask() 38 | { 39 | return new ReplicationTask(); 40 | } 41 | 42 | } -------------------------------------------------------------------------------- /tests/TestUtil.php: -------------------------------------------------------------------------------- 1 |