├── LICENSE ├── README.md ├── init_worker.lua ├── lib ├── JSON.lua ├── Valider.lua ├── ngx_tasks.lua └── posix.lua ├── module ├── heartbeat.lua ├── invalid.lua ├── invoke.lua └── ngx_cc_core.lua ├── nginx.conf ├── ngx_cc.lua └── patch ├── ngx-worker-port.patch ├── per-worker.patch └── run.sh /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ngx_cc 2 | 3 | A framework of Nginx Communication Cluster. reliable distranslation messages in nginx nodes and processes. 4 | 5 | The chinese intro document at here: [chinese wiki](https://github.com/aimingoo/ngx_cc/wiki/%E7%AE%80%E4%BB%8B). 6 | 7 | The framework support: 8 | 9 | > * communication between cluster nodes and worker processes, with directions: super/clients, master/workers 10 | > 11 | > * native ngx.location.capture* based, support coroutine sub-request 12 | > 13 | > * without cosocket (not dependent) 14 | > 15 | > * multi channels and sub-channels supported 16 | > 17 | > * multi-root or cross-cluster communication supported 18 | > 19 | > * full support NGX_4C programming architecture 20 | > 21 | > ``` 22 | > * http://github.com/aimingoo/ngx_4c 23 | > ``` 24 | 25 | The contents of current document: 26 | 27 | > * [environment](#environment) 28 | > * [run testcase with ngx_cc](#run-testcase-with-ngx_cc) 29 | > * [build cluster configures](#build-cluster-configures) 30 | > * [programming](#programming-with-ngx_cc) 31 | > * [APIs](#apis) 32 | > * [modules in the framework](#modules-in-the-framework) 33 | > * [locations in nginx.conf](#locations-in-nginxconf) 34 | > * [ngx_cc APIs](#ngx_cc-apis) 35 | > * [route APIs](#route-apis) 36 | > * [History and update](#history) 37 | 38 | ## environment 39 | 40 | Requirements: 41 | 42 | * nginx + lua 43 | * per-worker-listener patch, a update version included (original version from Roman Arutyunyan) 44 | 45 | Optional: 46 | 47 | * require ngx_tasks by heartbeat module, from: 48 | * http://github.com/aimingoo/ngx_tasks 49 | * require Valider by invalid module, from: 50 | * http://github.com/aimingoo/Valider 51 | * require JSON by simple/standard invoke module, from: 52 | * http://regex.info/blog/lua/json 53 | 54 | all optional module saved to ngx_cc/lib/*, by default. 55 | 56 | #### 1) install nginx+lua 57 | 58 | OpenResty is recommend([here](http://openresty.org/)),or install nginx+lua, see: 59 | 60 | > [http://wiki.nginx.org/HttpLuaModule#Installation](http://wiki.nginx.org/HttpLuaModule#Installation) 61 | 62 | #### 2) install per-worker-listener patch 63 | 64 | need install/apply these patchs before compile nginx+lua, see: 65 | 66 | > /patchs/run.sh 67 | 68 | please read the script, apply patchs and compile/rebuild nginx. 69 | 70 | #### 3) test in the nginx environment 71 | 72 | write a ngxin.conf, and put to home of current user: 73 | 74 | ``` conf 75 | ## 76 | ## vi ~/nginx.conf 77 | ## 78 | user nobody; 79 | worker_processes 4; 80 | 81 | events { 82 | worker_connections 10240; 83 | accept_mutex off; ## per-worker-listener patch required 84 | } 85 | 86 | http { 87 | server { 88 | listen 80; 89 | listen 8010 per_worker; ## per-worker-listener patch required 90 | server_name localhost; 91 | 92 | location /test { 93 | content_by_lua ' 94 | ngx.say("pid: " .. ngx.var.pid .. ", port: " .. ngx.var.server_port .. "\tOkay.")'; 95 | } 96 | } 97 | } 98 | ``` 99 | 100 | Ok. now start nginx and run testcase: 101 | 102 | ``` bash 103 | > sudo sbin/nginx -c ~/nginx.conf 104 | > for port in 80 {8010..8013}; do curl "http://127.0.0.1:$port/test"; done 105 | ``` 106 | 107 | if success, output(pid changeable): 108 | 109 | ``` 110 | pid: 24017, port: 80 Okay. 111 | pid: 24016, port: 8010 Okay. 112 | pid: 24017, port: 8011 Okay. 113 | pid: 24018, port: 8012 Okay. 114 | pid: 24019, port: 8013 Okay. 115 | ``` 116 | 117 | ## run testcase with ngx_cc 118 | 119 | download ngx_cc: 120 | 121 | ``` bash 122 | > cd ~ 123 | > git clone https://github.com/aimingoo/ngx_cc 124 | 125 | # or 126 | 127 | > cd ~ 128 | > wget https://github.com/aimingoo/ngx_cc/archive/master.zip -O ngx_cc.zip 129 | > unzip ngx_cc.zip -d ngx_cc/ 130 | > mv ngx_cc/ngx_cc-master/* ngx_cc/ 131 | > rm -rf ngx_cc/ngx_cc-master 132 | ``` 133 | 134 | run nginx with prefix parament: 135 | 136 | ``` bash 137 | > # cd home directory of nginx 138 | > sudo ./sbin/nginx -c ~/ngx_cc/nginx.conf -p ~/ngx_cc 139 | ``` 140 | 141 | and test it: 142 | 143 | > curl http://127.0.0.1/test/hub?getServiceStat 144 | 145 | {"clients":[],"ports":"8012,8011,8010,8013","routePort":"8011","service":"127.0.0.1:80"} 146 | 147 | > curl http://127.0.0.1/kada/hub?showMe 148 | 149 | Hi, Welcome to the ngx_cc cluster! 150 | 151 | ``` 152 | 153 | ## build cluster configures 154 | This is base demo for a simple cluster. 155 | 156 | 1) clone demo configures and change it 157 | ​```bash 158 | # clone configures 159 | > cp ~/work/ngx_cc/nginx.conf ~/work/ngx_cc/nginx.conf.1 160 | > cp ~/work/ngx_cc/nginx.conf ~/work/ngx_cc/nginx.conf.2 161 | 162 | # change next lines with '90' or '9091' and pid.1 163 | > grep -Pne 'pid\t|listen\t' ~/work/ngx_cc/nginx.conf.1 164 | 13:pid logs/nginx.pid.1; 165 | 36: listen 90; 166 | 37: listen 9010 per_worker; 167 | 168 | # change next lines with '100' or '10091' and pid.2 169 | > grep -Pne 'pid\t|listen\t' ~/work/ngx_cc/nginx.conf.2 170 | 13:pid logs/nginx.pid.2; 171 | 36: listen 100; 172 | 37: listen 10010 per_worker; 173 | ``` 174 | 175 | 2) run instances 176 | 177 | ``` bash 178 | > sudo ./nginx -c ~/work/ngx_cc/nginx.conf.1 -p ~/ngx_cc 179 | > sudo ./nginx -c ~/work/ngx_cc/nginx.conf.2 -p ~/ngx_cc 180 | ``` 181 | 182 | OK, now, you will get a cluster with next topology: [here](https://github.com/aimingoo/ngx_cc/wiki/images/cluster_arch_4.png) 183 | 184 | 3) and, more changes(clients for a client) 185 | 186 | ``` bash 187 | # clone configures 188 | > cp ~/work/ngx_cc/nginx.conf ~/work/ngx_cc/nginx.conf.11 189 | 190 | # clone init_worker.lua 191 | > cp ~/work/ngx_cc/init_worker.lua ~/work/ngx_cc/init_worker_11.lua 192 | 193 | # change next lines with '1100' or '11010' and pid.11 194 | > grep -Pne 'pid\t|listen\t' ~/work/ngx_cc/nginx.conf.11 195 | 13:pid logs/nginx.pid.11; 196 | 36: listen 1100; 197 | 37: listen 11010 per_worker; 198 | 199 | # change init_worker_11.lua, insert a line at after "require()" 200 | > grep -A1 -Fe "require('ngx_cc')" ~/work/ngx_cc/init_worker_11.lua 201 | ngx_cc = require('ngx_cc') 202 | ngx_cc.cluster.super = { host='127.0.0.1', port='90' } 203 | ``` 204 | 205 | 4) run instance with nginx.conf.11 206 | 207 | ``` bash 208 | > sudo ./nginx -c ~/work/ngx_cc/nginx.conf.11 209 | ``` 210 | 211 | 5) print server list 212 | 213 | ``` bash 214 | > curl -s 'http://127.0.0.1/test/hub?getServiceStat' | python -m json.tool 215 | { 216 | "clients": { 217 | "127.0.0.1:100": { 218 | "clients": [], 219 | "ports": "10011,10013,10010,10012", 220 | "routePort": "10012", 221 | "service": "127.0.0.1:100", 222 | "super": "127.0.0.1:80" 223 | }, 224 | "127.0.0.1:90": { 225 | "clients": { 226 | "127.0.0.1:1100": { 227 | "clients": [], 228 | "ports": "11011,11012,11010,11013", 229 | "routePort": "11013", 230 | "service": "127.0.0.1:1100", 231 | "super": "127.0.0.1:90" 232 | } 233 | }, 234 | "ports": "9011,9013,9012,9010", 235 | "routePort": "9010", 236 | "service": "127.0.0.1:90", 237 | "super": "127.0.0.1:80" 238 | } 239 | }, 240 | "ports": "8012,8010,8013,8011", 241 | "routePort": "8011", 242 | "service": "127.0.0.1:80" 243 | } 244 | ``` 245 | 246 | check error.log in $(nginx)/logs to get more 'ALERT' information about initialization processes. 247 | 248 | ## programming with ngx_cc 249 | 250 | #### 1) insert locations into your nginx.conf 251 | 252 | you need change your nginx.conf base demo configures, the demo in $(ngx_cc)/nginx.conf. 253 | 254 | and, insert three locations into your nginx.conf: 255 | 256 | ``` conf 257 | http { 258 | ... 259 | 260 | server { 261 | ... 262 | 263 | # caster, internal only 264 | location ~ ^/([^/]+)/cast { 265 | internal; 266 | set_by_lua $cc_host 'return ngx.var.cc_host'; 267 | set_by_lua $cc_port 'local p = ngx.var.cc_port or ""; return (p=="" or p=="80") and "" or ":"..p;'; 268 | rewrite ^/([^/]+)/cast/(.*)$ /$2 break; # if no match, will continue next-rewrite, skip current opt 269 | rewrite ^/([^/]+)/cast([^/]*)$ /$1/invoke$2 break; 270 | proxy_pass http://$cc_host$cc_port; 271 | proxy_read_timeout 2s; 272 | proxy_send_timeout 2s; 273 | proxy_connect_timeout 2s; 274 | } 275 | 276 | # invoke at worker listen port 277 | location ~ ^/([^/]+)/invoke { 278 | content_by_lua 'ngx_cc.invokes(ngx.var[1])'; 279 | } 280 | 281 | # hub at main listen port(80), will redirect to route listen port 282 | location ~ ^/([^/]+)/hub { 283 | content_by_lua 'ngx_cc.invokes(ngx.var[1], true)'; 284 | } 285 | ``` 286 | 287 | #### 2) add shared dictionary in your nginx.conf 288 | 289 | ``` conf 290 | http { 291 | ... 292 | 293 | server { 294 | ... 295 | 296 | lua_shared_dict ngxcc_dict 10M; 297 | ``` 298 | 299 | #### 3) initialization ngx_cc in init_worker.lua 300 | 301 | configures of <init_worker_by_lua_file> in your nginx.conf: 302 | 303 | ``` conf 304 | http { 305 | ... 306 | init_worker_by_lua_file '/init_worker.lua'; 307 | ``` 308 | 309 | and coding in init_worker.lua: 310 | 311 | ``` lua 312 | -- get ngx_cc instance 313 | ngx_cc = require('ngx_cc') 314 | 315 | -- get route instance for 'test' channel 316 | route = ngx_cc:new('test') -- work with 'test' channel 317 | 318 | -- load invokes of 'test' channel 319 | require('module.invoke').apply(route) -- it's default module 320 | require('module.heartbeat').apply(route) -- it's heartbeat module 321 | require('module.YOURMODULE').apply(route) -- your modules 322 | ``` 323 | 324 | #### 4) write invokes in modules/* 325 | 326 | write your invokes and put into $(ngx_cc)/modules/<YOURMODULE>.lua: 327 | 328 | ``` lua 329 | -- a demo invoke module 330 | local function apply(invoke) 331 | invoke.XXXX = function(route, channel, arg) 332 | ... 333 | end 334 | ... 335 | end 336 | 337 | -- return invokes helper object 338 | return { 339 | apply = function(route) 340 | return apply(route.invoke) 341 | end 342 | } 343 | ``` 344 | 345 | #### 5) test your invokes 346 | 347 | ``` bash 348 | # run nginx with your nginx.conf, and call the test: 349 | > curl 'http://127.0.0.1/test/invoke?XXXX 350 | ``` 351 | 352 | ## APIs 353 | 354 | There are published APIs of ngx_cc. The <ngx_cc> and <route > APIs for these intances/variants: 355 | 356 | ## ```lua 357 | 358 | ## -- (in init_workers.lua) 359 | 360 | -- get ngx_cc instance 361 | 362 | ngx_cc = require('ngx_cc') 363 | 364 | -- get route instance for 'test' channel 365 | 366 | route = ngx_cc:new('test') -- work with 'test' channel 367 | 368 | ``` 369 | ### modules in the framework 370 | ``` 371 | 372 | > cd ~/ngx_cc/ 373 | > 374 | > ls 375 | 376 | ngx_cc.lua -- main module 377 | 378 | init_worker.lua -- (demo init_worker.lua) 379 | 380 | nginx.conf -- (demo nginx.conf) 381 | 382 | > cd module/ 383 | > 384 | > ls 385 | 386 | ngx_cc_core.lua -- core module, load by ngx_cc.lua 387 | 388 | heartbeat.lua -- (heartbeat module, optional) 389 | 390 | invoke.lua -- (getServiceStat action, optional) 391 | 392 | invalid.lua -- (cluster invalid check, optional) 393 | 394 | > cd ../lib/ 395 | > 396 | > ls 397 | 398 | JSON.lua -- JSON format output, dependency by getServiceStat action. 399 | 400 | ngx_tasks.lua -- tasks management, dependency by ngx_cc.tasks interface, a example in module/heartbeat.lua 401 | 402 | Valider.lua -- invalid rate check, dependency by module/invalid.lua 403 | 404 | posix.lua -- a minimum posix system module 405 | 406 | > cd ../patch 407 | > 408 | > ls 409 | 410 | per-worker.patch -- 'per_worker' directive in nginx.conf 411 | 412 | ngx-worker-port.patch -- 'ngx.worker.port()' api in ngx_lua 413 | 414 | run.sh -- a demo launch 415 | 416 | ``` 417 | 418 | ### locations in nginx.conf 419 | these locations in nginx.conf: 420 | ​```conf 421 | location ~ ^/([^/]+)/cast 422 | location ~ ^/([^/]+)/invoke 423 | location ~ ^/([^/]+)/hub 424 | ``` 425 | 426 | The regexp: 427 | 428 | > ^/([^/]+) 429 | 430 | will match channel_name of your request/api accesses. ex: 431 | 432 | ``` bash 433 | > curl 'http://127.0.0.1/test/invoke?XXXX 434 | ``` 435 | 436 | OR 437 | 438 | ``` lua 439 | -- lua code 440 | route.cc('/test/invoke?XXXX', { direction = 'workers' }) 441 | ``` 442 | 443 | for these cases, the channel_name is 'test'. 444 | 445 | ##### 1) location: /channel_name/cast 446 | 447 | it's internal sub-request of ngx_cc. don't access it in your program. if you want boardcast messages, need call route.cast() from your code. 448 | 449 | ##### 2) location: /channel_name/invoke 450 | 451 | it's main sub-request, access with route.cc() is commented. and you can seed http request from client(or anywhere): 452 | 453 | ``` bash 454 | > curl 'http://127.0.0.1/test/invoke?XXXX 455 | ``` 456 | 457 | the 'XXXX' is <action_name> invoked at <channel_name>. 458 | 459 | '/channel_name/invoke' will launch single worker process to answer request, it's unicast. 460 | 461 | ##### 3) location: /channel_name/hub 462 | 463 | it's warp of '/channel_name/invoke' location. 464 | 465 | the '/channel_name/hub' will launch 'router process' to answer request. the router is unique elected process by all workers. 466 | 467 | ### ngx_cc APIs 468 | 469 | ``` 470 | # internal 471 | ngx_cc.tasks : default tasks, drive by tasks management module/plugins, see module/heartbeat.lua or ngx_tasks project. 472 | ngx_cc.channels : channel list 473 | ngx_cc.cluster : cluster infomation 474 | ngx_cc.invokes() : internal invoker 475 | 476 | # ngx_cc version 1.x 477 | ngx_cc:new() : create communication channel and return its route 478 | ngx_cc.optionAgain() : options generater of ngx_cc.cc() 479 | ngx_cc.optionAgain2(): a warp of optionAgain() 480 | ngx_cc.say() : output http responses 481 | 482 | # ngx_cc version 2.x 483 | ngx_cc.self() : warp of ngx.location.capture(), send sub-request to myself with current context 484 | route.remote() : remote procedure call(RPC) for multi-root architecture, base RESTApi or direct http request 485 | ngx_cc.transfer() : online transfer some clients to new super 486 | ngx_cc.all() : batch communication request, call and return once, ngx.thread based 487 | ``` 488 | 489 | ###### >> ngx_cc:new 490 | 491 | > function ngx_cc:new(channel, options) 492 | 493 | try get a route instance workat 'test' channel, and with default options: 494 | 495 | ``` lua 496 | route = ngx_cc:new('test') 497 | ``` 498 | 499 | the default options is: 500 | 501 | ``` lua 502 | options = { 503 | host = '127.0.0.1', 504 | port = '80', 505 | dict = 'test_dict', -- 'channel_name' .. '_dict' 506 | initializer = 'automatic' 507 | } 508 | ``` 509 | 510 | you can put custom options, ex: 511 | 512 | ``` lua 513 | route = ngx_cc:new('test', { port = '90' }) 514 | ``` 515 | 516 | ###### >> ngx_cc.say 517 | 518 | > function ngx_cc.say(r_status, r_resps) 519 | 520 | print/write context into current http responses with a route.cc() communication: 521 | 522 | ``` lua 523 | ngx_cc.say(route.cc('/_/invoke')) 524 | ``` 525 | 526 | for 'workers'/'clients' direction, will output all response body of success communication. 527 | 528 | ###### >> ngx_cc.self and ngx_cc.remote 529 | 530 | > function ngx_cc.self(url, opt) 531 | 532 | > 533 | 534 | > function ngx_cc.remote(url, opt) 535 | 536 | ngx_cc.self() will send a sub-request from current request context. it's warp of ngx.location.capture*, with same interface of route.cc(). 537 | 538 | ngx_cc.remote() will send a rpc(remote process call). so, the full remote url is required for 'url' parament. 539 | 540 | > ngx_cc.self/remote is **none channel dependency**, so you can call them without communication channel. 541 | > 542 | > ngx_cc.self() unsupport '_' replacement symbol, but route.self() is supported. 543 | 544 | ###### >> ngx_cc.transfer 545 | 546 | > function ngx_cc.transfer(super, channels, clients) 547 | 548 | ``` 549 | paraments: 550 | super - string, 'HOST:PORT' 551 | channels - string, 'channelName1,channelName2,...', or '*' 552 | clients - string, 'ip1,ip2,ip3,...', or '*' 553 | ``` 554 | 555 | Transfer these **clients** to new **super** at these **channels**. the api will rewrite clients register table in shared dictionary, and send transfer command to these **clients**. 556 | 557 | **clients** will invoke the command and transfer himself. 558 | 559 | ###### >> ngx_cc.all 560 | 561 | > function ngx_cc.all(requests, comment) -- comment is log only 562 | 563 | ngx_cc.all() will batch send all requests. the request define: 564 | 565 | > { cc_command, arg1, arg2, ... } 566 | 567 | so, you can push any command/call, ex: 568 | 569 | ``` lua 570 | local reuests = {} 571 | table.insert(reuests, {ngx_cc.remote, 'a_url', a_option_table}) 572 | table.insert(reuests, {ngx_cc.all, request2}) 573 | table.insert(reuests, {route.cc, 'a_url', a_option_table}) 574 | 575 | local ok, resps = ngx_cc.all(reuests) 576 | ngx_cc.say(ok, resps) 577 | ``` 578 | 579 | ###### >> ngx_cc.invokes 580 | 581 | > function ngx_cc.invokes(channel, master_only) 582 | 583 | will call from localtions in nginx.conf only: 584 | 585 | ``` conf 586 | # invoke at worker listen port 587 | location ~ ^/([^/]+)/invoke { 588 | content_by_lua 'ngx_cc.invokes(ngx.var[1])'; 589 | } 590 | 591 | # hub at main listen port(80), will redirect to route listen port 592 | location ~ ^/([^/]+)/hub { 593 | content_by_lua 'ngx_cc.invokes(ngx.var[1], true)'; 594 | } 595 | ``` 596 | 597 | ###### >> ngx_cc.optionAgain and ngx_cc.optionAgain2 598 | 599 | > function ngx_cc.optionAgain(direction, opt) 600 | > 601 | > function ngx_cc.optionAgain2(direction, opt, force_mix_into_current) 602 | 603 | the 'opt' parament see: options of [ngx.location.capture*](http://wiki.nginx.org/HttpLuaModule#ngx.location.capture) 604 | 605 | the 'direction' parament is string(will copy to opt.direction): 606 | 607 | ``` lua 608 | -- 'super' : 1:1 send to super node, the super is parent node. 609 | -- 'master' : 1:1 send to router process from any worker 610 | -- 'workers': 1:* send to all workers 611 | -- 'clients': 1:* send to all clients 612 | ``` 613 | 614 | Usage: 615 | 616 | > * function ngx_cc.optionAgain() 617 | > * function ngx_cc.optionAgain('direction') 618 | > * function ngx_cc.optionAgain(direction_object) 619 | > * function ngx_cc.optionAgain(direction_object, opt) 620 | 621 | examples: 622 | 623 | ``` lua 624 | -- case 1 625 | route.cc('/_/invoke', ngx_cc.optionAgain()) 626 | ``` 627 | 628 | will return default option object: 629 | 630 | ``` lua 631 | default_opt = { 632 | direction = 'master', -- default 633 | method = ngx.req.get_method(), 634 | args = ngx.req.get_uri_args(), 635 | body = ngx.req.get_body_data() 636 | } 637 | ``` 638 | 639 | and, 640 | 641 | ``` lua 642 | -- case 2 643 | route.cc('/_/invoke', ngx_cc.optionAgain('clients')) 644 | ``` 645 | 646 | will return default option object, but opt.direction is 'clients'. 647 | 648 | ``` lua 649 | -- case 3 650 | ngx_cc.optionAgain({ 651 | direction = 'super', 652 | body = '' 653 | }) 654 | ``` 655 | 656 | will mix these options 657 | 658 | * { direction = 'super', body = '' } 659 | 660 | into default option object. 661 | 662 | ``` lua 663 | -- case 4 664 | ngx_cc.optionAgain({ 665 | direction = 'super', 666 | args = { 667 | tryDoSomething = false, 668 | doSomething = true 669 | } 670 | }, { 671 | method = 'POST', 672 | body = '' 673 | }) 674 | ``` 675 | 676 | will get mixed options beetwen direction_object and opt, but default_option_object is ignored. 677 | 678 | ### route APIs 679 | 680 | ``` 681 | # internal 682 | route.cluster : cluster infomation for current worker process 683 | 684 | # inherited from ngx_cc 685 | route.say() : see: ngx_cc.say() 686 | route.self() : see: ngx_cc.self(), support '_' replacement symbol 687 | route.remote() : see: ngx_cc.remote() 688 | route.optionAgain() : see: ngx_cc.optionAgain() 689 | route.optionAgain2() : see: ngx_cc.optionAgain2() 690 | 691 | # ngx_cc version 1.x 692 | route.cc() : main communication function, support directions: super/master/clients/workers 693 | route.cast() : communication at 'workers' direction only 694 | route.isRoot() : utility,check root node for current 695 | route.isInvokeAtMaster() : utility,check master/router node for current, and force communication invoke at 'master' 696 | 697 | # ngx_cc version 2.x 698 | route.isInvokeAtPer() : utility,check current is worker node, and force communication invoke at 'workers' 699 | route.transfer() : override ngx_cc.transfer(), transfer current worker only. 700 | ``` 701 | 702 | ###### >> route.cc and route.cast 703 | 704 | > function route.cc(url, opt) 705 | > 706 | > function route.cast(url, opt) 707 | 708 | the 'url' parament include pattens: 709 | 710 | ``` 711 | '/_/invoke' : a action invoking, the action_name setting in options 712 | '/_/invoke/ding' : a remote call with uri '/ding' 713 | '/_/cast/ding' : a remote call with uri '/ding', and target service will boardcast the message. 714 | '/_/_/test/invokeXXX' : a sub-channel 'XXX' invoke at 'test' channel 715 | ``` 716 | 717 | the 'opt' parament see: [ngx_cc.optionAgain](https://github.com/aimingoo/ngx_cc/blob/master/README.md#-ngx_ccoptionagain-and-ngx_ccoptionagain2) 718 | 719 | if you want send 'AAA' as action_name for all 'workers', the communication implement by these codes: 720 | 721 | ``` lua 722 | route.cc('/_/invoke', { 723 | direction = 'workers', 724 | args = { AAA = true } 725 | }) 726 | 727 | -- OR 728 | 729 | route.cc('/_/invoke?AAA', { direction = 'workers' }) 730 | ``` 731 | 732 | and, if you want copy all from current request context(ngx.vars/ctx/body...), ex: 733 | 734 | ``` lua 735 | route.cc('/_/invoke?AAA', ngx_cc.optionAgain('workers')) 736 | ``` 737 | 738 | ###### >> route.self and route.remote 739 | 740 | > function route.self(url, opt) 741 | > 742 | > function route.remote(url, opt) 743 | 744 | route.self() will send a sub-request from current request context. it's warp of ngx.location.capture*, with same interface of route.cc(). 745 | 746 | route.remote() will send a rpc(remote process call). so, the full remote url is required for 'url' parament. 747 | 748 | ###### >> route.isRoot() 749 | 750 | > function route.isRoot() 751 | 752 | check root node for current process. equivlant to: 753 | 754 | > * cluster.master.host == cluster.super.host 755 | > 756 | > and 757 | > 758 | > * cluster.master.port == cluster.super.port 759 | 760 | if current is root node, then route.cc() with "direction = 'super'" will ignored, and return: 761 | 762 | > * r_status, r_resps = true, {} 763 | 764 | ###### >> route.isInvokeAtMaster() 765 | 766 | > function route.isInvokeAtMaster() 767 | 768 | if current is master, then isInvokeAtMaster() return true only. else, will return false and re-send current request to real master/route node. 769 | 770 | the function will force invoke at 'master/router' of current communication always. there is simple example, try it in your invoke code: 771 | 772 | ``` lua 773 | route.invoke.XXX = function() 774 | local no_redirected, r_status, r_resps = route.isInvokeAtMaster() 775 | if no_redirected then 776 | ... -- your process for action XXX 777 | else 778 | route.say(r_status, r_resps) 779 | end 780 | end 781 | ``` 782 | 783 | ###### >> route.isInvokeAtPer() 784 | 785 | > function route.isInvokeAtPer() 786 | 787 | if current worker is listen at per-worker port, then isInvokeAtPer() return true only. else, will return false and re-send current request to real all per-workers. 788 | 789 | the function will force invoke at 'workers' of current communication always. so will try request for per-workers, **and** process same action by per-worker. there is simple example, try it in your invoke code: 790 | 791 | ``` lua 792 | route.invoke.XXX = function() 793 | local no_redirected, r_status, r_resps = route.isInvokeAtPer() 794 | if no_redirected then 795 | ... -- your process for action XXX 796 | else 797 | route.say(r_status, r_resps) 798 | end 799 | end 800 | ``` 801 | 802 | another, a real case in module/invoke.lua. 803 | 804 | ## History 805 | 806 | ``` text 807 | 2016.01.13 release v2.1.2, minor bug fix, thanks for stone-wind, he is 1st reporter of the project. 808 | 809 | 2015.11.04 release v2.1.1, channel_resources as native resource management 810 | - n4c_supported tag removed in ngx_cc.lua 811 | - n4c dependency removed in module/invalid.lua 812 | 813 | 2015.10.22 release v2.1.0, publish NGX_CC node as N4C resources 814 | - support N4C resource management(setting "n4c_supported" in ngx_cc.lua) and high performance node list access 815 | - support N4C distribution node management 816 | - support master/worker process crash check and dynamic restore 817 | - procfs_process.lua removed, get parent_pid by LuaJIT now 818 | - status check is safe&correct by HTTP_SUCCESS() 819 | 820 | 2015.08.13 release v2.0.0, support NGX_4C programming architecture 821 | - single shared dictionary multi channels 822 | - custom headers when pass_proxy or ngx.location.capture (require NGX_4C framework) 823 | - support super/client nodes online/dynamic transfer in cluster 824 | - support cluster/node invalid check (require module/invalid.lua) 825 | - single node invalid, will restart and put it to cluster 826 | - super invalid, will waiting and try reconnection 827 | - clients and workers invalid, will report to master of channel and try remove it 828 | - add api 829 | - route.isInvokeAtPer() 830 | - ngx_cc.all(), ngx_cc.transfer(), ngx_cc.remote(), ngx_cc.self() 831 | - update ngx_cc core framework 832 | - requestSelf_BASH/requestSelf_DIRECT/requestSelf_CC is optional 833 | - ssl port 433 is protected 834 | - BUGFIX: post body lost 835 | - UPDATE: standard/sample nginx.conf 836 | 837 | 2015.02 release v1.0.0 838 | ``` -------------------------------------------------------------------------------- /init_worker.lua: -------------------------------------------------------------------------------- 1 | -- ----------------------------------------------------------------------------- 2 | -- This is a sample init_worker.lua of ngx_cc 3 | -- 1) please read sample nginx.conf first 4 | -- 2) multi channels is optional, all functions can work in singel channel 5 | -- 3) tasks management(and heartbeat module) is optional 6 | -- 4) need install lua-process module on macosx(procfs is unsupported) 7 | -- - https://github.com/mah0x211/lua-process 8 | -- ----------------------------------------------------------------------------- 9 | 10 | -- load ngx_cc framework 11 | ngx_cc = require('ngx_cc') 12 | 13 | -- case one: base test 14 | route = ngx_cc:new('test') -- work at 'test' channel 15 | require('module.invoke').apply(route) 16 | 17 | -- case two: full test, more module/feature to route2.invoke 18 | -- 1) with sample invokes 19 | -- 2) with heartbeat (include tasks management) 20 | -- 3) with cluster invalid checker 21 | route2 = ngx_cc:new('kada') -- work at 'kada' channel 22 | require('module.invoke').apply(route2) 23 | require('module.heartbeat').apply(route2) 24 | require('module.invalid').apply(route2) 25 | 26 | -- custom route2.invoke 27 | route2.invoke.showMe = function() 28 | ngx.say('Hi, Welcome to the ngx_cc cluster.') 29 | end 30 | 31 | ngx.log(ngx.ALERT, 'DONE. in init_worker.lua') -------------------------------------------------------------------------------- /lib/JSON.lua: -------------------------------------------------------------------------------- 1 | -- -*- coding: utf-8 -*- 2 | -- 3 | -- Simple JSON encoding and decoding in pure Lua. 4 | -- 5 | -- Copyright 2010-2014 Jeffrey Friedl 6 | -- http://regex.info/blog/ 7 | -- 8 | -- Latest version: http://regex.info/blog/lua/json 9 | -- 10 | -- This code is released under a Creative Commons CC-BY "Attribution" License: 11 | -- http://creativecommons.org/licenses/by/3.0/deed.en_US 12 | -- 13 | -- It can be used for any purpose so long as the copyright notice above, 14 | -- the web-page links above, and the 'AUTHOR_NOTE' string below are 15 | -- maintained. Enjoy. 16 | -- 17 | local VERSION = 20141223.14 -- version history at end of file 18 | local AUTHOR_NOTE = "-[ JSON.lua package by Jeffrey Friedl (http://regex.info/blog/lua/json) version 20141223.14 ]-" 19 | 20 | -- 21 | -- The 'AUTHOR_NOTE' variable exists so that information about the source 22 | -- of the package is maintained even in compiled versions. It's also 23 | -- included in OBJDEF below mostly to quiet warnings about unused variables. 24 | -- 25 | local OBJDEF = { 26 | VERSION = VERSION, 27 | AUTHOR_NOTE = AUTHOR_NOTE, 28 | } 29 | 30 | 31 | -- 32 | -- Simple JSON encoding and decoding in pure Lua. 33 | -- http://www.json.org/ 34 | -- 35 | -- 36 | -- JSON = assert(loadfile "JSON.lua")() -- one-time load of the routines 37 | -- 38 | -- local lua_value = JSON:decode(raw_json_text) 39 | -- 40 | -- local raw_json_text = JSON:encode(lua_table_or_value) 41 | -- local pretty_json_text = JSON:encode_pretty(lua_table_or_value) -- "pretty printed" version for human readability 42 | -- 43 | -- 44 | -- 45 | -- DECODING (from a JSON string to a Lua table) 46 | -- 47 | -- 48 | -- JSON = assert(loadfile "JSON.lua")() -- one-time load of the routines 49 | -- 50 | -- local lua_value = JSON:decode(raw_json_text) 51 | -- 52 | -- If the JSON text is for an object or an array, e.g. 53 | -- { "what": "books", "count": 3 } 54 | -- or 55 | -- [ "Larry", "Curly", "Moe" ] 56 | -- 57 | -- the result is a Lua table, e.g. 58 | -- { what = "books", count = 3 } 59 | -- or 60 | -- { "Larry", "Curly", "Moe" } 61 | -- 62 | -- 63 | -- The encode and decode routines accept an optional second argument, 64 | -- "etc", which is not used during encoding or decoding, but upon error 65 | -- is passed along to error handlers. It can be of any type (including nil). 66 | -- 67 | -- 68 | -- 69 | -- ERROR HANDLING 70 | -- 71 | -- With most errors during decoding, this code calls 72 | -- 73 | -- JSON:onDecodeError(message, text, location, etc) 74 | -- 75 | -- with a message about the error, and if known, the JSON text being 76 | -- parsed and the byte count where the problem was discovered. You can 77 | -- replace the default JSON:onDecodeError() with your own function. 78 | -- 79 | -- The default onDecodeError() merely augments the message with data 80 | -- about the text and the location if known (and if a second 'etc' 81 | -- argument had been provided to decode(), its value is tacked onto the 82 | -- message as well), and then calls JSON.assert(), which itself defaults 83 | -- to Lua's built-in assert(), and can also be overridden. 84 | -- 85 | -- For example, in an Adobe Lightroom plugin, you might use something like 86 | -- 87 | -- function JSON:onDecodeError(message, text, location, etc) 88 | -- LrErrors.throwUserError("Internal Error: invalid JSON data") 89 | -- end 90 | -- 91 | -- or even just 92 | -- 93 | -- function JSON.assert(message) 94 | -- LrErrors.throwUserError("Internal Error: " .. message) 95 | -- end 96 | -- 97 | -- If JSON:decode() is passed a nil, this is called instead: 98 | -- 99 | -- JSON:onDecodeOfNilError(message, nil, nil, etc) 100 | -- 101 | -- and if JSON:decode() is passed HTML instead of JSON, this is called: 102 | -- 103 | -- JSON:onDecodeOfHTMLError(message, text, nil, etc) 104 | -- 105 | -- The use of the fourth 'etc' argument allows stronger coordination 106 | -- between decoding and error reporting, especially when you provide your 107 | -- own error-handling routines. Continuing with the the Adobe Lightroom 108 | -- plugin example: 109 | -- 110 | -- function JSON:onDecodeError(message, text, location, etc) 111 | -- local note = "Internal Error: invalid JSON data" 112 | -- if type(etc) = 'table' and etc.photo then 113 | -- note = note .. " while processing for " .. etc.photo:getFormattedMetadata('fileName') 114 | -- end 115 | -- LrErrors.throwUserError(note) 116 | -- end 117 | -- 118 | -- : 119 | -- : 120 | -- 121 | -- for i, photo in ipairs(photosToProcess) do 122 | -- : 123 | -- : 124 | -- local data = JSON:decode(someJsonText, { photo = photo }) 125 | -- : 126 | -- : 127 | -- end 128 | -- 129 | -- 130 | -- 131 | -- 132 | -- 133 | -- DECODING AND STRICT TYPES 134 | -- 135 | -- Because both JSON objects and JSON arrays are converted to Lua tables, 136 | -- it's not normally possible to tell which original JSON type a 137 | -- particular Lua table was derived from, or guarantee decode-encode 138 | -- round-trip equivalency. 139 | -- 140 | -- However, if you enable strictTypes, e.g. 141 | -- 142 | -- JSON = assert(loadfile "JSON.lua")() --load the routines 143 | -- JSON.strictTypes = true 144 | -- 145 | -- then the Lua table resulting from the decoding of a JSON object or 146 | -- JSON array is marked via Lua metatable, so that when re-encoded with 147 | -- JSON:encode() it ends up as the appropriate JSON type. 148 | -- 149 | -- (This is not the default because other routines may not work well with 150 | -- tables that have a metatable set, for example, Lightroom API calls.) 151 | -- 152 | -- 153 | -- ENCODING (from a lua table to a JSON string) 154 | -- 155 | -- JSON = assert(loadfile "JSON.lua")() -- one-time load of the routines 156 | -- 157 | -- local raw_json_text = JSON:encode(lua_table_or_value) 158 | -- local pretty_json_text = JSON:encode_pretty(lua_table_or_value) -- "pretty printed" version for human readability 159 | -- local custom_pretty = JSON:encode(lua_table_or_value, etc, { pretty = true, indent = "| ", align_keys = false }) 160 | -- 161 | -- On error during encoding, this code calls: 162 | -- 163 | -- JSON:onEncodeError(message, etc) 164 | -- 165 | -- which you can override in your local JSON object. 166 | -- 167 | -- The 'etc' in the error call is the second argument to encode() 168 | -- and encode_pretty(), or nil if it wasn't provided. 169 | -- 170 | -- 171 | -- PRETTY-PRINTING 172 | -- 173 | -- An optional third argument, a table of options, allows a bit of 174 | -- configuration about how the encoding takes place: 175 | -- 176 | -- pretty = JSON:encode(val, etc, { 177 | -- pretty = true, -- if false, no other options matter 178 | -- indent = " ", -- this provides for a three-space indent per nesting level 179 | -- align_keys = false, -- see below 180 | -- }) 181 | -- 182 | -- encode() and encode_pretty() are identical except that encode_pretty() 183 | -- provides a default options table if none given in the call: 184 | -- 185 | -- { pretty = true, align_keys = false, indent = " " } 186 | -- 187 | -- For example, if 188 | -- 189 | -- JSON:encode(data) 190 | -- 191 | -- produces: 192 | -- 193 | -- {"city":"Kyoto","climate":{"avg_temp":16,"humidity":"high","snowfall":"minimal"},"country":"Japan","wards":11} 194 | -- 195 | -- then 196 | -- 197 | -- JSON:encode_pretty(data) 198 | -- 199 | -- produces: 200 | -- 201 | -- { 202 | -- "city": "Kyoto", 203 | -- "climate": { 204 | -- "avg_temp": 16, 205 | -- "humidity": "high", 206 | -- "snowfall": "minimal" 207 | -- }, 208 | -- "country": "Japan", 209 | -- "wards": 11 210 | -- } 211 | -- 212 | -- The following three lines return identical results: 213 | -- JSON:encode_pretty(data) 214 | -- JSON:encode_pretty(data, nil, { pretty = true, align_keys = false, indent = " " }) 215 | -- JSON:encode (data, nil, { pretty = true, align_keys = false, indent = " " }) 216 | -- 217 | -- An example of setting your own indent string: 218 | -- 219 | -- JSON:encode_pretty(data, nil, { pretty = true, indent = "| " }) 220 | -- 221 | -- produces: 222 | -- 223 | -- { 224 | -- | "city": "Kyoto", 225 | -- | "climate": { 226 | -- | | "avg_temp": 16, 227 | -- | | "humidity": "high", 228 | -- | | "snowfall": "minimal" 229 | -- | }, 230 | -- | "country": "Japan", 231 | -- | "wards": 11 232 | -- } 233 | -- 234 | -- An example of setting align_keys to true: 235 | -- 236 | -- JSON:encode_pretty(data, nil, { pretty = true, indent = " ", align_keys = true }) 237 | -- 238 | -- produces: 239 | -- 240 | -- { 241 | -- "city": "Kyoto", 242 | -- "climate": { 243 | -- "avg_temp": 16, 244 | -- "humidity": "high", 245 | -- "snowfall": "minimal" 246 | -- }, 247 | -- "country": "Japan", 248 | -- "wards": 11 249 | -- } 250 | -- 251 | -- which I must admit is kinda ugly, sorry. This was the default for 252 | -- encode_pretty() prior to version 20141223.14. 253 | -- 254 | -- 255 | -- AMBIGUOUS SITUATIONS DURING THE ENCODING 256 | -- 257 | -- During the encode, if a Lua table being encoded contains both string 258 | -- and numeric keys, it fits neither JSON's idea of an object, nor its 259 | -- idea of an array. To get around this, when any string key exists (or 260 | -- when non-positive numeric keys exist), numeric keys are converted to 261 | -- strings. 262 | -- 263 | -- For example, 264 | -- JSON:encode({ "one", "two", "three", SOMESTRING = "some string" })) 265 | -- produces the JSON object 266 | -- {"1":"one","2":"two","3":"three","SOMESTRING":"some string"} 267 | -- 268 | -- To prohibit this conversion and instead make it an error condition, set 269 | -- JSON.noKeyConversion = true 270 | -- 271 | 272 | 273 | 274 | 275 | -- 276 | -- SUMMARY OF METHODS YOU CAN OVERRIDE IN YOUR LOCAL LUA JSON OBJECT 277 | -- 278 | -- assert 279 | -- onDecodeError 280 | -- onDecodeOfNilError 281 | -- onDecodeOfHTMLError 282 | -- onEncodeError 283 | -- 284 | -- If you want to create a separate Lua JSON object with its own error handlers, 285 | -- you can reload JSON.lua or use the :new() method. 286 | -- 287 | --------------------------------------------------------------------------- 288 | 289 | local default_pretty_indent = " " 290 | local default_pretty_options = { pretty = true, align_keys = false, indent = default_pretty_indent } 291 | 292 | local isArray = { __tostring = function() return "JSON array" end } isArray.__index = isArray 293 | local isObject = { __tostring = function() return "JSON object" end } isObject.__index = isObject 294 | 295 | 296 | function OBJDEF:newArray(tbl) 297 | return setmetatable(tbl or {}, isArray) 298 | end 299 | 300 | function OBJDEF:newObject(tbl) 301 | return setmetatable(tbl or {}, isObject) 302 | end 303 | 304 | local function unicode_codepoint_as_utf8(codepoint) 305 | -- 306 | -- codepoint is a number 307 | -- 308 | if codepoint <= 127 then 309 | return string.char(codepoint) 310 | 311 | elseif codepoint <= 2047 then 312 | -- 313 | -- 110yyyxx 10xxxxxx <-- useful notation from http://en.wikipedia.org/wiki/Utf8 314 | -- 315 | local highpart = math.floor(codepoint / 0x40) 316 | local lowpart = codepoint - (0x40 * highpart) 317 | return string.char(0xC0 + highpart, 318 | 0x80 + lowpart) 319 | 320 | elseif codepoint <= 65535 then 321 | -- 322 | -- 1110yyyy 10yyyyxx 10xxxxxx 323 | -- 324 | local highpart = math.floor(codepoint / 0x1000) 325 | local remainder = codepoint - 0x1000 * highpart 326 | local midpart = math.floor(remainder / 0x40) 327 | local lowpart = remainder - 0x40 * midpart 328 | 329 | highpart = 0xE0 + highpart 330 | midpart = 0x80 + midpart 331 | lowpart = 0x80 + lowpart 332 | 333 | -- 334 | -- Check for an invalid character (thanks Andy R. at Adobe). 335 | -- See table 3.7, page 93, in http://www.unicode.org/versions/Unicode5.2.0/ch03.pdf#G28070 336 | -- 337 | if ( highpart == 0xE0 and midpart < 0xA0 ) or 338 | ( highpart == 0xED and midpart > 0x9F ) or 339 | ( highpart == 0xF0 and midpart < 0x90 ) or 340 | ( highpart == 0xF4 and midpart > 0x8F ) 341 | then 342 | return "?" 343 | else 344 | return string.char(highpart, 345 | midpart, 346 | lowpart) 347 | end 348 | 349 | else 350 | -- 351 | -- 11110zzz 10zzyyyy 10yyyyxx 10xxxxxx 352 | -- 353 | local highpart = math.floor(codepoint / 0x40000) 354 | local remainder = codepoint - 0x40000 * highpart 355 | local midA = math.floor(remainder / 0x1000) 356 | remainder = remainder - 0x1000 * midA 357 | local midB = math.floor(remainder / 0x40) 358 | local lowpart = remainder - 0x40 * midB 359 | 360 | return string.char(0xF0 + highpart, 361 | 0x80 + midA, 362 | 0x80 + midB, 363 | 0x80 + lowpart) 364 | end 365 | end 366 | 367 | function OBJDEF:onDecodeError(message, text, location, etc) 368 | if text then 369 | if location then 370 | message = string.format("%s at char %d of: %s", message, location, text) 371 | else 372 | message = string.format("%s: %s", message, text) 373 | end 374 | end 375 | 376 | if etc ~= nil then 377 | message = message .. " (" .. OBJDEF:encode(etc) .. ")" 378 | end 379 | 380 | if self.assert then 381 | self.assert(false, message) 382 | else 383 | assert(false, message) 384 | end 385 | end 386 | 387 | OBJDEF.onDecodeOfNilError = OBJDEF.onDecodeError 388 | OBJDEF.onDecodeOfHTMLError = OBJDEF.onDecodeError 389 | 390 | function OBJDEF:onEncodeError(message, etc) 391 | if etc ~= nil then 392 | message = message .. " (" .. OBJDEF:encode(etc) .. ")" 393 | end 394 | 395 | if self.assert then 396 | self.assert(false, message) 397 | else 398 | assert(false, message) 399 | end 400 | end 401 | 402 | local function grok_number(self, text, start, etc) 403 | -- 404 | -- Grab the integer part 405 | -- 406 | local integer_part = text:match('^-?[1-9]%d*', start) 407 | or text:match("^-?0", start) 408 | 409 | if not integer_part then 410 | self:onDecodeError("expected number", text, start, etc) 411 | end 412 | 413 | local i = start + integer_part:len() 414 | 415 | -- 416 | -- Grab an optional decimal part 417 | -- 418 | local decimal_part = text:match('^%.%d+', i) or "" 419 | 420 | i = i + decimal_part:len() 421 | 422 | -- 423 | -- Grab an optional exponential part 424 | -- 425 | local exponent_part = text:match('^[eE][-+]?%d+', i) or "" 426 | 427 | i = i + exponent_part:len() 428 | 429 | local full_number_text = integer_part .. decimal_part .. exponent_part 430 | local as_number = tonumber(full_number_text) 431 | 432 | if not as_number then 433 | self:onDecodeError("bad number", text, start, etc) 434 | end 435 | 436 | return as_number, i 437 | end 438 | 439 | 440 | local function grok_string(self, text, start, etc) 441 | 442 | if text:sub(start,start) ~= '"' then 443 | self:onDecodeError("expected string's opening quote", text, start, etc) 444 | end 445 | 446 | local i = start + 1 -- +1 to bypass the initial quote 447 | local text_len = text:len() 448 | local VALUE = "" 449 | while i <= text_len do 450 | local c = text:sub(i,i) 451 | if c == '"' then 452 | return VALUE, i + 1 453 | end 454 | if c ~= '\\' then 455 | VALUE = VALUE .. c 456 | i = i + 1 457 | elseif text:match('^\\b', i) then 458 | VALUE = VALUE .. "\b" 459 | i = i + 2 460 | elseif text:match('^\\f', i) then 461 | VALUE = VALUE .. "\f" 462 | i = i + 2 463 | elseif text:match('^\\n', i) then 464 | VALUE = VALUE .. "\n" 465 | i = i + 2 466 | elseif text:match('^\\r', i) then 467 | VALUE = VALUE .. "\r" 468 | i = i + 2 469 | elseif text:match('^\\t', i) then 470 | VALUE = VALUE .. "\t" 471 | i = i + 2 472 | else 473 | local hex = text:match('^\\u([0123456789aAbBcCdDeEfF][0123456789aAbBcCdDeEfF][0123456789aAbBcCdDeEfF][0123456789aAbBcCdDeEfF])', i) 474 | if hex then 475 | i = i + 6 -- bypass what we just read 476 | 477 | -- We have a Unicode codepoint. It could be standalone, or if in the proper range and 478 | -- followed by another in a specific range, it'll be a two-code surrogate pair. 479 | local codepoint = tonumber(hex, 16) 480 | if codepoint >= 0xD800 and codepoint <= 0xDBFF then 481 | -- it's a hi surrogate... see whether we have a following low 482 | local lo_surrogate = text:match('^\\u([dD][cdefCDEF][0123456789aAbBcCdDeEfF][0123456789aAbBcCdDeEfF])', i) 483 | if lo_surrogate then 484 | i = i + 6 -- bypass the low surrogate we just read 485 | codepoint = 0x2400 + (codepoint - 0xD800) * 0x400 + tonumber(lo_surrogate, 16) 486 | else 487 | -- not a proper low, so we'll just leave the first codepoint as is and spit it out. 488 | end 489 | end 490 | VALUE = VALUE .. unicode_codepoint_as_utf8(codepoint) 491 | 492 | else 493 | 494 | -- just pass through what's escaped 495 | VALUE = VALUE .. text:match('^\\(.)', i) 496 | i = i + 2 497 | end 498 | end 499 | end 500 | 501 | self:onDecodeError("unclosed string", text, start, etc) 502 | end 503 | 504 | local function skip_whitespace(text, start) 505 | 506 | local _, match_end = text:find("^[ \n\r\t]+", start) -- [http://www.ietf.org/rfc/rfc4627.txt] Section 2 507 | if match_end then 508 | return match_end + 1 509 | else 510 | return start 511 | end 512 | end 513 | 514 | local grok_one -- assigned later 515 | 516 | local function grok_object(self, text, start, etc) 517 | if text:sub(start,start) ~= '{' then 518 | self:onDecodeError("expected '{'", text, start, etc) 519 | end 520 | 521 | local i = skip_whitespace(text, start + 1) -- +1 to skip the '{' 522 | 523 | local VALUE = self.strictTypes and self:newObject { } or { } 524 | 525 | if text:sub(i,i) == '}' then 526 | return VALUE, i + 1 527 | end 528 | local text_len = text:len() 529 | while i <= text_len do 530 | local key, new_i = grok_string(self, text, i, etc) 531 | 532 | i = skip_whitespace(text, new_i) 533 | 534 | if text:sub(i, i) ~= ':' then 535 | self:onDecodeError("expected colon", text, i, etc) 536 | end 537 | 538 | i = skip_whitespace(text, i + 1) 539 | 540 | local new_val, new_i = grok_one(self, text, i) 541 | 542 | VALUE[key] = new_val 543 | 544 | -- 545 | -- Expect now either '}' to end things, or a ',' to allow us to continue. 546 | -- 547 | i = skip_whitespace(text, new_i) 548 | 549 | local c = text:sub(i,i) 550 | 551 | if c == '}' then 552 | return VALUE, i + 1 553 | end 554 | 555 | if text:sub(i, i) ~= ',' then 556 | self:onDecodeError("expected comma or '}'", text, i, etc) 557 | end 558 | 559 | i = skip_whitespace(text, i + 1) 560 | end 561 | 562 | self:onDecodeError("unclosed '{'", text, start, etc) 563 | end 564 | 565 | local function grok_array(self, text, start, etc) 566 | if text:sub(start,start) ~= '[' then 567 | self:onDecodeError("expected '['", text, start, etc) 568 | end 569 | 570 | local i = skip_whitespace(text, start + 1) -- +1 to skip the '[' 571 | local VALUE = self.strictTypes and self:newArray { } or { } 572 | if text:sub(i,i) == ']' then 573 | return VALUE, i + 1 574 | end 575 | 576 | local VALUE_INDEX = 1 577 | 578 | local text_len = text:len() 579 | while i <= text_len do 580 | local val, new_i = grok_one(self, text, i) 581 | 582 | -- can't table.insert(VALUE, val) here because it's a no-op if val is nil 583 | VALUE[VALUE_INDEX] = val 584 | VALUE_INDEX = VALUE_INDEX + 1 585 | 586 | i = skip_whitespace(text, new_i) 587 | 588 | -- 589 | -- Expect now either ']' to end things, or a ',' to allow us to continue. 590 | -- 591 | local c = text:sub(i,i) 592 | if c == ']' then 593 | return VALUE, i + 1 594 | end 595 | if text:sub(i, i) ~= ',' then 596 | self:onDecodeError("expected comma or '['", text, i, etc) 597 | end 598 | i = skip_whitespace(text, i + 1) 599 | end 600 | self:onDecodeError("unclosed '['", text, start, etc) 601 | end 602 | 603 | 604 | grok_one = function(self, text, start, etc) 605 | -- Skip any whitespace 606 | start = skip_whitespace(text, start) 607 | 608 | if start > text:len() then 609 | self:onDecodeError("unexpected end of string", text, nil, etc) 610 | end 611 | 612 | if text:find('^"', start) then 613 | return grok_string(self, text, start, etc) 614 | 615 | elseif text:find('^[-0123456789 ]', start) then 616 | return grok_number(self, text, start, etc) 617 | 618 | elseif text:find('^%{', start) then 619 | return grok_object(self, text, start, etc) 620 | 621 | elseif text:find('^%[', start) then 622 | return grok_array(self, text, start, etc) 623 | 624 | elseif text:find('^true', start) then 625 | return true, start + 4 626 | 627 | elseif text:find('^false', start) then 628 | return false, start + 5 629 | 630 | elseif text:find('^null', start) then 631 | return nil, start + 4 632 | 633 | else 634 | self:onDecodeError("can't parse JSON", text, start, etc) 635 | end 636 | end 637 | 638 | function OBJDEF:decode(text, etc) 639 | if type(self) ~= 'table' or self.__index ~= OBJDEF then 640 | OBJDEF:onDecodeError("JSON:decode must be called in method format", nil, nil, etc) 641 | end 642 | 643 | if text == nil then 644 | self:onDecodeOfNilError(string.format("nil passed to JSON:decode()"), nil, nil, etc) 645 | elseif type(text) ~= 'string' then 646 | self:onDecodeError(string.format("expected string argument to JSON:decode(), got %s", type(text)), nil, nil, etc) 647 | end 648 | 649 | if text:match('^%s*$') then 650 | return nil 651 | end 652 | 653 | if text:match('^%s*<') then 654 | -- Can't be JSON... we'll assume it's HTML 655 | self:onDecodeOfHTMLError(string.format("html passed to JSON:decode()"), text, nil, etc) 656 | end 657 | 658 | -- 659 | -- Ensure that it's not UTF-32 or UTF-16. 660 | -- Those are perfectly valid encodings for JSON (as per RFC 4627 section 3), 661 | -- but this package can't handle them. 662 | -- 663 | if text:sub(1,1):byte() == 0 or (text:len() >= 2 and text:sub(2,2):byte() == 0) then 664 | self:onDecodeError("JSON package groks only UTF-8, sorry", text, nil, etc) 665 | end 666 | 667 | local success, value = pcall(grok_one, self, text, 1, etc) 668 | 669 | if success then 670 | return value 671 | else 672 | -- if JSON:onDecodeError() didn't abort out of the pcall, we'll have received the error message here as "value", so pass it along as an assert. 673 | if self.assert then 674 | self.assert(false, value) 675 | else 676 | assert(false, value) 677 | end 678 | -- and if we're still here, return a nil and throw the error message on as a second arg 679 | return nil, value 680 | end 681 | end 682 | 683 | local function backslash_replacement_function(c) 684 | if c == "\n" then 685 | return "\\n" 686 | elseif c == "\r" then 687 | return "\\r" 688 | elseif c == "\t" then 689 | return "\\t" 690 | elseif c == "\b" then 691 | return "\\b" 692 | elseif c == "\f" then 693 | return "\\f" 694 | elseif c == '"' then 695 | return '\\"' 696 | elseif c == '\\' then 697 | return '\\\\' 698 | else 699 | return string.format("\\u%04x", c:byte()) 700 | end 701 | end 702 | 703 | local chars_to_be_escaped_in_JSON_string 704 | = '[' 705 | .. '"' -- class sub-pattern to match a double quote 706 | .. '%\\' -- class sub-pattern to match a backslash 707 | .. '%z' -- class sub-pattern to match a null 708 | .. '\001' .. '-' .. '\031' -- class sub-pattern to match control characters 709 | .. ']' 710 | 711 | local function json_string_literal(value) 712 | local newval = value:gsub(chars_to_be_escaped_in_JSON_string, backslash_replacement_function) 713 | return '"' .. newval .. '"' 714 | end 715 | 716 | local function object_or_array(self, T, etc) 717 | -- 718 | -- We need to inspect all the keys... if there are any strings, we'll convert to a JSON 719 | -- object. If there are only numbers, it's a JSON array. 720 | -- 721 | -- If we'll be converting to a JSON object, we'll want to sort the keys so that the 722 | -- end result is deterministic. 723 | -- 724 | local string_keys = { } 725 | local number_keys = { } 726 | local number_keys_must_be_strings = false 727 | local maximum_number_key 728 | 729 | for key in pairs(T) do 730 | if type(key) == 'string' then 731 | table.insert(string_keys, key) 732 | elseif type(key) == 'number' then 733 | table.insert(number_keys, key) 734 | if key <= 0 or key >= math.huge then 735 | number_keys_must_be_strings = true 736 | elseif not maximum_number_key or key > maximum_number_key then 737 | maximum_number_key = key 738 | end 739 | else 740 | self:onEncodeError("can't encode table with a key of type " .. type(key), etc) 741 | end 742 | end 743 | 744 | if #string_keys == 0 and not number_keys_must_be_strings then 745 | -- 746 | -- An empty table, or a numeric-only array 747 | -- 748 | if #number_keys > 0 then 749 | return nil, maximum_number_key -- an array 750 | elseif tostring(T) == "JSON array" then 751 | return nil 752 | elseif tostring(T) == "JSON object" then 753 | return { } 754 | else 755 | -- have to guess, so we'll pick array, since empty arrays are likely more common than empty objects 756 | return nil 757 | end 758 | end 759 | 760 | table.sort(string_keys) 761 | 762 | local map 763 | if #number_keys > 0 then 764 | -- 765 | -- If we're here then we have either mixed string/number keys, or numbers inappropriate for a JSON array 766 | -- It's not ideal, but we'll turn the numbers into strings so that we can at least create a JSON object. 767 | -- 768 | 769 | if self.noKeyConversion then 770 | self:onEncodeError("a table with both numeric and string keys could be an object or array; aborting", etc) 771 | end 772 | 773 | -- 774 | -- Have to make a shallow copy of the source table so we can remap the numeric keys to be strings 775 | -- 776 | map = { } 777 | for key, val in pairs(T) do 778 | map[key] = val 779 | end 780 | 781 | table.sort(number_keys) 782 | 783 | -- 784 | -- Throw numeric keys in there as strings 785 | -- 786 | for _, number_key in ipairs(number_keys) do 787 | local string_key = tostring(number_key) 788 | if map[string_key] == nil then 789 | table.insert(string_keys , string_key) 790 | map[string_key] = T[number_key] 791 | else 792 | self:onEncodeError("conflict converting table with mixed-type keys into a JSON object: key " .. number_key .. " exists both as a string and a number.", etc) 793 | end 794 | end 795 | end 796 | 797 | return string_keys, nil, map 798 | end 799 | 800 | -- 801 | -- Encode 802 | -- 803 | -- 'options' is nil, or a table with possible keys: 804 | -- pretty -- if true, return a pretty-printed version 805 | -- indent -- a string (usually of spaces) used to indent each nested level 806 | -- align_keys -- if true, align all the keys when formatting a table 807 | -- 808 | local encode_value -- must predeclare because it calls itself 809 | function encode_value(self, value, parents, etc, options, indent) 810 | 811 | if value == nil then 812 | return 'null' 813 | 814 | elseif type(value) == 'string' then 815 | return json_string_literal(value) 816 | 817 | elseif type(value) == 'number' then 818 | if value ~= value then 819 | -- 820 | -- NaN (Not a Number). 821 | -- JSON has no NaN, so we have to fudge the best we can. This should really be a package option. 822 | -- 823 | return "null" 824 | elseif value >= math.huge then 825 | -- 826 | -- Positive infinity. JSON has no INF, so we have to fudge the best we can. This should 827 | -- really be a package option. Note: at least with some implementations, positive infinity 828 | -- is both ">= math.huge" and "<= -math.huge", which makes no sense but that's how it is. 829 | -- Negative infinity is properly "<= -math.huge". So, we must be sure to check the ">=" 830 | -- case first. 831 | -- 832 | return "1e+9999" 833 | elseif value <= -math.huge then 834 | -- 835 | -- Negative infinity. 836 | -- JSON has no INF, so we have to fudge the best we can. This should really be a package option. 837 | -- 838 | return "-1e+9999" 839 | else 840 | return tostring(value) 841 | end 842 | 843 | elseif type(value) == 'boolean' then 844 | return tostring(value) 845 | 846 | elseif type(value) ~= 'table' then 847 | self:onEncodeError("can't convert " .. type(value) .. " to JSON", etc) 848 | 849 | else 850 | -- 851 | -- A table to be converted to either a JSON object or array. 852 | -- 853 | local T = value 854 | 855 | if type(options) ~= 'table' then 856 | options = {} 857 | end 858 | if type(indent) ~= 'string' then 859 | indent = "" 860 | end 861 | 862 | if parents[T] then 863 | self:onEncodeError("table " .. tostring(T) .. " is a child of itself", etc) 864 | else 865 | parents[T] = true 866 | end 867 | 868 | local result_value 869 | 870 | local object_keys, maximum_number_key, map = object_or_array(self, T, etc) 871 | if maximum_number_key then 872 | -- 873 | -- An array... 874 | -- 875 | local ITEMS = { } 876 | for i = 1, maximum_number_key do 877 | table.insert(ITEMS, encode_value(self, T[i], parents, etc, options, indent)) 878 | end 879 | 880 | if options.pretty then 881 | result_value = "[ " .. table.concat(ITEMS, ", ") .. " ]" 882 | else 883 | result_value = "[" .. table.concat(ITEMS, ",") .. "]" 884 | end 885 | 886 | elseif object_keys then 887 | -- 888 | -- An object 889 | -- 890 | local TT = map or T 891 | 892 | if options.pretty then 893 | 894 | local KEYS = { } 895 | local max_key_length = 0 896 | for _, key in ipairs(object_keys) do 897 | local encoded = encode_value(self, tostring(key), parents, etc, options, indent) 898 | if options.align_keys then 899 | max_key_length = math.max(max_key_length, #encoded) 900 | end 901 | table.insert(KEYS, encoded) 902 | end 903 | local key_indent = indent .. tostring(options.indent or "") 904 | local subtable_indent = key_indent .. string.rep(" ", max_key_length) .. (options.align_keys and " " or "") 905 | local FORMAT = "%s%" .. string.format("%d", max_key_length) .. "s: %s" 906 | 907 | local COMBINED_PARTS = { } 908 | for i, key in ipairs(object_keys) do 909 | local encoded_val = encode_value(self, TT[key], parents, etc, options, subtable_indent) 910 | table.insert(COMBINED_PARTS, string.format(FORMAT, key_indent, KEYS[i], encoded_val)) 911 | end 912 | result_value = "{\n" .. table.concat(COMBINED_PARTS, ",\n") .. "\n" .. indent .. "}" 913 | 914 | else 915 | 916 | local PARTS = { } 917 | for _, key in ipairs(object_keys) do 918 | local encoded_val = encode_value(self, TT[key], parents, etc, options, indent) 919 | local encoded_key = encode_value(self, tostring(key), parents, etc, options, indent) 920 | table.insert(PARTS, string.format("%s:%s", encoded_key, encoded_val)) 921 | end 922 | result_value = "{" .. table.concat(PARTS, ",") .. "}" 923 | 924 | end 925 | else 926 | -- 927 | -- An empty array/object... we'll treat it as an array, though it should really be an option 928 | -- 929 | result_value = "[]" 930 | end 931 | 932 | parents[T] = false 933 | return result_value 934 | end 935 | end 936 | 937 | 938 | function OBJDEF:encode(value, etc, options) 939 | if type(self) ~= 'table' or self.__index ~= OBJDEF then 940 | OBJDEF:onEncodeError("JSON:encode must be called in method format", etc) 941 | end 942 | return encode_value(self, value, {}, etc, options or nil) 943 | end 944 | 945 | function OBJDEF:encode_pretty(value, etc, options) 946 | if type(self) ~= 'table' or self.__index ~= OBJDEF then 947 | OBJDEF:onEncodeError("JSON:encode_pretty must be called in method format", etc) 948 | end 949 | return encode_value(self, value, {}, etc, options or default_pretty_options) 950 | end 951 | 952 | function OBJDEF.__tostring() 953 | return "JSON encode/decode package" 954 | end 955 | 956 | OBJDEF.__index = OBJDEF 957 | 958 | function OBJDEF:new(args) 959 | local new = { } 960 | 961 | if args then 962 | for key, val in pairs(args) do 963 | new[key] = val 964 | end 965 | end 966 | 967 | return setmetatable(new, OBJDEF) 968 | end 969 | 970 | return OBJDEF:new() 971 | 972 | -- 973 | -- Version history: 974 | -- 975 | -- 20141223.14 The encode_pretty() routine produced fine results for small datasets, but isn't really 976 | -- appropriate for anything large, so with help from Alex Aulbach I've made the encode routines 977 | -- more flexible, and changed the default encode_pretty() to be more generally useful. 978 | -- 979 | -- Added a third 'options' argument to the encode() and encode_pretty() routines, to control 980 | -- how the encoding takes place. 981 | -- 982 | -- Updated docs to add assert() call to the loadfile() line, just as good practice so that 983 | -- if there is a problem loading JSON.lua, the appropriate error message will percolate up. 984 | -- 985 | -- 20140920.13 Put back (in a way that doesn't cause warnings about unused variables) the author string, 986 | -- so that the source of the package, and its version number, are visible in compiled copies. 987 | -- 988 | -- 20140911.12 Minor lua cleanup. 989 | -- Fixed internal reference to 'JSON.noKeyConversion' to reference 'self' instead of 'JSON'. 990 | -- (Thanks to SmugMug's David Parry for these.) 991 | -- 992 | -- 20140418.11 JSON nulls embedded within an array were being ignored, such that 993 | -- ["1",null,null,null,null,null,"seven"], 994 | -- would return 995 | -- {1,"seven"} 996 | -- It's now fixed to properly return 997 | -- {1, nil, nil, nil, nil, nil, "seven"} 998 | -- Thanks to "haddock" for catching the error. 999 | -- 1000 | -- 20140116.10 The user's JSON.assert() wasn't always being used. Thanks to "blue" for the heads up. 1001 | -- 1002 | -- 20131118.9 Update for Lua 5.3... it seems that tostring(2/1) produces "2.0" instead of "2", 1003 | -- and this caused some problems. 1004 | -- 1005 | -- 20131031.8 Unified the code for encode() and encode_pretty(); they had been stupidly separate, 1006 | -- and had of course diverged (encode_pretty didn't get the fixes that encode got, so 1007 | -- sometimes produced incorrect results; thanks to Mattie for the heads up). 1008 | -- 1009 | -- Handle encoding tables with non-positive numeric keys (unlikely, but possible). 1010 | -- 1011 | -- If a table has both numeric and string keys, or its numeric keys are inappropriate 1012 | -- (such as being non-positive or infinite), the numeric keys are turned into 1013 | -- string keys appropriate for a JSON object. So, as before, 1014 | -- JSON:encode({ "one", "two", "three" }) 1015 | -- produces the array 1016 | -- ["one","two","three"] 1017 | -- but now something with mixed key types like 1018 | -- JSON:encode({ "one", "two", "three", SOMESTRING = "some string" })) 1019 | -- instead of throwing an error produces an object: 1020 | -- {"1":"one","2":"two","3":"three","SOMESTRING":"some string"} 1021 | -- 1022 | -- To maintain the prior throw-an-error semantics, set 1023 | -- JSON.noKeyConversion = true 1024 | -- 1025 | -- 20131004.7 Release under a Creative Commons CC-BY license, which I should have done from day one, sorry. 1026 | -- 1027 | -- 20130120.6 Comment update: added a link to the specific page on my blog where this code can 1028 | -- be found, so that folks who come across the code outside of my blog can find updates 1029 | -- more easily. 1030 | -- 1031 | -- 20111207.5 Added support for the 'etc' arguments, for better error reporting. 1032 | -- 1033 | -- 20110731.4 More feedback from David Kolf on how to make the tests for Nan/Infinity system independent. 1034 | -- 1035 | -- 20110730.3 Incorporated feedback from David Kolf at http://lua-users.org/wiki/JsonModules: 1036 | -- 1037 | -- * When encoding lua for JSON, Sparse numeric arrays are now handled by 1038 | -- spitting out full arrays, such that 1039 | -- JSON:encode({"one", "two", [10] = "ten"}) 1040 | -- returns 1041 | -- ["one","two",null,null,null,null,null,null,null,"ten"] 1042 | -- 1043 | -- In 20100810.2 and earlier, only up to the first non-null value would have been retained. 1044 | -- 1045 | -- * When encoding lua for JSON, numeric value NaN gets spit out as null, and infinity as "1+e9999". 1046 | -- Version 20100810.2 and earlier created invalid JSON in both cases. 1047 | -- 1048 | -- * Unicode surrogate pairs are now detected when decoding JSON. 1049 | -- 1050 | -- 20100810.2 added some checking to ensure that an invalid Unicode character couldn't leak in to the UTF-8 encoding 1051 | -- 1052 | -- 20100731.1 initial public release 1053 | -- 1054 | -------------------------------------------------------------------------------- /lib/Valider.lua: -------------------------------------------------------------------------------- 1 | ----------------------------------------------------------------------------- 2 | -- Valider class v1.1.1 3 | -- Author: aimingoo@wandoujia.com 4 | -- Copyright (c) 2015.06 5 | -- 6 | -- The promise module from NGX_4C architecture 7 | -- 1) N4C is programming framework. 8 | -- 2) N4C = a Controllable & Computable Communication Cluster architectur. 9 | -- 10 | -- Usage: 11 | -- checker = require('Valider'):new(opt) 12 | -- isInvalid = checker:invalid(key, isContinuous) 13 | -- 14 | -- History: 15 | -- 2015.10.29 release v1.1.1, update testcases 16 | -- 2015.08.12 release v1.1, rewirte invalid() without queue, publish on github 17 | -- 2015.08.11 release v1.0.1, full testcases 18 | -- 2015.03 release v1.0.0 19 | ----------------------------------------------------------------------------- 20 | 21 | local Valider = { 22 | maxContinuousInterval = 3, 23 | maxContinuous = 100, -- continuous invalid X times (default 100), or 24 | maxTimes = 30, -- invalid X times in Y seconds history (default is 30 times in 30s) 25 | maxSeconds = 30 -- (max history seconds, default is 30s) 26 | } 27 | 28 | function Valider:invalid(key, isContinuous) -- key is '.' or anythings 29 | if not rawget(self, key) then 30 | rawset(self, key, { 1, 0, os.time(), 0 }) -- { count, seconds, lastTime, continuous } 31 | else 32 | local now, s = os.time(), rawget(self, key) 33 | local count, seconds, lastTime, continuous = s[1], s[2], s[3], s[4] -- or unpack(s) 34 | s[3], s[4] = now, (isContinuous or (lastTime+self.maxContinuousInterval) > now) and continuous + 1 or 0 35 | 36 | local gapTime, maxSeconds = now - lastTime, self.maxSeconds 37 | if gapTime > maxSeconds then -- reset 38 | s[1], s[2] = 1, 0 39 | return false 40 | end 41 | 42 | local saveTime = seconds + gapTime 43 | local dropTime = saveTime - maxSeconds 44 | if dropTime > 0 then 45 | local avgTime = seconds/count 46 | local dropN = math.ceil(dropTime/avgTime) 47 | s[1], s[2] = count - dropN + 1, math.ceil(seconds - dropN*avgTime) 48 | else 49 | s[1], s[2] = count + 1, saveTime 50 | end 51 | 52 | return ((s[4] >= self.maxContinuous) or 53 | (s[1] >= self.maxTimes)) 54 | end 55 | end 56 | 57 | function Valider:new(opt) -- options or nil 58 | return setmetatable({}, { -- instance is empty on init 59 | __index = opt and setmetatable(opt, {__index=self}) or self -- access options 60 | }) 61 | end 62 | 63 | return Valider -------------------------------------------------------------------------------- /lib/ngx_tasks.lua: -------------------------------------------------------------------------------- 1 | ----------------------------------------------------------------------------- 2 | -- Tasks management module for lua in nginx 3 | -- Author: chaihaotian@wandoujia.com, aimingoo@wandoujia.com 4 | -- Copyright (c) 2014.12 5 | -- 6 | -- Usage: 7 | -- tasks = require('ngx_tasks') 8 | -- tasks.dict = 'your_shared_dict_config_in_' 9 | -- tasks:push(rule) 10 | -- ... 11 | -- tasks:run() 12 | ----------------------------------------------------------------------------- 13 | 14 | local function hasPreempt(shared, version, preemptionName) 15 | local preempted, memoryVersion = true, shared:get(preemptionName) 16 | 17 | if memoryVersion == nil then 18 | if shared:add(preemptionName, version, 86400) then 19 | return preempted 20 | end 21 | memoryVersion = shared:get(preemptionName) or version 22 | end 23 | 24 | if memoryVersion < version then 25 | if shared:add(preemptionName..'_'..memoryVersion, version, 3600) then 26 | return shared:set(preemptionName, version, 86400) 27 | else 28 | local newVersion = shared:get(preemptionName..'_'..memoryVersion) or version 29 | if newVersion < version then 30 | if shared:set(preemptionName, version, 86400) then 31 | return preempted 32 | end 33 | ngx.log(ngx.ALERT, 'Error in hasPreempt(), version/newVersion/memoryVersion: ' .. 34 | table.concat({version, newVersion, memoryVersion}, '/')) 35 | end 36 | end 37 | end 38 | 39 | return not preempted 40 | end 41 | 42 | local Tasks = { 43 | dict = 'PreemptionTasks', 44 | 45 | run = function(self) 46 | if type(self.dict) == 'string' then 47 | self.dict = assert(ngx.shared[self.dict], 'access shared dictionary: '..self.dict) 48 | end 49 | local now = os.time() 50 | for _, task in ipairs(self) do 51 | if not task.last_execute or (task.last_execute + task.interval < now) then 52 | task.last_execute = now 53 | if ((task.typ == 'normal') or 54 | (task.typ == 'preemption' and hasPreempt(self.dict, self:keys(task, now)))) then 55 | if not task.filter or task:filter() then 56 | task:callback() 57 | end 58 | end 59 | end 60 | end 61 | end, 62 | 63 | push = function(self, rule) 64 | table.insert(self, rule) 65 | end, 66 | 67 | remove = function(self, id) 68 | for i = #self, 1, -1 do 69 | if self[i].identifier == id then 70 | table.remove(self, i) 71 | end 72 | end 73 | end, 74 | 75 | keys = function(self, task, now) 76 | local d = os.date('*t', now) 77 | local second = now - os.time{year=d.year, month=d.month, day=d.day, hour=0} 78 | return math.floor(second/task.interval), task.identifier .. 'Preemption.' .. os.date('%Y%m%d', now) 79 | end 80 | } 81 | 82 | return Tasks -------------------------------------------------------------------------------- /lib/posix.lua: -------------------------------------------------------------------------------- 1 | -- a minimum posix system module 2 | -- 3 | -- from: https://github.com/nyfair/freeimagerip/blob/master/wrapper/luajit-ffi/lua/fsposix.lua 4 | -- https://github.com/justincormack/ljsyscall/blob/master/syscall/bsd/ffi.lua 5 | -- fixed: aimingoo@wandoujia.com 6 | -- 7 | -- The file is in public domain 8 | -- nyfair (nyfair2012@gmail.com) 9 | 10 | local function mktype(tp, x) if ffi.istype(tp, x) then return x else return tp(x) end end 11 | local function istype(tp, x) if ffi.istype(tp, x) then return x else return false end end 12 | 13 | local ffi = require 'ffi' 14 | local def = [[ 15 | int mkdir(const char*, mode_t mode); 16 | int rmdir(const char *pathname); 17 | int chmod(const char *path, mode_t mode); 18 | pid_t getppid(pid_t pid); 19 | pid_t getpgid(pid_t pid); 20 | ]] 21 | 22 | local abi = ffi.os:lower() -- + ffi.abi("32bit") 23 | if abi == 'osx' or abi == 'freebsd' then 24 | def = 'typedef uint16_t mode_t;\n' .. def; 25 | else 26 | def = 'typedef uint32_t mode_t;\n' .. def; 27 | end 28 | def = 'typedef int32_t pid_t;\n' .. def; 29 | 30 | ffi.cdef(def) 31 | 32 | local posix = {} 33 | 34 | function posix.ls(pattern) 35 | local files = {} 36 | local output = assert(io.popen('ls '..pattern:gsub(' ', '\\ '))) 37 | for line in output:lines() do 38 | table.insert(files, line) 39 | end 40 | return files 41 | end 42 | 43 | function posix.cp(src, dest) 44 | -- @see: http://stackoverflow.com/questions/16367524/copy-csv-file-to-new-file-in-lua 45 | -- os.execute("cp '" .. src .. "' '" .. dst .. "'"') 46 | local infile, outfile = io.open(src, "r"), io.open(dest, "w") 47 | outfile:write(infile:read("*a")) 48 | infile:close() 49 | outfile:close() 50 | end 51 | 52 | -- local octal = function (s) return tonumber(s, 8) end 53 | -- local mode_t = ffi.typeof('mode_t') 54 | -- local mode = octal('0777') 55 | function posix.md(dst, mod) 56 | return ffi.C.mkdir(dst, mod or 493) --0755 57 | end 58 | 59 | function posix.chmod(dst, mod) 60 | return ffi.C.chmod(dst, mod or 493) --0755 61 | end 62 | 63 | function posix.rd(dst) 64 | -- os.execute('rm -rf '..normalize(dst:gsub(' ', '\\ '))) 65 | return ffi.C.mkdir(dst) 66 | end 67 | 68 | -- check file exist 69 | function posix.exist(name) 70 | local f = io.open(name, "r") 71 | return f ~= nil and (f:close() or true) or false 72 | end 73 | 74 | -- read small file to string, blocking 75 | function posix.all(name) 76 | local f = io.open(name, "rb") 77 | local content = f:read('*a') 78 | f:close() 79 | return content 80 | end 81 | 82 | -- get all lines from a file 83 | -- 1) returns an empty list/table if the file does not exist 84 | function posix.lines(name) 85 | local lines = {} 86 | for line in io.lines(name) do 87 | table.insert(lines, line) 88 | end 89 | return lines 90 | end 91 | 92 | -- save all lines to a file, is table/string, or any value as string. 93 | -- 1) no returns 94 | function posix.save(name, lines) 95 | local file = io.open(name, 'w+') 96 | file:write(type(lines)=='table' and table.concat(lines, '') or tostring(lines)) 97 | file:close() 98 | end 99 | 100 | -- http://stackoverflow.com/questions/2833675/using-lua-check-if-file-is-a-directory 101 | -- 1) WINDOWS unsupport 102 | local function _isdir(path) 103 | local f = io.open(path, "r") 104 | if (f ~= nil) then 105 | local ok, err, code = f:read(1) 106 | f:close() 107 | return code == 21 -- exist, cant read as file (is directory, maybe) 108 | end 109 | return false -- no exist 110 | end 111 | 112 | local function _mkdir(path) 113 | -- if WINDOWS and path:find('^%a:/*$') then return true end 114 | if not _isdir(path) then 115 | local parent = path:match('(.+)/[^/]+$') 116 | if parent and not _mkdir(parent) then 117 | return nil, 'cannot create '..parent 118 | else 119 | return (posix.md(path) == 0) 120 | end 121 | else 122 | return true 123 | end 124 | end 125 | 126 | -- make diretory with fullpath 127 | posix.mkdir = _mkdir; 128 | -- posix.mkdir = function(path) 129 | -- -- if WINDOWS then path = path:gsub('\\','/') end 130 | -- return _mkdir(path) 131 | -- end 132 | 133 | function posix.ppid(pid) 134 | return ffi.C.getppid(pid) 135 | end 136 | 137 | function posix.pgid(pid) 138 | return ffi.C.getpgid(pid) 139 | end 140 | 141 | return posix -------------------------------------------------------------------------------- /module/heartbeat.lua: -------------------------------------------------------------------------------- 1 | local tasks_management = require("lib.ngx_tasks") 2 | 3 | local function apply(invoke) 4 | -- client heartbeats 5 | -- /channel_name/invoke?heartbeat 6 | invoke.heartbeat = function(route, channel, arg) 7 | local cluster = route.cluster 8 | if cluster.worker_initiated then 9 | route.cc('/_/invoke', { direction='super', args={reportHubPort=cluster.master.port} }) 10 | if cluster.report_clients then 11 | route.cc('/_/invoke', { direction='workers', args={heartbeat2=true} }) 12 | end 13 | end 14 | end 15 | 16 | -- client heartbeats, when report_clients is true 17 | -- /channel_name/invoke?heartbeat2 18 | invoke.heartbeat2 = function(route, channel, arg) 19 | route.cc('/_/invoke', { direction='super', args={reportClient=route.cluster.worker.port} }) 20 | end 21 | 22 | return invoke 23 | end 24 | 25 | return { 26 | apply = function(route) 27 | -- reset tasks management module 28 | local Tasks, meta = tasks_management, getmetatable(route.tasks) 29 | if not meta or meta.__index ~= Tasks then 30 | setmetatable(route.tasks, { __index = Tasks }) 31 | route.tasks.dict = route.shared 32 | end 33 | -- task of heartbeat 34 | route.tasks:push({ 35 | name = 'heartbeats of clients', 36 | identifier = 'client_heartbeats', 37 | interval = 60, 38 | typ = 'preemption', -- preemption in multi-workers, only once 39 | callback = function(self) 40 | route.self('/_/invoke', { args={heartbeat=true} }) 41 | end 42 | }) 43 | return apply(route.invoke) 44 | end 45 | } 46 | -------------------------------------------------------------------------------- /module/invalid.lua: -------------------------------------------------------------------------------- 1 | local Valider = require('lib.Valider') 2 | 3 | local function apply(invoke) 4 | local checker = Valider:new({ 5 | maxContinuous = 5, -- continuous invalid 5 times, or 6 | maxTimes = 10, -- invalid 10 times in 5 seconds 7 | maxSeconds = 5, 8 | }) 9 | 10 | -- the worker is invalid, call from local with ngx_cc to-'master' always 11 | -- /channel_name/invoke?invalidWorker=port 12 | invoke.invalidWorker = function(route, channel, arg) 13 | local port, t = arg.invalidWorker, arg.t or false 14 | if checker:invalid(channel..'.localhost:'..port, t) then 15 | local shared, key_registed_workers = route.shared, 'ngx_cc.'..channel..'.registed.workers' 16 | local workers, ports, registedWorkers = {}, {}, shared:get(key_registed_workers) 17 | for worker in string.gmatch(registedWorkers, "([^,]+),?") do 18 | local p, pid = string.match(worker, '^(%d+)/(%d+)') 19 | if p ~= port then 20 | table.insert(ports, p) 21 | table.insert(workers, worker) 22 | end 23 | end 24 | shared:set(key_registed_workers, table.concat(workers, ',')) 25 | ngx.ctx.invalidWorker = arg.invalidWorker -- check by n4cDistrbutionTaskNode.lua module in n4c 26 | end 27 | end 28 | 29 | -- the client is invalid, call from local with ngx_cc to-'master' always 30 | -- /channel_name/invoke?invalidClient=ip:port 31 | invoke.invalidClient = function(route, channel, arg) 32 | local client, t = arg.invalidClient, arg.t or false 33 | if checker:invalid(channel..'.'..client, t) then 34 | local shared, key_registed_clients = route.shared, 'ngx_cc.'..channel..'.registed.clients' 35 | local clients, registedClients = {}, shared:get(key_registed_clients) 36 | local c, p = string.match(client, '([^:,]+)([^,]*)') 37 | for client, port in string.gmatch(registedClients, '([^:,]+)([^,]*),?') do 38 | if c ~= client then 39 | table.insert(clients, client .. port) 40 | end 41 | end 42 | shared:set(key_registed_clients, table.concat(clients, ',')) 43 | ngx.ctx.invalidClient = arg.invalidClient -- check by n4cDistrbutionTaskNode.lua module in n4c 44 | end 45 | end 46 | 47 | return invoke 48 | end 49 | 50 | return { 51 | apply = function(route) 52 | return apply(route.invoke) 53 | end 54 | } -------------------------------------------------------------------------------- /module/invoke.lua: -------------------------------------------------------------------------------- 1 | local JSON = require('cjson') 2 | 3 | local function apply(invoke) 4 | local function HOST(node) 5 | return node.host .. ':' .. (node.port or '80') 6 | end 7 | 8 | -- get services status (with/without clients deep discovery) 9 | -- /channel_name/invoke?getServiceStat&selfOnly=1 10 | invoke.getServiceStat = function(route, channel, arg) 11 | local clients 12 | if not arg.selfOnly then 13 | local r_status, r_resps = route.cc('/_/invoke', route.optionAgain({ direction = 'clients' })) 14 | clients = {} 15 | for _, resp in ipairs(r_resps) do 16 | if (resp.status == ngx.HTTP_OK) then 17 | local ok, result = pcall(JSON.decode, resp.body) 18 | clients[resp.client] = ok and result or 19 | { super = '-', ports = '-', routePort = '-', clients = {} } 20 | end 21 | if clients[resp.client] then 22 | clients[resp.client].service = resp.client 23 | end 24 | end 25 | end 26 | 27 | -- NOTE: cant access 'channel_resources[]', so direct read shared dictionary 28 | -- @see: reosure getter for native ngx_cc in ngx_cc.lua 29 | local key_registed_workers = 'ngx_cc.'..channel..'.registed.workers' 30 | local shared, cluster = route.shared, route.cluster 31 | local get_ports = function(registedWorkers) 32 | local ports = {} 33 | for port in string.gmatch(registedWorkers, '(%d+)[^,]*,?') do table.insert(ports, port) end 34 | return table.concat(ports, ',') 35 | end 36 | ngx.say(JSON.encode({ 37 | super = (not route.isRoot()) and HOST(cluster.super) or nil, 38 | service = HOST(cluster.master), 39 | ports = get_ports(shared:get(key_registed_workers)), 40 | routePort = cluster.router.port, 41 | clients = clients 42 | })) 43 | ngx.exit(ngx.HTTP_OK) 44 | end 45 | 46 | -- transfer channel's super to new server, invoke and process by per-workers 47 | -- /channel_name/invoke?transferServer=ip:port 48 | -- 1) for per-workers, change instance's cluster.super only 49 | invoke.transferServer = function(route, channel, arg) 50 | if route.isInvokeAtPer() then 51 | route.transfer(arg.transferServer) 52 | end 53 | ngx.say('Okay.') 54 | ngx.exit(ngx.HTTP_OK) 55 | end 56 | 57 | return invoke 58 | end 59 | 60 | return { 61 | apply = function(route) 62 | return apply(route.invoke) 63 | end 64 | } -------------------------------------------------------------------------------- /module/ngx_cc_core.lua: -------------------------------------------------------------------------------- 1 | ----------------------------------------------------------------------------- 2 | -- NGX_CC v2.0.0 3 | -- Author: aimingoo@wandoujia.com 4 | -- Copyright (c) 2015.02-2015.08 5 | -- Descition: a framework of Nginx Communication Cluster. reliable 6 | -- dispatch/translation messages in nginx nodes or processes. 7 | -- 8 | -- core method or actions, need module/procfs_process.lua module 9 | ----------------------------------------------------------------------------- 10 | 11 | -- cast to master with real remote_client address 12 | local function _isInvokeAtMasterAA(route, arg) 13 | if route.cluster.worker.port ~= route.cluster.router.port then 14 | return false, route.cc(ngx.var.uri, route.optionAgain({ 15 | direction = 'master', 16 | args = { clientAddr = ngx.var.remote_addr } -- Attach_Address 17 | })); 18 | else 19 | if arg.clientAddr then 20 | if ngx.var._faked_by_cc_ then 21 | ngx.var.remote_addr = arg.clientAddr 22 | else 23 | ngx.var = setmetatable({remote_addr = arg.clientAddr, _faked_by_cc_ = true }, { __index = ngx.var }) -- rewrite 24 | end 25 | arg.clientAddr = nil 26 | end 27 | return true 28 | end 29 | end 30 | 31 | local function success_initialized(route, channel) 32 | local cluster = route.cluster 33 | local worker, master, router = cluster.worker, cluster.master, cluster.router 34 | ngx.log(ngx.ALERT, worker.pid .. ' listen at port ' .. worker.port .. ', [', 35 | channel .. '] router.port: ' .. router.port .. ', ', 36 | 'and master pid/port: ' .. master.pid .. '/' .. master.port .. '.') 37 | end 38 | 39 | -- register current worker on master 40 | -- /channel_name/invoke?registerWorker 41 | local function registerWorker(route, channel, arg) 42 | local shared, cluster = route.shared, route.cluster 43 | local key_registed_workers = 'ngx_cc.'..channel..'.registed.workers' 44 | local registedWorkers = shared:get(key_registed_workers) 45 | 46 | local worker_port = arg.registerWorker or '80' 47 | local workers = { worker_port .. '/' .. arg.pid } 48 | if not registedWorkers then 49 | shared:add(key_registed_workers, workers[1]) 50 | else -- dedup and save, check valid for per worker process 51 | local ps, master_pid = require('lib.posix'), tonumber(cluster.master.pid) 52 | local function is_valid_workerprocess(pid) 53 | return ps.pgid(tonumber(pid)) == master_pid -- pgid is '-1' or pid of master process 54 | end 55 | for worker in string.gmatch(registedWorkers, "([^,]+),?") do 56 | if (worker ~= workers[1]) then 57 | local pid = string.match(worker, '%d+$') 58 | if is_valid_workerprocess(pid) then 59 | table.insert(workers, worker) 60 | end 61 | end 62 | end 63 | shared:set(key_registed_workers, table.concat(workers, ',')) 64 | end 65 | end 66 | 67 | -- report client workport to super 68 | -- /channel_name/invoke?reportClient=xxxx 69 | local function reportClient(route, channel, arg) 70 | if arg.reportClient ~= '' and _isInvokeAtMasterAA(route, arg) then 71 | local key_registed_clients = 'ngx_cc.'..channel..'.registed.clients' 72 | local shared, key = route.shared, key_registed_clients .. '.' .. ngx.var.remote_addr 73 | local clientPorts = shared:get(key) 74 | 75 | local ports = { arg.reportClient } 76 | if not clientPorts then 77 | shared:add(key, ports[1]) 78 | else -- dedup and save 79 | for port in string.gmatch(clientPorts, "(%d+),?") do 80 | if port ~= ports[1] then 81 | table.insert(ports, port) 82 | end 83 | end 84 | shared:set(key, table.concat(ports, ',')) 85 | end 86 | end 87 | end 88 | 89 | -- report client hubport to super 90 | -- /channel_name/invoke?reportHubPort=xxx 91 | local function reportHubPort(route, channel, arg) 92 | if _isInvokeAtMasterAA(route, arg) then 93 | local shared = route.shared 94 | local key_registed_clients = 'ngx_cc.'..channel..'.registed.clients' 95 | local masterHost, masterPort, registedClients = ngx.var.remote_addr, arg.reportHubPort, shared:get(key_registed_clients) 96 | if masterPort ~= '' then 97 | masterPort = (masterPort == '80' and '' or ':'..masterPort) 98 | end 99 | 100 | local clients = { masterHost .. masterPort } 101 | if not registedClients then 102 | shared:add(key_registed_clients, clients[1]) 103 | else -- dedup and save 104 | for client, port in string.gmatch(registedClients, '([^:,]+)([^,]*),?') do 105 | if client ~= masterHost then 106 | table.insert(clients, client .. port) 107 | end 108 | end 109 | shared:set(key_registed_clients, table.concat(clients, ',')) 110 | end 111 | end 112 | end 113 | 114 | -- manual initializer, need per_worker with get_per_port patcher 115 | local manual_initializer = function(route, channel, options) 116 | local cluster = route.cluster 117 | cluster.master.port = options.port or '80' 118 | cluster.worker.port = tostring(ngx.worker.port()) 119 | 120 | -- try register RouterPort 121 | local shared, key = route.shared, 'ngx_cc.'..channel..'.RouterPort' 122 | local port = assert(shared:get(key), 'cant find RouterPort from dictionary.') 123 | cluster.router.port, cluster.router.pid = string.match(port, '^(%d+)/(%d+)') 124 | cluster.worker_initiated = port ~= nil 125 | success_initialized(route, channel) 126 | end 127 | 128 | -- (default) automatic initializer, need bash and lsof 129 | local automatic_initializer = function(route, channel, options) 130 | -- send request to self, dependencies: 131 | -- /bin/bash ## support build-in: read, exec, and Bash socket programming. 132 | -- /usr/sbin/lsof 133 | -- see also: 134 | -- http://www.linuxjournal.com/content/more-using-bashs-built-devtcp-file-tcpip 135 | -- http://thesmithfam.org/blog/2006/05/23/bash-socket-programming-with-devtcp-2/ 136 | local function requestSelf_BASH(action) 137 | local cmd = '/usr/sbin/lsof -sTCP:LISTEN -Pani -Fn -p ' .. ngx.worker.pid() .. ' | /bin/bash -c \'' .. 138 | 'NC="&lsof=";LINES="";PORT="80";while read -r LINE; do LINES="$LINES$NC$LINE"; NC="%20";'.. 139 | ' PORT2=${LINE##n*:}; if ((PORT2==433)); then continue; fi; if ((PORT2>PORT)); then PORT=$PORT2; fi; done;' .. 140 | 'exec 4<>/dev/tcp/' .. route.cluster.master.host .. '/$PORT; ' .. 141 | 'echo -e "GET /' .. channel .. '/invoke?' .. action .. '$LINES HTTP/1.0\\n\\n" >&4; ' .. 142 | 'exec 4>&-\' &' 143 | os.execute(cmd) 144 | end 145 | 146 | -- fake a lsof result, runtime only 147 | -- sample: setWorker&lsof=p14739%20n*:80%20n*:8080 148 | local function LSOF_LINES() 149 | return table.concat({ 150 | 'p'..route.cluster.worker.pid, 151 | '*:'..route.cluster.master.port, 152 | '*:'..route.cluster.worker.port 153 | }, ' ') 154 | end 155 | 156 | -- fake a requestSelf_BASH() with ngx_cc.remote(), in runtime 157 | local function requestSelf_CC(action) 158 | route.remote('http://'..route.cluster.master.host .. 159 | ':' .. route.cluster.worker.port .. 160 | '/' .. channel .. '/invoke', { arg = {[action] = true, lsof = LSOF_LINES()} }) 161 | end 162 | 163 | -- fake a requestSelf_BASH(), and direct call route.invoke.setWorker 164 | local function requestSelf_DIRECT(action) 165 | pcall(route.invoke.setWorker, route, channel, { lsof = LSOF_LINES() }) 166 | end 167 | 168 | -- /channel_name/invoke?setWorker&lsof=p14739%20n*:80%20n*:8080 169 | local function setWorker(route, channel, arg) 170 | -- init worker infomation 171 | -- *) you can set worker.port by ngx.var.server_port without 'get_per_port' patch 172 | local cluster = route.cluster 173 | local worker, master = cluster.worker, cluster.master 174 | worker.pid, worker.port = ngx.var.pid, tostring(ngx.worker.port()) 175 | 176 | -- try register me as RouterPort 177 | local shared, key = route.shared, 'ngx_cc.'..channel..'.RouterPort' 178 | local port = shared:get(key) 179 | if port then 180 | cluster.router.port, cluster.router.pid = string.match(port, '^(%d+)/(%d+)') 181 | elseif shared:add(key, (worker.port .. '/' .. worker.pid)) then 182 | cluster.router.port, cluster.router.pid = worker.port, worker.pid 183 | else 184 | port = assert(shared:get(key), 'cant access router port from dictionary') -- again 185 | cluster.router.port, cluster.router.pid = string.match(port, '^(%d+)/(%d+)') 186 | end 187 | 188 | -- master setting 189 | local ps = require('lib.posix') 190 | local function is_valid_workerprocess(pid) 191 | return ps.pgid(tonumber(pid)) == tonumber(master.pid) -- pgid is '-1' or pid of master process 192 | end 193 | master.pid = tostring(ps.ppid(tonumber(worker.pid))) 194 | for port in string.gmatch(arg.lsof or '', "n[^:]+:(%d+) ?") do 195 | if (port ~= '433') and (port ~= worker.port) then 196 | master.port = port 197 | break 198 | end 199 | end 200 | 201 | -- super setting 202 | if cluster.super then 203 | local su = cluster.super 204 | if not su.host or (su.host == '') then 205 | su.host = master.host 206 | end 207 | if not su.port or (su.port == '') then 208 | su.port = master.port 209 | end 210 | end 211 | 212 | -- current worker is initialized 213 | -- PORT/channel_name/invoke?registerWorker=xxx 214 | cluster.worker_initiated = true 215 | if cluster.router.pid ~= worker.pid then -- router port&pid check, promise 'master' direction valid 216 | if ((cluster.router.port == worker.port) or -- new worker process reopen at old port, old pid invalid 217 | (not is_valid_workerprocess(cluster.router.pid))) then -- router worker process crush or exit 218 | cluster.router.port, cluster.router.pid = worker.port, worker.pid -- set me only 219 | -- shared:set(key, (worker.port .. '/' .. worker.pid)) -- register me as RouterPort delay, @see n4cDistrbutionTaskNode.lua module in ngx_4c 220 | end 221 | end 222 | route.cc('/_/invoke', { direction='master', args={registerWorker=worker.port, pid=worker.pid} }) 223 | 224 | -- report me as client 225 | -- SUPER:PORT/channel_name/invoke?reportClient=xxx 226 | if cluster.report_clients then 227 | route.cc('/_/invoke', { direction='super', args={reportClient=worker.port} }) 228 | end 229 | 230 | -- report hubPort, once for router, and will remove old hubPort by per-channel in super 231 | -- SUPER:PORT/channel_name/invoke?reportHubPort=xxx 232 | if worker.port == cluster.router.port then 233 | route.cc('/_/invoke', { direction='super', args={reportHubPort=master.port} }) 234 | end 235 | 236 | success_initialized(route, channel) 237 | end 238 | 239 | -- when work initiating, the requestSelf() will send a system process as daemon 240 | local parse = ngx.get_phase() 241 | assert(parse ~= 'init', 'initiating ngx_cc channel [' .. channel .. '] in init_by_lua*') 242 | local requestSelf = (parse == 'init_worker') and requestSelf_BASH or requestSelf_DIRECT 243 | route.invoke.setWorker = setWorker 244 | requestSelf('setWorker') 245 | end 246 | 247 | -- core 248 | return { 249 | kernal_actions = { 250 | reportClient = reportClient, 251 | reportHubPort = reportHubPort, 252 | registerWorker = registerWorker, 253 | }, 254 | kernal_initializer = { 255 | manual = manual_initializer, 256 | automatic = automatic_initializer, 257 | } 258 | } 259 | -------------------------------------------------------------------------------- /nginx.conf: -------------------------------------------------------------------------------- 1 | # ----------------------------------------------------------------------------- 2 | # This is a sample nginx.conf of ngx_cc v2 3 | # - need build nginx with lua-nginx-module and per-worker-listener patch 4 | # - http://wiki.nginx.org/HttpLuaModule#Installation 5 | # - https://github.com/arut/nginx-patches 6 | # - need one shared_dict, default name 'ngxcc_dict' 7 | # - accept_mutex must off (required by per-worker-listener) 8 | # - install tasks management is optional, will work in kada channel 9 | # ----------------------------------------------------------------------------- 10 | 11 | user nobody; 12 | worker_processes 4; 13 | #pid logs/nginx.pid; 14 | 15 | events { 16 | worker_connections 10240; 17 | accept_mutex off; 18 | } 19 | 20 | http { 21 | # include mime.types; 22 | 23 | ## init for per-worker, the '$prefix' setting by command line: 24 | ## > sbin/nginx -p ... 25 | lua_package_path '$prefix/?.lua;;'; 26 | init_worker_by_lua_file '$prefix/init_worker.lua'; 27 | 28 | # lua_code_cache off; 29 | lua_shared_dict ngxcc_dict 10M; 30 | uninitialized_variable_warn off; # recommend 31 | 32 | # install into request's response time 33 | rewrite_by_lua 'local kada=ngx_cc.channels["kada"]; if kada then kada.tasks:run() end'; 34 | 35 | # test with bash: 36 | # > for port in 80 {8010..8013}; do curl "http://127.0.0.1:$port/test"; done 37 | server { 38 | listen 80; 39 | listen 8010 per_worker; 40 | server_name localhost; 41 | 42 | location /test { 43 | content_by_lua ' 44 | ngx.say("pid: " .. ngx.var.pid .. ", port: " .. ngx.var.server_port .. "\tOkay.")'; 45 | } 46 | 47 | # caster, internal only 48 | location ~ ^/([^/]+)/cast { 49 | internal; 50 | set_by_lua $cc_host 'return ngx.var.cc_host'; 51 | set_by_lua $cc_port 'local p = ngx.var.cc_port or ""; return (p=="" or p=="80") and "" or ":"..p;'; 52 | rewrite ^/([^/]+)/cast/(.*)$ /$2 break; 53 | rewrite ^/([^/]+)/cast([^/]*)$ /$1/invoke$2 break; 54 | proxy_pass http://$cc_host$cc_port; 55 | proxy_read_timeout 2s; 56 | proxy_send_timeout 2s; 57 | proxy_connect_timeout 2s; 58 | } 59 | 60 | # invoke at worker listen port 61 | location ~ ^/([^/]+)/invoke { 62 | content_by_lua 'ngx_cc.invokes(ngx.var[1])'; 63 | } 64 | 65 | # hub at main listen port(80), will redirect to route listen port 66 | location ~ ^/([^/]+)/hub { 67 | content_by_lua 'ngx_cc.invokes(ngx.var[1], true)'; 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /ngx_cc.lua: -------------------------------------------------------------------------------- 1 | ----------------------------------------------------------------------------- 2 | -- NGX_CC v2.1.1 3 | -- Author: aimingoo@wandoujia.com 4 | -- Copyright (c) 2015.02-2015.10 5 | -- Descition: a framework of Nginx Communication Cluster. reliable 6 | -- dispatch/translation messages in nginx nodes and processes. 7 | -- 8 | -- Usage: 9 | -- ngx_cc = require('ngx_cc') 10 | -- route = ngx_cc:new('kada') -- work with 'kada' channel 11 | -- route.cc('/kada/invoke', 'master') 12 | -- .. 13 | -- instance methods: 14 | -- communication functions: route.cc(), route.cast(), and inherited from ngx_cc: all/remote/self, ... 15 | -- helper functions: route.isRoot(), route.isInvokeAtMaster(), route.isInvokeAtPer() 16 | -- global methods(instance inherited): 17 | -- ngx_cc.say(), ngx_cc.all(), ngx_cc.remote(), ngx_cc.self(), ngx_cc.transfer() 18 | -- ngx_cc.optionAgain() and ngx_cc.optionAgain2() 19 | -- these are index of ngx_cc 20 | -- ngx_cc.invokes(channelName) 21 | -- ngx_cc.channels 22 | -- 23 | -- History: 24 | -- 2015.11.04 release v2.1.1, channel_resources as native resource management 25 | -- 2015.10.22 release v2.1.0, publish NGX_CC node as N4C resources 26 | -- 2015.08.13 release v2.0.0, support NGX_4C programming architecture 27 | -- 2015.02 release v1.0.0 28 | ----------------------------------------------------------------------------- 29 | 30 | -- debugging 31 | local function ngx_log(...) 32 | return ngx.log(...) 33 | end 34 | 35 | -- core methods 36 | local ngx_cc_core = require('module.ngx_cc_core') 37 | 38 | -- with status >= 200 (ngx.HTTP_OK) and status < 300 (ngx.HTTP_SPECIAL_RESPONSE) for successful quits 39 | -- *) see: https://www.nginx.com/resources/wiki/modules/lua/#ngx-exit 40 | local HTTP_SUCCESS = function(status) 41 | return status >= 200 and status < 300 42 | end 43 | 44 | -- resource management 45 | local channel_resources = {} 46 | 47 | -- route framework 48 | local R = { 49 | tasks = {}, 50 | channels = {}, 51 | resources = channel_resources, -- for n4c architecture, removing in init_worker.lua 52 | 53 | -- the options: 54 | -- host : current nginx host ip, default is inherited from ngx_cc.cluster.master.host, or '127.0.0.1' 55 | -- port : current nginx listen port, invalid with initializer only, and deault is '80' 56 | -- dict : shared dictionary name in nginx.conf, default is 'ngxccv2_dict' in R.cluster.dict for all channels 57 | -- initializer : manual/automatic, default is automatic 58 | new = function(self, channel, options) 59 | local function clone(src, dest) 60 | dest = dest or {}; 61 | for name, value in pairs(src) do 62 | dest[name] = value 63 | end 64 | return dest 65 | end 66 | 67 | local R, options = self, options or {} 68 | local host = options.host or R.cluster.master.host or '127.0.0.1' 69 | local c_dict = options and options.dict or R.cluster.dict 70 | local cluster = { 71 | super = setmetatable({}, {__index=R.cluster.super}), 72 | master = setmetatable({ host = (R.cluster.master.host ~= host and host or nil)}, {__index=R.cluster.master}), 73 | router = setmetatable({ host = (R.cluster.router.host ~= host and host or nil)}, {__index=R.cluster.router}), 74 | worker = { host = host }, 75 | worker_initiated = false 76 | } 77 | local instance = { 78 | tasks = {}, 79 | invoke = clone(ngx_cc_core.kernal_actions), 80 | shared = assert(ngx.shared[c_dict], "access shared dictionary: " .. c_dict), 81 | cluster = cluster 82 | } 83 | 84 | -- cc(Cluster Communications) for per-instance 85 | -- support opt.directions: 86 | -- 'super' : 1:1 send to super node, the super is parent node. 87 | -- 'master' : 1:1 send to router process from any worker 88 | -- 'workers': 1:* send to all workers 89 | -- 'clients': 1:* send to all clients 90 | local c_patt, c_path = '^/_/', '/'..channel..'/' 91 | local c_patt2, c_path2 = '^/'..channel..'/[^/%?]+', '/'..channel..'/cast' 92 | local key_registed_workers = 'ngx_cc.'..channel..'.registed.workers' 93 | local key_registed_clients = 'ngx_cc.'..channel..'.registed.clients' 94 | local invalid = { master = nil, super = nil } -- continuous invalid for direction+addr in current channel 95 | instance.cc = function(url, opt) 96 | local opt = (type(opt) == 'string') and { direction = opt } or opt or R.optionAgain() 97 | if opt.method == nil then 98 | opt.method = ngx.HTTP_GET 99 | elseif type(opt.method) == 'string' then 100 | opt.method = ngx['HTTP_'..opt.method] 101 | end 102 | 103 | if not opt.direction then 104 | opt.direction = cluster.worker_initiated and 'master' or 'skip' 105 | elseif ((opt.direction == 'super' and instance.isRoot()) or 106 | (opt.direction == 'master' and not cluster.worker_initiated)) then 107 | opt.direction = 'skip' 108 | end 109 | 110 | if opt.always_forward_body then 111 | if opt.method == ngx.HTTP_POST then 112 | ngx.req.read_body() 113 | else 114 | opt.always_forward_body = false 115 | end 116 | end 117 | 118 | local addr = string.match(url, '^[^/]*//[^/]+') 119 | local uri = string.sub(url, string.len(addr or '')+1) 120 | local uri2 = uri:gsub(c_patt, c_path, 1):gsub(c_patt2, c_path2, 1); 121 | local r_status, r_resps, errRequests = true, {}, {} 122 | 123 | if opt.direction == 'master' then 124 | local ctx2 = { cc_headers = nil } -- the will reset to default 'ON' 125 | local opt2 = setmetatable({ 126 | vars = { 127 | cc_host = cluster.router.host, 128 | cc_port = cluster.router.port, 129 | }, 130 | ctx = not opt.ctx and ctx2 or (not opt.ctx.cc_headers and setmetatable(ctx2, { __index = opt.ctx })) or nil, 131 | }, {__index=opt}); 132 | 133 | local resp = ngx.location.capture(uri2, opt2); 134 | r_status = HTTP_SUCCESS(resp.status) 135 | r_resps = { resp } 136 | 137 | if r_status then 138 | if not invalid.master then invalid.master = nil end 139 | else 140 | invalid.master = invalid.master and (invalid.master + 1) or 1 141 | if invalid.master > 15 then -- continuous invalid 142 | -- TODO: the master is fail/invalid. kick all ??? 143 | ngx_log(ngx.ALERT, 'direction master is invalid at port ' .. cluster.router.port) 144 | end 145 | end 146 | 147 | -- local fmt = 'Cast from port %s to master, ' .. (r_status and 'done.' or 'error with status %d.') 148 | -- ngx_log(ngx.INFO, string.format(fmt, cluster.worker.port, resp.status)) 149 | elseif opt.direction == 'super' then 150 | local ctx2 = { cc_headers = 'OFF' } 151 | local opt2 = setmetatable({ 152 | vars = { 153 | cc_host = cluster.super.host, 154 | cc_port = cluster.super.port, 155 | }, 156 | ctx = not opt.ctx and ctx2 or (not opt.ctx.cc_headers and setmetatable(ctx2, { __index = opt.ctx })) or nil, 157 | }, {__index=opt}) 158 | 159 | local resp = ngx.location.capture(uri2, opt2) 160 | r_status = HTTP_SUCCESS(resp.status) 161 | r_resps = { resp } 162 | 163 | if r_status then 164 | if not invalid.super then invalid.super = nil end 165 | else 166 | invalid.super = invalid.super and (invalid.super + 1) or 1 167 | if invalid.super > 15 then -- continuous invalid 168 | ngx_log(ngx.ALERT, 'direction super is invalid at ' .. cluster.super.host .. ':' .. cluster.super.port) 169 | end 170 | end 171 | 172 | -- local fmt = 'Cast from port %s to super, ' .. (r_status and 'done.' or 'error with status %d.') 173 | -- ngx_log(ngx.INFO, string.format(fmt, cluster.worker.port, resp.status)) 174 | elseif opt.direction == 'workers' then 175 | local reqs, ports, registed_ports = {}, {}, channel_resources[key_registed_workers] or {} 176 | local ctx2, opt2 = { cc_headers = nil } -- the will reset to default 'ON' 177 | for _, port in ipairs(registed_ports) do 178 | opt2 = setmetatable({ 179 | vars = { 180 | cc_host = cluster.master.host, 181 | cc_port = port, 182 | }, 183 | ctx = not opt.ctx and ctx2 or (not opt.ctx.cc_headers and setmetatable(ctx2, { __index = opt.ctx })) or nil, 184 | }, {__index=opt}); 185 | table.insert(reqs, { uri2, opt2 }); 186 | table.insert(ports, port); 187 | end 188 | 189 | if #ports > 0 then 190 | local resps, errPorts = { ngx.location.capture_multi(reqs) }, {} 191 | for i, resp, key in ipairs(resps) do 192 | resp.port = ports[i] 193 | key = cluster.master.host .. ':' .. resp.port 194 | if not HTTP_SUCCESS(resp.status) then 195 | table.insert(errPorts, ports[i]) 196 | table.insert(errRequests, {instance.cc, '/_/invoke', 197 | { direction='master', args={invalidWorker=ports[i], t=invalid[key]} }}) 198 | invalid[key] = invalid[key] and (invalid[key] + 1) or 1 199 | else 200 | if not invalid[key] then invalid[key] = nil end 201 | end 202 | end 203 | r_status = #errPorts == 0 204 | r_resps = resps 205 | 206 | if not r_status then instance.all(errRequests) end 207 | -- local fmt = 'Cast from port %s to workers: %d/%d, ' .. (r_status and 'done.' or 'and no passed ports: %s.') 208 | -- ngx_log(ngx.INFO, string.format(fmt, cluster.worker.port, #reqs, #resps, table.concat(errPorts, ','))) 209 | end 210 | elseif opt.direction == 'clients' then 211 | local reqs, clients, registed_clients = {}, {}, channel_resources[key_registed_clients] or {} 212 | local ctx2, opt2 = { cc_headers = nil } -- the will reset to default 'ON' 213 | for _, client in ipairs(registed_clients) do 214 | local client, port = unpack(client) 215 | opt2 = setmetatable({ 216 | vars = { 217 | cc_host = client, 218 | cc_port = string.sub(port or '', 2), 219 | }, 220 | ctx = not opt.ctx and ctx2 or (not opt.ctx.cc_headers and setmetatable(ctx2, { __index = opt.ctx })) or nil, 221 | }, {__index=opt}) 222 | table.insert(reqs, { uri2, opt2 }); 223 | table.insert(clients, client..port); 224 | end 225 | 226 | if #clients > 0 then 227 | local resps, errClients, errRequests = { ngx.location.capture_multi(reqs) }, {}, {} 228 | for i, resp, key in ipairs(resps) do 229 | resp.client = clients[i] 230 | key = resp.client 231 | if not HTTP_SUCCESS(resp.status) then 232 | table.insert(errClients, clients[i]) 233 | table.insert(errRequests, {instance.cc, '/_/invoke', 234 | { direction='master', args={invalidClient=clients[i], t=invalid[key]} }}) 235 | invalid[key] = invalid[key] and (invalid[key] + 1) or 1 236 | else 237 | if not invalid[key] then invalid[key] = nil end 238 | end 239 | end 240 | r_status = #errClients == 0 241 | r_resps = resps 242 | 243 | if not r_status then instance.all(errRequests) end 244 | -- local fmt = 'Cast from port %s to clients: %d/%d, ' .. (r_status and 'done.' or 'and no passed clients: %s.') 245 | -- ngx_log(ngx.INFO, string.format(fmt, cluster.worker.port, #reqs, #resps, table.concat(errClients, ','))) 246 | end 247 | end 248 | 249 | return r_status, r_resps 250 | end 251 | 252 | -- cast to self, support '^/_/' replacer in route.self() 253 | instance.self = function(url, opt) 254 | return R.self(url:gsub(c_patt, c_path, 1), opt) 255 | end 256 | 257 | -- cast to all workers (with current worker) 258 | instance.cast = function(url, opt) 259 | return instance.cc(url, R.optionAgain('workers', opt)) 260 | end 261 | 262 | -- check current node is root 263 | instance.isRoot = function() 264 | local master, su = cluster.master, cluster.super 265 | return not su or (su.host == master.host and su.port == master.port) 266 | end 267 | 268 | -- check current worker is master, else false will cast to 'master' with full invoke data 269 | instance.isInvokeAtMaster = function() 270 | if cluster.worker.port ~= cluster.router.port then 271 | return false, instance.cc('/_/_'..ngx.var.uri); 272 | else 273 | return true 274 | end 275 | end 276 | 277 | -- check current is per-worker, else false will cast to 'workers' with full invoke data 278 | instance.isInvokeAtPer = function() 279 | if cluster.worker.port ~= ngx.var.server_port then 280 | return false, instance.cc('/_/_'..ngx.var.uri, instance.optionAgain('workers')); 281 | else 282 | return true 283 | end 284 | end 285 | 286 | -- reset super 287 | instance.transfer = function(super, ...) 288 | local host, port = string.match(super, '([^/:]*):?(%d*)$') 289 | cluster.super.host, cluster.super.port = host, port or ({...})[1] or '80' 290 | end 291 | 292 | -- channel initiatation 293 | local kernal = ngx_cc_core.kernal_initializer 294 | local initializer = kernal[options.initializer or 'automatic'] or kernal.automatic 295 | initializer(instance, channel, options) 296 | 297 | R.channels[channel] = instance 298 | return setmetatable(instance, {__index=function(_, key) 299 | local disabled = {channels = true, invokes = true} 300 | return not disabled[key] and R[key] or nil 301 | end}) 302 | end 303 | } 304 | 305 | -- option base current worker's request/context/vars/... 306 | function R.optionAgain(direction, opt) 307 | local opt = opt or { 308 | always_forward_body = true, 309 | method = ngx.req.get_method(), 310 | args = ngx.req.get_uri_args(), 311 | } 312 | if direction and direction.args and opt.args then 313 | for key, value in pairs(opt.args) do 314 | if direction.args[key] == nil then 315 | direction.args[key] = value 316 | end 317 | end 318 | end 319 | return (direction == nil) and opt or setmetatable( 320 | type(direction) == 'table' and direction or { direction = tostring(direction) }, 321 | { __index = opt } 322 | ) 323 | end 324 | 325 | -- optionAgain() with force_mix support 326 | function R.optionAgain2(direction, opt, force_mix_into_current) 327 | return R.optionAgain(direction, force_mix_into_current and R.optionAgain(opt) or opt) 328 | end 329 | 330 | -- cast to self, it's warp of ngx.location.capture() only 331 | -- 1) for ngx_cc.self(), cant use '^/_/' replacer in url 332 | function R.self(url, opt) 333 | local resp = ngx.location.capture(url, opt); 334 | -- ngx_log(ngx.INFO, 'Cast to self: ' .. (r_status and 'done.' or 'and no passed.')) 335 | return HTTP_SUCCESS(resp.status), { resp } 336 | end 337 | 338 | -- cast to remote, RPC 339 | function R.remote(url, opt) 340 | local addr = string.match(url, '^[^/]*//[^/]+') 341 | local uri = string.sub(url, string.len(addr or '')+1) 342 | assert(addr, 'need full remote url') 343 | local uri2, host, port = '/_/cast' .. uri, string.match(addr, '^[^/]*//([^:]+):?(.*)') 344 | local ctx, ctx2 = opt and opt.ctx or nil, { cc_headers = 'OFF' } 345 | local resp = ngx.location.capture(uri2, setmetatable({ 346 | vars = { 347 | cc_host = host, 348 | cc_port = port, 349 | }, 350 | ctx = not ctx and ctx2 or (not ctx.cc_headers and setmetatable(ctx2, { __index = ctx })) or nil, 351 | }, {__index=opt})); 352 | -- local fmt = 'Cast from port %s to remote: %s, ' .. (r_status and 'done.' or 'error with status %d.') 353 | -- ngx_log(ngx.INFO, string.format(fmt, cluster.worker.port, addr, resp.status)) 354 | return HTTP_SUCCESS(resp.status), { resp } 355 | end 356 | 357 | -- simple say() all responses body for R.cc() result 358 | function R.say(r_status, r_resps) 359 | for _, resp in ipairs(r_resps) do 360 | if resp then -- false value, or a resp object 361 | if (HTTP_SUCCESS(resp.status) and (type(resp.body) == 'string') and 362 | (string.len(resp.body) > 0)) then 363 | ngx.say(resp.body) 364 | end 365 | if #resp > 0 then R.say(r_status, resp) end -- deep 366 | end 367 | end 368 | end 369 | 370 | -- capture all requests and return, ngx.thread based 371 | function R.all(requests, comment) -- comment is log only 372 | 373 | local function future(threads) 374 | return setmetatable({}, { 375 | __index = function(t, key) -- call once only 376 | for i, co in ipairs(threads) do 377 | local FAIL, success, captured, resps = false, ngx.thread.wait(co) 378 | rawset(t, i, (success and captured) and (#resps>1 and resps or resps[1]) or FAIL) 379 | end 380 | getmetatable(t).__index = nil 381 | return rawget(t, key) 382 | end 383 | }) 384 | end 385 | 386 | local threads = {} 387 | for _, request in ipairs(requests) do 388 | table.insert(threads, ngx.thread.spawn(request[1], unpack(request, 2))) 389 | end 390 | 391 | -- try wait all thread once, and return all status 392 | local results, ok = future(threads), 0 393 | for i = 1, #threads do 394 | if results[i] then ok = ok + 1 end 395 | end 396 | 397 | -- local fmt = (comment or 'all requests') .. ' done, %d/%d success.' 398 | -- ngx_log(ngx.INFO, string.format(fmt, ok, #threads)) 399 | return ok>0, results 400 | end 401 | 402 | -- map to named index table 403 | local function maped(keys, map) 404 | local map = map or {} 405 | for _, key in ipairs(keys) do map[key] = true end 406 | return map 407 | end 408 | 409 | -- see: https://coronalabs.com/blog/2013/04/16/lua-string-magic/ 410 | local function split(self, inSplitPattern, outResults) 411 | local theStart, thePatten, result = 1, inSplitPattern or ',', outResults or {} 412 | local theSplitStart, theSplitEnd = string.find(self, thePatten, theStart) 413 | while theSplitStart do 414 | table.insert(result, string.sub(self, theStart, theSplitStart-1)) 415 | theStart = theSplitEnd + 1 416 | theSplitStart, theSplitEnd = string.find(self, thePatten, theStart) 417 | end 418 | table.insert(result, string.sub(self, theStart )) 419 | return result 420 | end 421 | 422 | -- check empty table 423 | local function isEmpty(t) 424 | return next(t) == nil 425 | end 426 | 427 | -- transfer server at root/super 428 | function R.transfer(super, channels, clients) 429 | local channels = channels == '*' and R.channels or split(channels) 430 | local clients = clients == '*' and {} or maped(split(clients)) 431 | local forAll, requests, opt = isEmpty(clients), {}, { args={transferServer=super} } 432 | for channel in pairs(channels) do 433 | local saved, instance, key_registed_clients = {}, R.channels[channel], 'ngx_cc.'..channel..'.registed.clients' 434 | local registed_clients = channel_resources[key_registed_clients] or {} 435 | for client, port in ipairs(registed_clients) do 436 | if forAll or clients[client] then 437 | table.insert(requests, {instance.remote, 'http://'..client..port..'/'..channel..'/invoke', opt}) 438 | else 439 | table.insert(saved, client..port) 440 | end 441 | end 442 | -- saving 443 | instance.shared:set(key_registed_clients, table.concat(saved, ',')) 444 | end 445 | -- remote call these removed clients 446 | R.all(requests) 447 | end 448 | 449 | local function defaultInvoker(route, channel, arg) 450 | for key, action in pairs(route.invoke or {}) do 451 | if arg[key] then 452 | return action(route, channel, arg) 453 | end 454 | end 455 | end 456 | 457 | function R.invokes(channel, master_only) 458 | local route = R.channels[channel] 459 | if route then 460 | -- local no_redirected, r_status, r_resps = unpack(master_only and {route.isInvokeAtMaster()} or {true}) 461 | local no_redirected, r_status, r_resps = true 462 | if master_only then 463 | no_redirected, r_status, r_resps = route.isInvokeAtMaster() 464 | end 465 | if no_redirected then 466 | local invoker = type(route.invoke) == 'function' and route.invoke or defaultInvoker 467 | invoker(route, channel, ngx.req.get_uri_args()) 468 | else 469 | R.say(r_status, r_resps) 470 | end 471 | else 472 | ngx.status = 403 473 | ngx.say("URL include unknow channel.") 474 | ngx.exit(ngx.HTTP_OK) 475 | end 476 | -- ngx.log(ngx.INFO, table.concat({'', channel, ngx.var.cc_host, ngx.var.cc_port, ''}, '||')) 477 | end 478 | 479 | -- reosure getter for native ngx_cc, direct read shared dictionary 480 | -- *) saving iterator of worker/clients direction, uses unpack(iterator) to got values by caller 481 | local registed_keys = {} 482 | setmetatable(channel_resources, {__index = function(t, key) 483 | local channel, direction = unpack(registed_keys[key] or {false}) 484 | if channel == false then 485 | local ngx_cc_registed_key = '^ngx_cc%.([^%.]+)%.registed%.([^%.]+)$' 486 | channel, direction = string.match(key, ngx_cc_registed_key) 487 | registed_keys[key] = {channel, direction} 488 | end 489 | 490 | local instance = channel and direction and R.channels[channel] 491 | if instance and (direction == 'workers' or direction == 'clients') then 492 | local registed, newValue = instance.shared:get(key), {} 493 | if registed then 494 | if direction == 'workers' then 495 | for port in string.gmatch(registed, '(%d+)[^,]*,?') do table.insert(newValue, port) end 496 | elseif direction == 'clients' then 497 | for host, port in string.gmatch(registed, '([^:,]+)([^,]*),?') do table.insert(newValue, {host, port}) end 498 | end 499 | end 500 | return newValue 501 | end 502 | end}) 503 | 504 | -- base struct for multi-workers sub-system 505 | R.cluster = { 506 | super = { host='127.0.0.1', port='80' }, -- cc:super 507 | master = { host='127.0.0.1' }, -- cc:workers, use and communication in workes only 508 | dict = 'ngxcc_dict', -- default cluster/global dictionry name 509 | report_clients = false, -- false only, non implement now 510 | } 511 | -- current worker 512 | R.cluster.worker = { 513 | host = R.cluster.master.host, 514 | port = assert(ngx.worker.port and tostring(ngx.worker.port()), 'require per_worker with get_per_port patch'), 515 | } 516 | -- cc:master, and will reset in ngx_cc_core.lua 517 | R.cluster.router = { host=R.cluster.master.host } 518 | 519 | return R 520 | -------------------------------------------------------------------------------- /patch/ngx-worker-port.patch: -------------------------------------------------------------------------------- 1 | --- ./ngx_lua-0.9.15.orig/src/ngx_http_lua_worker.c 2015-02-19 07:29:39.000000000 +0800 2 | +++ ./ngx_lua-0.9.15/src/ngx_http_lua_worker.c 2015-06-21 05:19:59.000000000 +0800 3 | @@ -15,6 +15,7 @@ 4 | 5 | static int ngx_http_lua_ngx_worker_exiting(lua_State *L); 6 | static int ngx_http_lua_ngx_worker_pid(lua_State *L); 7 | +static int ngx_http_lua_ngx_worker_port(lua_State *L); 8 | 9 | 10 | void 11 | @@ -28,6 +29,9 @@ 12 | lua_pushcfunction(L, ngx_http_lua_ngx_worker_pid); 13 | lua_setfield(L, -2, "pid"); 14 | 15 | + lua_pushcfunction(L, ngx_http_lua_ngx_worker_port); 16 | + lua_setfield(L, -2, "port"); 17 | + 18 | lua_setfield(L, -2, "worker"); 19 | } 20 | 21 | @@ -47,6 +51,12 @@ 22 | return 1; 23 | } 24 | 25 | +static int 26 | +ngx_http_lua_ngx_worker_port(lua_State *L) 27 | +{ 28 | + lua_pushinteger(L, (lua_Integer) ngx_server_port); 29 | + return 1; 30 | +} 31 | 32 | #ifndef NGX_LUA_NO_FFI_API 33 | int 34 | @@ -61,4 +71,10 @@ 35 | { 36 | return (int) ngx_exiting; 37 | } 38 | + 39 | +int 40 | +ngx_http_lua_ffi_worker_port(void) 41 | +{ 42 | + return (int) ngx_server_port; 43 | +} 44 | #endif 45 | -------------------------------------------------------------------------------- /patch/per-worker.patch: -------------------------------------------------------------------------------- 1 | diff -Naur ./nginx-1.7.7.orig/src/core/ngx_connection.c ./nginx-1.7.7/src/core/ngx_connection.c 2 | --- ./nginx-1.7.7.orig/src/core/ngx_connection.c 2014-10-28 23:04:47.000000000 +0800 3 | +++ ./nginx-1.7.7/src/core/ngx_connection.c 2015-05-28 03:04:54.000000000 +0800 4 | @@ -8,6 +8,7 @@ 5 | #include 6 | #include 7 | #include 8 | +#include 9 | 10 | 11 | ngx_os_io_t ngx_io; 12 | @@ -305,11 +306,19 @@ 13 | ngx_open_listening_sockets(ngx_cycle_t *cycle) 14 | { 15 | int reuseaddr; 16 | - ngx_uint_t i, tries, failed; 17 | + ngx_uint_t i, tries, failed, jumper; 18 | ngx_err_t err; 19 | ngx_log_t *log; 20 | ngx_socket_t s; 21 | ngx_listening_t *ls; 22 | + ngx_core_conf_t *ccf; 23 | + 24 | + size_t len; 25 | + struct sockaddr *sockaddr; 26 | + struct sockaddr_in *sin; 27 | +#if (NGX_HAVE_INET6) 28 | + struct sockaddr_in6 *sin6; 29 | +#endif 30 | 31 | reuseaddr = 1; 32 | #if (NGX_SUPPRESS_WARN) 33 | @@ -317,6 +326,8 @@ 34 | #endif 35 | 36 | log = cycle->log; 37 | + ccf = (ngx_core_conf_t *) ngx_get_conf(cycle->conf_ctx, ngx_core_module); 38 | + jumper = 0; 39 | 40 | /* TODO: configurable try number */ 41 | 42 | @@ -336,6 +347,60 @@ 43 | continue; 44 | } 45 | 46 | + sockaddr = ls[i].sockaddr; 47 | + 48 | + if (ngx_process == NGX_PROCESS_WORKER) { 49 | + 50 | + if (!ls[i].per_worker) { 51 | + continue; 52 | + } 53 | + 54 | + sockaddr = ngx_palloc(cycle->pool, ls[i].socklen); 55 | + if (sockaddr == NULL) { 56 | + return NGX_ERROR; 57 | + } 58 | + 59 | + ngx_memcpy(sockaddr, ls[i].sockaddr, ls[i].socklen); 60 | + 61 | + switch (ls[i].sockaddr->sa_family) { 62 | +#if (NGX_HAVE_INET6) 63 | + case AF_INET6: 64 | + sin6 = (struct sockaddr_in6 *) sockaddr; 65 | + sin6->sin6_port = htons(ntohs(sin6->sin6_port) + jumper + 66 | + ngx_worker_slot); 67 | + ngx_server_port = ntohs(sin->sin_port); 68 | + break; 69 | +#endif 70 | + default: /* AF_INET */ 71 | + sin = (struct sockaddr_in *) sockaddr; 72 | + sin->sin_port = htons(ntohs(sin->sin_port) + jumper + 73 | + ngx_worker_slot); 74 | + ngx_server_port = ntohs(sin->sin_port); 75 | + } 76 | + 77 | + jumper += ccf->worker_processes; 78 | + len = ls[i].addr_text_max_len; 79 | + ls[i].addr_text.data = ngx_palloc(cycle->pool, len); 80 | + 81 | + if (ls[i].addr_text.data == NULL) { 82 | + return NGX_ERROR; 83 | + } 84 | + 85 | + len = ngx_sock_ntop(sockaddr, 86 | +#if (nginx_version >= 1005003) 87 | + ls[i].socklen, 88 | +#endif 89 | + ls[i].addr_text.data, len, 1); 90 | + if (len == 0) { 91 | + return NGX_ERROR; 92 | + } 93 | + 94 | + ls[i].addr_text.len = len; 95 | + 96 | + } else if (ls[i].per_worker) { 97 | + continue; 98 | + } 99 | + 100 | if (ls[i].inherited) { 101 | 102 | /* TODO: close on exit */ 103 | @@ -408,7 +473,7 @@ 104 | ngx_log_debug2(NGX_LOG_DEBUG_CORE, log, 0, 105 | "bind() %V #%d ", &ls[i].addr_text, s); 106 | 107 | - if (bind(s, ls[i].sockaddr, ls[i].socklen) == -1) { 108 | + if (bind(s, sockaddr, ls[i].socklen) == -1) { 109 | err = ngx_socket_errno; 110 | 111 | if (err != NGX_EADDRINUSE || !ngx_test_config) { 112 | diff -Naur ./nginx-1.7.7.orig/src/core/ngx_connection.h ./nginx-1.7.7/src/core/ngx_connection.h 113 | --- ./nginx-1.7.7.orig/src/core/ngx_connection.h 2014-10-28 23:04:47.000000000 +0800 114 | +++ ./nginx-1.7.7/src/core/ngx_connection.h 2015-06-03 01:39:43.000000000 +0800 115 | @@ -54,6 +54,7 @@ 116 | unsigned open:1; 117 | unsigned remain:1; 118 | unsigned ignore:1; 119 | + unsigned per_worker:1; 120 | 121 | unsigned bound:1; /* already bound */ 122 | unsigned inherited:1; /* inherited from previous process */ 123 | diff -Naur ./nginx-1.7.7.orig/src/http/ngx_http.c ./nginx-1.7.7/src/http/ngx_http.c 124 | --- ./nginx-1.7.7.orig/src/http/ngx_http.c 2014-12-07 09:32:49.000000000 +0800 125 | +++ ./nginx-1.7.7/src/http/ngx_http.c 2015-06-03 01:39:43.000000000 +0800 126 | @@ -1821,6 +1821,8 @@ 127 | ls->fastopen = addr->opt.fastopen; 128 | #endif 129 | 130 | + ls->per_worker = addr->opt.per_worker; 131 | + 132 | return ls; 133 | } 134 | 135 | diff -Naur ./nginx-1.7.7.orig/src/http/ngx_http_core_module.c ./nginx-1.7.7/src/http/ngx_http_core_module.c 136 | --- ./nginx-1.7.7.orig/src/http/ngx_http_core_module.c 2014-12-07 09:32:49.000000000 +0800 137 | +++ ./nginx-1.7.7/src/http/ngx_http_core_module.c 2015-06-03 01:39:43.000000000 +0800 138 | @@ -4061,6 +4061,11 @@ 139 | continue; 140 | } 141 | 142 | + if (ngx_strcmp(value[n].data, "per_worker") == 0) { 143 | + lsopt.per_worker = 1; 144 | + continue; 145 | + } 146 | + 147 | #if (NGX_HAVE_SETFIB) 148 | if (ngx_strncmp(value[n].data, "setfib=", 7) == 0) { 149 | lsopt.setfib = ngx_atoi(value[n].data + 7, value[n].len - 7); 150 | diff -Naur ./nginx-1.7.7.orig/src/http/ngx_http_core_module.h ./nginx-1.7.7/src/http/ngx_http_core_module.h 151 | --- ./nginx-1.7.7.orig/src/http/ngx_http_core_module.h 2014-10-28 23:04:48.000000000 +0800 152 | +++ ./nginx-1.7.7/src/http/ngx_http_core_module.h 2015-06-03 01:39:43.000000000 +0800 153 | @@ -72,6 +72,7 @@ 154 | unsigned default_server:1; 155 | unsigned bind:1; 156 | unsigned wildcard:1; 157 | + unsigned per_worker:1; 158 | #if (NGX_HTTP_SSL) 159 | unsigned ssl:1; 160 | #endif 161 | diff -Naur ./nginx-1.7.7.orig/src/os/unix/ngx_process.c ./nginx-1.7.7/src/os/unix/ngx_process.c 162 | --- ./nginx-1.7.7.orig/src/os/unix/ngx_process.c 2014-10-28 23:04:49.000000000 +0800 163 | +++ ./nginx-1.7.7/src/os/unix/ngx_process.c 2015-06-03 01:39:43.000000000 +0800 164 | @@ -34,6 +34,7 @@ 165 | ngx_socket_t ngx_channel; 166 | ngx_int_t ngx_last_process; 167 | ngx_process_t ngx_processes[NGX_MAX_PROCESSES]; 168 | +ngx_int_t ngx_worker_slot; 169 | 170 | 171 | ngx_signal_t signals[] = { 172 | diff -Naur ./nginx-1.7.7.orig/src/os/unix/ngx_process.h ./nginx-1.7.7/src/os/unix/ngx_process.h 173 | --- ./nginx-1.7.7.orig/src/os/unix/ngx_process.h 2014-10-28 23:04:49.000000000 +0800 174 | +++ ./nginx-1.7.7/src/os/unix/ngx_process.h 2015-06-03 01:39:43.000000000 +0800 175 | @@ -83,6 +83,7 @@ 176 | extern ngx_int_t ngx_process_slot; 177 | extern ngx_int_t ngx_last_process; 178 | extern ngx_process_t ngx_processes[NGX_MAX_PROCESSES]; 179 | +extern ngx_int_t ngx_worker_slot; 180 | 181 | 182 | #endif /* _NGX_PROCESS_H_INCLUDED_ */ 183 | diff -Naur ./nginx-1.7.7.orig/src/os/unix/ngx_process_cycle.c ./nginx-1.7.7/src/os/unix/ngx_process_cycle.c 184 | --- ./nginx-1.7.7.orig/src/os/unix/ngx_process_cycle.c 2014-12-07 09:32:49.000000000 +0800 185 | +++ ./nginx-1.7.7/src/os/unix/ngx_process_cycle.c 2015-06-03 01:39:43.000000000 +0800 186 | @@ -35,6 +35,7 @@ 187 | ngx_uint_t ngx_process; 188 | ngx_pid_t ngx_pid; 189 | ngx_uint_t ngx_threaded; 190 | +ngx_uint_t ngx_server_port; 191 | 192 | sig_atomic_t ngx_reap; 193 | sig_atomic_t ngx_sigio; 194 | @@ -743,6 +744,8 @@ 195 | 196 | ngx_process = NGX_PROCESS_WORKER; 197 | 198 | + ngx_worker_slot = worker; 199 | + 200 | ngx_worker_process_init(cycle, worker); 201 | 202 | ngx_setproctitle("worker process"); 203 | @@ -976,6 +979,12 @@ 204 | ls[i].previous = NULL; 205 | } 206 | 207 | + if (ngx_open_listening_sockets(cycle) != NGX_OK) { 208 | + ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno, 209 | + "failed to init worker listeners"); 210 | + exit(2); 211 | + } 212 | + 213 | for (i = 0; ngx_modules[i]; i++) { 214 | if (ngx_modules[i]->init_process) { 215 | if (ngx_modules[i]->init_process(cycle) == NGX_ERROR) { 216 | diff -Naur ./nginx-1.7.7.orig/src/os/unix/ngx_process_cycle.h ./nginx-1.7.7/src/os/unix/ngx_process_cycle.h 217 | --- ./nginx-1.7.7.orig/src/os/unix/ngx_process_cycle.h 2014-10-28 23:04:49.000000000 +0800 218 | +++ ./nginx-1.7.7/src/os/unix/ngx_process_cycle.h 2015-06-03 01:39:43.000000000 +0800 219 | @@ -45,6 +45,7 @@ 220 | extern ngx_uint_t ngx_daemonized; 221 | extern ngx_uint_t ngx_threaded; 222 | extern ngx_uint_t ngx_exiting; 223 | +extern ngx_uint_t ngx_server_port; 224 | 225 | extern sig_atomic_t ngx_reap; 226 | extern sig_atomic_t ngx_sigio; 227 | -------------------------------------------------------------------------------- /patch/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ## 4 | ## download and unpack nginx with lua, openresty is recommend 5 | ## 6 | # curl -s 'https://openresty.org/download/ngx_openresty-1.7.10.2.tar.gz' | tar -xzv 7 | 8 | ## 9 | ## apply patchs 10 | ## - support nginx 1.7x and ngx_lua 0.9x 11 | ## - nginx 1.5+ is supported, plz force apply 12 | ## 13 | 14 | cd ./ngx_openresty-1.7.*/bundle/nginx-1.7*/ 15 | patch -p2 < ../../../per-worker.patch 16 | cd - 17 | 18 | cd ./ngx_openresty-1.7.*/bundle/ngx_lua-0.9*/ 19 | patch -p2 < ../../../ngx-worker-port.patch 20 | cd - --------------------------------------------------------------------------------