├── .gitignore ├── Makefile ├── README.markdown ├── apiary.apib ├── config ├── doc ├── couchbase.en.markdown └── couchbase.ru.markdown ├── etc ├── nginx.conf └── nginx.stress.conf ├── src ├── ddebug.h ├── ngx_lcb_callbacks.c ├── ngx_lcb_callbacks.h ├── ngx_lcb_module.c ├── ngx_lcb_module.h ├── ngx_lcb_plugin.c └── ngx_lcb_plugin.h └── t └── smoke.t /.gitignore: -------------------------------------------------------------------------------- 1 | *.orig 2 | cbgb* 3 | t/servroot 4 | tmp 5 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | GET := wget -q -O 2 | GUNZIP := gzip -d 3 | PLATFORM = $(shell uname -sm) 4 | MOCK = ./cbgb 5 | PKGNAME := nginx-couchbase-module-$(shell git describe) 6 | 7 | NGX_VERSIONS := v1.2.6 v1.4.0 8 | AB_CONCURRENCY := 10 9 | AB_REQUESTS := 1000 10 | AB_NGX_CONFIG := etc/nginx.stress.conf 11 | AB_URI := http://localhost:8080/lcb?cmd=set&key=foo&val=bar 12 | 13 | all: 14 | $(MAKE) -C.. all 15 | 16 | check: 17 | @for ver in ${NGX_VERSIONS} ; do \ 18 | echo "==========================" ; \ 19 | echo "Checking with nginx $$ver" ; \ 20 | if (cd ../nginx; git checkout $$ver && git clean -dfx) && \ 21 | $(MAKE) -C.. check ; \ 22 | then \ 23 | res="$$res\n$$ver - OK" ; \ 24 | else \ 25 | res="$$res\n$$ver - FAILURE" ; \ 26 | fi \ 27 | done; \ 28 | printf "$$res\n" 29 | 30 | stress: 31 | @for ver in ${NGX_VERSIONS} ; do \ 32 | echo "==========================" ; \ 33 | echo "Stressing with nginx $$ver" ; \ 34 | pid=`cat ../install/logs/nginx.pid 2>/dev/null` ; \ 35 | if [ -n $$pid ] ; then \ 36 | kill $$pid > /dev/null 2>&1 ; \ 37 | fi ; \ 38 | if (cd ../nginx; git checkout $$ver && git clean -dfx) && \ 39 | $(MAKE) -C.. all WARNINGS= DEBUG=0 && \ 40 | cp $(AB_NGX_CONFIG) ../install/conf/nginx.conf && \ 41 | ../install/sbin/nginx && \ 42 | ab -c $(AB_CONCURRENCY) -n $(AB_REQUESTS) '$(AB_URI)' ; \ 43 | then \ 44 | res="$$res\n$$ver - OK" ; \ 45 | else \ 46 | res="$$res\n$$ver - FAILURE" ; \ 47 | fi ; \ 48 | pid=`cat ../install/logs/nginx.pid 2>/dev/null` ; \ 49 | if [ -n $$pid ] ; then \ 50 | kill $$pid > /dev/null 2>&1 ; \ 51 | fi ; \ 52 | done; \ 53 | printf "$$res\n" 54 | 55 | cbgb-run: cbgb 56 | $(MOCK) > $(MOCK).log 2>&1 & 57 | @echo wait 1 seconds until cbgb will start 58 | @sleep 1 59 | 60 | cbgb: 61 | ifeq ($(PLATFORM),Linux x86_64) 62 | $(GET) $(MOCK).gz http://cbgb.io/cbgb.lin64.gz 63 | else 64 | @echo "Unknown OS. Update cbgb target in Makefile" 65 | @exit 1 66 | endif 67 | $(GUNZIP) $(MOCK).gz 68 | chmod a+x $(MOCK) 69 | 70 | dist: 71 | git clean -dfx 72 | mkdir $(PKGNAME) 73 | cp -a config src etc doc README.markdown t $(PKGNAME) 74 | tar cf - $(PKGNAME) | gzip -9 - > $(PKGNAME).tar.gz 75 | rm -rf $(PKGNAME) 76 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | # nginx-couchbase-module 2 | 3 | Hi, this is couchbase module for nginx. Here is short build 4 | instructions. More docs are coming. 5 | 6 | Your test machine should have C compiler installed, as well as 7 | autotools. 8 | 9 | mkdir couchbase-nginx-module 10 | cd couchbase-nginx-module 11 | repo init -u git://github.com/trondn/manifests.git -m nginx.xml 12 | repo sync 13 | make 14 | 15 | # Important Note 16 | 17 | This release depends on [fork of libcouchbase][1], therefore it **will 18 | not work** with the client, you can download from binary repositories. 19 | The most recent version of the forked library is **2.0.3nginx3** and 20 | you can download it using this URL: 21 | http://packages.couchbase.com/clients/c/libcouchbase-2.0.3nginx3.tar.gz 22 | 23 | The changes from the fork are going to be integrated to upstream soon. 24 | 25 | # Testing 26 | 27 | There is way to run all tests against different versions of nginx, in 28 | case you are using repo to checkout sources: 29 | 30 | cd module 31 | make check NGX_VERSIONS="v1.4.0 v1.3.10" 32 | 33 | By default `make check` will run use `NGX_VERSIONS="v1.2.6 v1.4.0"`. 34 | 35 | Note that until version [v1.3.10](https://github.com/nginx/nginx/commit/v1.3.10) 36 | nginx has a bug which doesn't allow to add headers to 201 responses, 37 | therefore corresponding tests are skipped in the testsuite for these 38 | versions. 39 | 40 | There is also make target for stress testing using Apache Benchmark: 41 | `make stress`. It also honours `NGX_VERSIONS` and also depends on 42 | several more variables. These are default values, pretty 43 | self-explanatory: 44 | 45 | NGX_VERSIONS="v1.2.6 v1.4.0" 46 | AB_CONCURRENCY=10 47 | AB_REQUESTS=1000 48 | AB_NGX_CONFIG=etc/nginx.stress.conf 49 | AB_URI=http://localhost:8080/lcb?cmd=set&key=foo&val=bar 50 | 51 | # Usage 52 | 53 | This section describes step by step guide how to build nginx with 54 | couchbase module. This version (0.3.1) is only tested with 55 | nginx v1.3.7, v1.2.6 and v1.4.0, other versions might also work, more 56 | broad coverage is subject of upcoming releases. Also it requires 57 | modified libcouchbase library from `nginx` branch of my fork 58 | [avsej/libcouchbase][1]. 59 | 60 | Create build directory and download all dependencies there: 61 | 62 | mkdir nginx-couchbase 63 | cd nginx-couchbase 64 | wget http://nginx.org/download/nginx-1.3.7.tar.gz 65 | wget http://packages.couchbase.com/clients/c/libcouchbase-2.0.3nginx3.tar.gz 66 | wget http://packages.couchbase.com/clients/c/nginx-couchbase-module-0.3.1.tar.gz 67 | for i in *.tar.gz; do tar xvf $i; done 68 | 69 | Build and install everything into `/opt/nginx-couchbase` prefix: 70 | 71 | export PREFIX=/opt/nginx-couchbase 72 | 73 | cd libcouchbase-2.0.3nginx3 74 | ./configure --prefix=$PREFIX --enable-debug --disable-plugins --disable-tests --disable-couchbasemock 75 | make && sudo make install 76 | cd .. 77 | 78 | cd nginx-1.3.7 79 | export LIBCOUCHBASE_INCLUDE=$PREFIX/include 80 | export LIBCOUCHBASE_LIB=$PREFIX/lib 81 | ./configure --prefix=$PREFIX --with-debug --add-module=../nginx-couchbase-module-0.3.1 82 | make && sudo make install 83 | 84 | Now you should install and configure Couchbase Server. Read more at 85 | official site: http://www.couchbase.com/download. Make sure you have 86 | configured bucket `default`. 87 | 88 | Given all stuff installed, lets deploy the simplest config file, 89 | which can be find in `etc/nginx.conf` of this repository. Here is 90 | `server` section of this config: 91 | 92 | server { 93 | listen 8080; 94 | server_name localhost; 95 | location /cb { 96 | set $couchbase_key $arg_key; 97 | set $couchbase_cmd $arg_cmd; 98 | set $couchbase_val $arg_val; 99 | couchbase_connect_timeout 2ms; 100 | couchbase_timeout 1.5ms; 101 | couchbase_pass localhost:8091,127.0.0.1:8091,localhost bucket=default; 102 | } 103 | } 104 | 105 | To deploy and check config file validness, run this commands (assuming 106 | your current directory is the root of this repository). 107 | 108 | sudo cp etc/nginx.conf /opt/nginx-couchbase/conf/nginx.conf 109 | sudo /opt/nginx-couchbase/sbin/nginx -t 110 | 111 | It should report that config is valid and everything is ok. Lets start 112 | the server and play with it using `curl`. 113 | 114 | sudo /opt/nginx-couchbase/sbin/nginx 115 | 116 | Put key into the storage 117 | 118 | $ curl -v 'http://localhost:8080/cb?cmd=set&key=foo&val=bar' 119 | * About to connect() to localhost port 8080 (#0) 120 | * Trying 127.0.0.1... 121 | * Connected to localhost (127.0.0.1) port 8080 (#0) 122 | > GET /cb?cmd=set&key=foo&val=bar HTTP/1.1 123 | > User-Agent: curl/7.29.0 124 | > Host: localhost:8080 125 | > Accept: */* 126 | > 127 | < HTTP/1.1 201 Created 128 | < Server: nginx/1.3.7 129 | < Date: Wed, 17 Apr 2013 23:47:06 GMT 130 | < Content-Length: 0 131 | < Connection: keep-alive 132 | < X-Couchbase-CAS: 16808439146359685120 133 | < 134 | * Connection #0 to host localhost left intact 135 | 136 | Fetch document back 137 | 138 | $ curl -v 'http://localhost:8080/cb?cmd=get&key=foo' 139 | * About to connect() to localhost port 8080 (#0) 140 | * Trying 127.0.0.1... 141 | * Connected to localhost (127.0.0.1) port 8080 (#0) 142 | > GET /cb?cmd=get&key=foo HTTP/1.1 143 | > User-Agent: curl/7.29.0 144 | > Host: localhost:8080 145 | > Accept: */* 146 | > 147 | < HTTP/1.1 200 OK 148 | < Server: nginx/1.3.7 149 | < Date: Wed, 17 Apr 2013 23:48:16 GMT 150 | < Content-Length: 3 151 | < Connection: keep-alive 152 | < X-Couchbase-CAS: 11078484393219129344 153 | < 154 | * Connection #0 to host localhost left intact 155 | 156 | Delete document from the storage 157 | 158 | $ curl -v 'http://localhost:8080/cb?cmd=delete&key=foo' 159 | * About to connect() to localhost port 8080 (#0) 160 | * Trying 127.0.0.1... 161 | * Connected to localhost (127.0.0.1) port 8080 (#0) 162 | > GET /cb?cmd=delete&key=foo HTTP/1.1 163 | > User-Agent: curl/7.29.0 164 | > Host: localhost:8080 165 | > Accept: */* 166 | > 167 | < HTTP/1.1 200 OK 168 | < Server: nginx/1.3.7 169 | < Date: Wed, 17 Apr 2013 23:48:49 GMT 170 | < Content-Length: 0 171 | < Connection: keep-alive 172 | < X-Couchbase-CAS: 11150541987257057280 173 | < 174 | * Connection #0 to host localhost left intact 175 | 176 | If you will skip `$couchbase_key`, `$couchbase_cmd`, `$couchbase_val` 177 | declarations it will use URL, HTTP method and HTTP body 178 | correspondingly. 179 | 180 | [1]: https://github.com/avsej/libcouchbase/tree/nginx 181 | -------------------------------------------------------------------------------- /apiary.apib: -------------------------------------------------------------------------------- 1 | HOST: http://example.com/ 2 | 3 | --- Couchbase NGINX Module --- 4 | 5 | --- 6 | This document describes the REST API exposed to access Couchbase 7 | resources 8 | 9 | There are several terms we are using while describing this API. 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 20 | 21 | 22 | 23 | 26 | 27 |
TermDefinition
documentArbitrary JSON or BLOB value. It could be referenced by 19 | its ID
bucketThe logical storage, which contains the documents. Like 24 | database in SQL world, or 25 | collection in mongodb
28 | 29 | Lets assume we have a bucket named `wonderland`. 30 | --- 31 | 32 | -- 33 | Document CRUD 34 | This section shows how to perform simple Create, Retrieve, Update and 35 | Delete operations 36 | -- 37 | 38 | Create document in the bucket 39 | POST /wonderland 40 | > Content-Type: application/json 41 | {"name":"Alice","species":"human"} 42 | < 201 43 | < ETag: "f5b8da2cf1560000" 44 | < Location: http://couchbasenginx.apiary.io/wonderland/0a4683e4b4b161f505d0c2ae0c3a1abf 45 | 46 | 47 | You can also specify ID with POST request if you need to `create` the 48 | document with given ID. 49 | POST /wonderland/alice 50 | > Content-Type: application/json 51 | {"name":"Alice","species":"human"} 52 | < 201 53 | < ETag: "f5b8da2cf1570000" 54 | < Location: http://couchbasenginx.apiary.io/wonderland/alice 55 | 56 | 57 | Note that when you are using POST request, the server is always 58 | supposed to create the new document. This means that you will get an 59 | error if the document already exists. 60 | POST /wonderland/foobar 61 | > Content-Type: application/json 62 | {"foo":"bar"} 63 | < 409 64 | < Content-Type: application/json 65 | < Content-Length: 74 66 | {"error":"key_eexists","reason":"Key exists (with a different ETag value)"} 67 | 68 | 69 | Update the document 70 | PUT /wonderland/white-rabbit 71 | > Content-Type: application/json 72 | {"name":"Write Rabbit","species":"rabbit"} 73 | < 204 74 | < ETag: "3d24e52cf1570000" 75 | < Location: http://couchbasenginx.apiary.io/wonderland/white-rabbit 76 | 77 | 78 | You can also specify `If-Match` header and the server will perform 79 | optimistic lock before updating. This operation might fail `412 80 | Precondition Failed` 81 | PUT /wonderland/duchess 82 | > Content-Type: application/json 83 | > If-Match: "wrong_etag" 84 | {"name":"Duchess","species":"human"} 85 | < 412 86 | < Content-Type: application/json 87 | < Content-Length: 74 88 | {"error":"key_eexists","reason":"Key exists (with a different ETag value)"} 89 | 90 | 91 | If the document being updated doesn't exist, you will get `404 Not 92 | Found` error 93 | PUT /wonderland/sheep 94 | {"name":"Sheep","species":"sheep"} 95 | < 404 96 | < Content-Type: application/json 97 | < Content-Length: 45 98 | {"error":"key_enoent","reason":"No such key"} 99 | 100 | 101 | Removing document from the bucket 102 | DELETE /wonderland/caterpillar 103 | < 204 104 | < ETag: "7f463c2df1570000" 105 | 106 | 107 | You can also specify `If-Match` header if you need to ensure you are removing 108 | know version of the document. 109 | DELETE /wonderland/bill-the-lizard 110 | > If-Match: "wrong_etag" 111 | < 412 112 | < Content-Type: application/json 113 | < Content-Length: 74 114 | {"error":"key_eexists","reason":"Key exists (with a different ETag value)"} 115 | 116 | 117 | If the document being removed doesn't exist, you will get `404 Not 118 | Found` error 119 | DELETE /wonderland/jabberwock 120 | < 404 121 | < Content-Type: application/json 122 | < Content-Length: 45 123 | {"error":"key_enoent","reason":"No such key"} 124 | 125 | 126 | Getting the document by ID. 127 | GET /wonderland/hatter 128 | < 200 129 | < Content-Type: application/json 130 | < Content-Length: 36 131 | < ETag: "5d45c301fc570000" 132 | {"name":"Hatter","species":"human"} 133 | 134 | 135 | In case when the given document ID isn't exist in the bucket you will 136 | get `404 Not Found` 137 | GET /wonderland/red-queen 138 | < 404 139 | < Content-Type: application/json 140 | < Content-Length: 45 141 | {"error":"key_enoent","reason":"No such key"} 142 | 143 | 144 | Use `HEAD` request to skip the document body 145 | HEAD /wonderland/gryphon 146 | < 200 147 | < Content-Type: application/json 148 | < ETag: "55c3a92df1570000" 149 | 150 | 151 | Each operation is carry ETag, which internally is CAS value. The ETag 152 | value can be used in the conditional requests with `If-Match`, 153 | `If-None-Match`. See examples of `If-Match` above. 154 | 155 | Check if the key has been modified (in this case the server responds 156 | that it wasn't modified): 157 | HEAD /wonderland/dormouse 158 | > If-None-Match: "3f5b792df1570000" 159 | < 304 160 | 161 | 162 | -- 163 | Couchbase Views 164 | This section shows more advanced server feature: View indexes 165 | -- 166 | 167 | One of the features of the Couchbase is ability to build efficient 168 | indexes leveraging Map/Reduce. They are called Views and you can 169 | define them on Admin Console UI. 170 | 171 | This module allows you to query your views proxying them to Couchbase. 172 | All arguments will be transparently passed to Couchbase and the result 173 | will be streamed back. 174 | 175 | For example we have a view `all` defined in the design document 176 | `characters`. It is simple map which will just emit all known 177 | characters (without any reduce function): 178 | 179 | function (doc, meta) { 180 | emit(meta.id, null); 181 | } 182 | 183 | The result will look like (note: when `id` isn't accessible like for 184 | reduced views, the `meta` will be missing too) 185 | GET /wonderland/_design/characters/_view/all 186 | < 200 187 | < Transfer-Encoding: chunked 188 | < Content-Type: application/json 189 | {"total_rows":20,"rows":[ 190 | {"id":"alice","key":"alice","value":null,"meta":{"etag":"f5b8da2cf1570000"}}, 191 | {"id":"bill-the-lizard","key":"bill-the-lizard","value":null,"meta":{"etag":"accb302df1570000"}}, 192 | {"id":"caterpillar","key":"caterpillar","value":null,"meta":{"etag":"7f463c2df1570000"}}, 193 | {"id":"cheshire-cat","key":"cheshire-cat","value":null,"meta":{"etag":"5b99582df1570000"}}, 194 | {"id":"dodo","key":"dodo","value":null,"meta":{"etag":"2fe2ed2cf1570000"}}, 195 | {"id":"dormouse","key":"dormouse","value":null,"meta":{"etag":"3f5b792df1570000"}}, 196 | {"id":"duchess","key":"duchess","value":null,"meta":{"etag":"a8bc492df1570000"}}, 197 | {"id":"duck","key":"duck","value":null,"meta":{"etag":"e076132df1570000"}}, 198 | {"id":"eaglet","key":"eaglet","value":null,"meta":{"etag":"b9bd062df1570000"}}, 199 | {"id":"gryphon","key":"gryphon","value":null,"meta":{"etag":"55c3a92df1570000"}}, 200 | {"id":"hatter","key":"hatter","value":null,"meta":{"etag":"5d45c301fc570000"}}, 201 | {"id":"king-of-hearts","key":"king-of-hearts","value":null,"meta":{"etag":"4e1c9f2df1570000"}}, 202 | {"id":"knave-of-hearts","key":"knave-of-hearts","value":null,"meta":{"etag":"43fa922df1570000"}}, 203 | {"id":"lory","key":"lory","value":null,"meta":{"etag":"01f6fa2cf1570000"}}, 204 | {"id":"march-hare","key":"march-hare","value":null,"meta":{"etag":"08f6642df1570000"}}, 205 | {"id":"mock-turtle","key":"mock-turtle","value":null,"meta":{"etag":"11a8b52df1570000"}}, 206 | {"id":"mouse","key":"mouse","value":null,"meta":{"etag":"fa38ea2cf1570000"}}, 207 | {"id":"pat","key":"pat","value":null,"meta":{"etag":"8bd71f2df1570000"}}, 208 | {"id":"queen-of-hearts","key":"queen-of-hearts","value":null,"meta":{"etag":"f72c862df1570000"}}, 209 | {"id":"white-rabbit","key":"white-rabbit","value":null,"meta":{"etag":"3d24e52cf1570000"}}, 210 | ] 211 | } 212 | 213 | 214 | You can pass the any of supported query parameters, like 215 | `include_docs=true` for example: 216 | GET /wonderland/_design/characters/_view/all?include_docs=true 217 | < 200 218 | < Transfer-Encoding: chunked 219 | < Content-Type: application/json 220 | {"total_rows":20,"rows":[ 221 | {"id":"alice","key":"alice","value":null,"meta":{"etag":"f5b8da2cf1570000"},"json":{"name":"Alice","species":"human"}}, 222 | {"id":"bill-the-lizard","key":"bill-the-lizard","value":null,"meta":{"etag":"accb302df1570000"},"json":{"name":"Bill the Lizard","species":"lizard"}}, 223 | {"id":"caterpillar","key":"caterpillar","value":null,"meta":{"etag":"7f463c2df1570000"},"json":{"name":"Caterpillar","species":"caterpillar"}}, 224 | {"id":"cheshire-cat","key":"cheshire-cat","value":null,"meta":{"etag":"5b99582df1570000"},"json":{"name":"Cheshire Cat","species":"cat"}}, 225 | {"id":"dodo","key":"dodo","value":null,"meta":{"etag":"2fe2ed2cf1570000"},"json":{"name":"Dodo","species":"bird"}}, 226 | {"id":"dormouse","key":"dormouse","value":null,"meta":{"etag":"3f5b792df1570000"},"json":{"name":"Dormouse","species":"dormouse"}}, 227 | {"id":"duchess","key":"duchess","value":null,"meta":{"etag":"a8bc492df1570000"},"json":{"name":"Duchess","species":"human"}}, 228 | {"id":"duck","key":"duck","value":null,"meta":{"etag":"e076132df1570000"},"json":{"name":"Duck","species":"bird"}}, 229 | {"id":"eaglet","key":"eaglet","value":null,"meta":{"etag":"b9bd062df1570000"},"json":{"name":"Eaglet","species":"bird"}}, 230 | {"id":"gryphon","key":"gryphon","value":null,"meta":{"etag":"55c3a92df1570000"},"json":{"name":"Gryphon","species":"gryphon"}}, 231 | {"id":"hatter","key":"hatter","value":null,"meta":{"etag":"5d45c301fc570000"},"json":"foo"}, 232 | {"id":"king-of-hearts","key":"king-of-hearts","value":null,"meta":{"etag":"4e1c9f2df1570000"},"json":{"name":"King of Hearts","species":"card"}}, 233 | {"id":"knave-of-hearts","key":"knave-of-hearts","value":null,"meta":{"etag":"43fa922df1570000"},"json":{"name":"Knave of Hearts","species":"card"}}, 234 | {"id":"lory","key":"lory","value":null,"meta":{"etag":"01f6fa2cf1570000"},"json":{"name":"Lory","species":"bird"}}, 235 | {"id":"march-hare","key":"march-hare","value":null,"meta":{"etag":"08f6642df1570000"},"json":{"name":"March Hare","species":"hare"}}, 236 | {"id":"mock-turtle","key":"mock-turtle","value":null,"meta":{"etag":"11a8b52df1570000"},"json":{"name":"Mock Turtle","species":"turtle"}}, 237 | {"id":"mouse","key":"mouse","value":null,"meta":{"etag":"fa38ea2cf1570000"},"json":{"name":"Mouse","species":"mouse"}}, 238 | {"id":"pat","key":"pat","value":null,"meta":{"etag":"8bd71f2df1570000"},"json":{"name":"Pat","species":"pig"}}, 239 | {"id":"queen-of-hearts","key":"queen-of-hearts","value":null,"meta":{"etag":"f72c862df1570000"},"json":{"name":"Queen of Hearts","species":"card"}}, 240 | {"id":"white-rabbit","key":"white-rabbit","value":null,"meta":{"etag":"3d24e52cf1570000"},"json":{"name":"White Rabbit","species":"rabbit"}} 241 | ] 242 | } 243 | -------------------------------------------------------------------------------- /config: -------------------------------------------------------------------------------- 1 | # vim:filetype=sh 2 | 3 | ngx_type="uint32_t" . auto/types/sizeof 4 | ngx_param=NGX_MAX_UINT32_T_VALUE; ngx_value=$ngx_max_value; . auto/types/value 5 | ngx_param=NGX_UINT32_T_LEN; ngx_value=$ngx_max_len; . auto/types/value 6 | 7 | ngx_type="uint64_t" . auto/types/sizeof 8 | ngx_param=NGX_MAX_UINT64_T_VALUE; ngx_value=$ngx_max_value; . auto/types/value 9 | ngx_param=NGX_UINT64_T_LEN; ngx_value=$ngx_max_len; . auto/types/value 10 | 11 | ngx_feature_name= 12 | ngx_feature_run=no 13 | ngx_feature_incs="#include " 14 | ngx_feature_test="lcb_get_version(NULL)" 15 | 16 | if [ -n "$LIBCOUCHBASE_INCLUDE" -o -n "$LIBCOUCHBASE_LIB" ]; then 17 | # explicit set libcouchbase lib path 18 | ngx_feature="libcouchbase library in directories specified by LIBCOUCHBASE_INCLUDE ($LIBCOUCHBASE_INCLUDE) and LIBCOUCHBASE_LIB ($LIBCOUCHBASE_LIB)" 19 | ngx_feature_path="$LIBCOUCHBASE_INCLUDE" 20 | if [ $NGX_RPATH = YES ]; then 21 | ngx_feature_libs="-R$LIBCOUCHBASE_LIB -L$LIBCOUCHBASE_LIB -lcouchbase" 22 | else 23 | ngx_feature_libs="-Wl,-rpath,$LIBCOUCHBASE_LIB -L$LIBCOUCHBASE_LIB -lcouchbase" 24 | fi 25 | . auto/feature 26 | else 27 | # autodicovery 28 | ngx_feature="libcouchbase library" 29 | ngx_feature_path= 30 | ngx_feature_libs="-lcouchbase" 31 | . auto/feature 32 | fi 33 | 34 | if [ $ngx_found = yes ]; then 35 | CORE_INCS="$CORE_INCS $ngx_feature_path" 36 | CORE_LIBS="$CORE_LIBS $ngx_feature_libs" 37 | else 38 | cat << END 39 | $0: error: the ngx_http_couchbase_module requires the libcouchbase library. 40 | END 41 | exit 1 42 | fi 43 | 44 | echo "CORE_INCS = $CORE_INCS" 45 | echo "CORE_LIBS = $CORE_LIBS" 46 | 47 | ngx_addon_name=ngx_http_couchbase_module 48 | HTTP_MODULES="$HTTP_MODULES ngx_http_couchbase_module" 49 | NGX_ADDON_SRCS="$NGX_ADDON_SRCS $ngx_addon_dir/src/ngx_lcb_plugin.c $ngx_addon_dir/src/ngx_lcb_callbacks.c $ngx_addon_dir/src/ngx_lcb_module.c" 50 | NGX_ADDON_DEPS="$NGX_ADDON_DEPS $ngx_addon_dir/src/ddebug.h $ngx_addon_dir/src/ngx_lcb_module.h $ngx_addon_dir/src/ngx_lcb_callbacks.h" 51 | 52 | have=NGX_COUCHBASE_MODULE . auto/have 53 | -------------------------------------------------------------------------------- /doc/couchbase.en.markdown: -------------------------------------------------------------------------------- 1 | # Nginx Couchbase Module (0.3.1) 2 | 3 | [Couchbase][1] client for [nginx][2]. 4 | 5 | ## Description 6 | 7 | This module translates requests to Couchbase cluster, specified with 8 | directive [couchbase_pass][3], and returns result back to the client. 9 | 10 | By default, the module can use information, accessible in the request, 11 | to construct data packet. As a key it use part of the request URI, 12 | which remains after stripping the name of the matching location. 13 | 14 | location /cache/ { 15 | couchbase_pass; 16 | } 17 | 18 | In example above, if address `/cache/example.html` will be requested 19 | from the server, then the key will be `example.html`. 20 | 21 | To choose command, which will be sent to Couchbase Server, it use HTTP 22 | method. The table below shows correspondence commands to the methods. 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 |
HTTPCouchbase
GET, HEADget
POSTadd
PUTset
DELETEdelete
31 | 32 | And, eventually, to pick the value (for mutation operations) it uses 33 | body of the HTTP request. 34 | 35 | Information, about how to override aforementioned behaviour using 36 | variables, can be found below: [set $couchbase_cmd][4], [set 37 | $couchbase_key][5], [set $couchbase_val][6]. 38 | 39 | ## Configuration Directives 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 |
syntax:couchbase_pass address bucket=value username=value password=value;
default:couchbase_pass localhost:8091 bucket=default;
severity:mandatory
context:location, if in location, limit_except
60 | 61 | Specifies connection parameters for the cluster. Parameter `address` represents comma-separated pairs `host:port`, where `:port` might be omitted in which case default port (8091) will be used: 62 | 63 | location /cache/ { 64 | couchbase_pass example.com:8091,example.org bucket=app; 65 | } 66 | 67 | * * * 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 |
syntax:couchbase_connect_timeout time;
default:couchbase_connect_timeout 2.5s;
severity:optional
context:location
88 | 89 | Sets timeout for establishing connection to Couchbase Server. 90 | 91 | * * * 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 |
syntax:couchbase_timeout time;
default:couchbase_timeout 2.5s;
severity:optional
context:location
112 | 113 | Sets timeout for data communication with Couchbase Server. 114 | 115 | ## Variables 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 |
syntax:set $couchbase_cmd command;
default:-
severity:optional
132 | 133 | Sets command type, executed for this location. For example, command 134 | can be taken from query string (`/cache/?cmd=append`): 135 | 136 | location /cache/ { 137 | set $couchbase_cmd $arg_cmd; 138 | couchbase_pass; 139 | } 140 | 141 | Or command can be fixed string: 142 | 143 | location /cache/ { 144 | set $couchbase_cmd 'append'; 145 | couchbase_pass; 146 | } 147 | 148 | As for version 0.3.1 supported the following commands: `get`, `set`, 149 | `add`, `replace`, `append`, `prepend`, `delete`. About corresponding 150 | HTTP methods to commands, read section "Description". 151 | 152 | * * * 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 |
syntax:set $couchbase_key value;
default:-
severity:optional
169 | 170 | Sets key for the document in this location. For example, key can be 171 | taken from query string (`/cache/?key=foo`): 172 | 173 | location /cache/ { 174 | set $couchbase_key $arg_key; 175 | couchbase_pass; 176 | } 177 | 178 | Or key can be fixed string: 179 | 180 | location /cache/ { 181 | set $couchbase_key 'foo'; 182 | couchbase_pass; 183 | } 184 | 185 | * * * 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 |
syntax:set $couchbase_val value;
default:-
severity:optional
202 | 203 | Picks the value for the document in this location. For example, value 204 | might be taken from query string (`/cache/?value=foo%0a`): 205 | 206 | location /cache/ { 207 | set $couchbase_val $arg_val; 208 | couchbase_pass; 209 | } 210 | 211 | Or value can be fixed string: 212 | 213 | location /cache/ { 214 | set $couchbase_val 'foo%a'; 215 | couchbase_pass; 216 | } 217 | 218 | Note, that if the value specified by `$couchbase_key`, then it will be 219 | considered URL-encoded, and will be decoded back. 220 | 221 | * * * 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 |
syntax:set $couchbase_cas value;
default:-
severity:optional
238 | 239 | This variable stores CAS value of the document. This value changes on 240 | each mutation of the document. Therefore it can be used for optimistic 241 | locking. 242 | 243 | location /cache/ { 244 | set $couchbase_key $arg_key; 245 | set $couchbase_val $arg_val; 246 | set $couchbase_cmd $arg_cmd; 247 | set $couchbase_cas $arg_cas; 248 | couchbase_pass; 249 | add_header X-CAS $couchbase_cas; 250 | } 251 | 252 | With configuration above, the server will set header 253 | X-CAS with actual CAS value. This value can be used for 254 | subsequent updates, and if someone managed to changed it before, the 255 | server will respond with code 409 Conflict, so that the 256 | client have to update local document and try again with new CAS. 257 | 258 | ## System Requirements 259 | 260 | This module has been tested with nginx v1.3.7, v1.2.6 and v1.4.0. 261 | 262 | ## Download 263 | 264 | Last version is 0.3.1: [nginx-couchbase-module-0.3.1.tar.gz][7]. 265 | 266 | Project repository: [https://github.com/couchbaselabs/couchbase-nginx-module][8] 267 | 268 | ## Usage 269 | 270 | Create build directory, download and unpack all dependencies there: 271 | 272 | mkdir nginx-couchbase 273 | cd nginx-couchbase 274 | wget http://nginx.org/download/nginx-1.3.7.tar.gz 275 | wget http://packages.couchbase.com/clients/c/libcouchbase-2.0.3nginx3.tar.gz 276 | wget http://packages.couchbase.com/clients/c/nginx-couchbase-module-0.3.1.tar.gz 277 | for i in *.tar.gz; do tar xvf $i; done 278 | 279 | The following steps describe how to install nginx with module into the 280 | directory `/opt/nginx-couchbase`: 281 | 282 | export PREFIX=/opt/nginx-couchbase 283 | 284 | cd libcouchbase-2.0.3nginx3 285 | ./configure --prefix=$PREFIX --enable-debug --disable-plugins --disable-tests --disable-couchbasemock 286 | make && sudo make install 287 | cd .. 288 | 289 | cd nginx-1.3.7 290 | export LIBCOUCHBASE_INCLUDE=$PREFIX/include 291 | export LIBCOUCHBASE_LIB=$PREFIX/lib 292 | ./configure --prefix=$PREFIX --with-debug --add-module=../nginx-couchbase-module-0.3.1 293 | make && sudo make install 294 | 295 | 296 | [1]: http://couchbase.com/download 297 | [2]: http://www.nginx.ru/ 298 | [3]: #couchbase_pass 299 | [4]: #set-couchbase_cmd 300 | [5]: #set-couchbase_key 301 | [6]: #set-couchbase_val 302 | [7]: http://packages.couchbase.com/clients/c/nginx-couchbase-module-0.3.1.tar.gz 303 | [8]: https://github.com/couchbaselabs/couchbase-nginx-module 304 | -------------------------------------------------------------------------------- /doc/couchbase.ru.markdown: -------------------------------------------------------------------------------- 1 | # Nginx Couchbase Module (0.3.1) 2 | 3 | Клиент к [Couchbase][1] для [nginx][2]. 4 | 5 | ## Описание 6 | 7 | Модуль транслирует запросы к кластеру Couchbase, заданному аргументом 8 | директивы [couchbase_pass][3], и возвращает результат обратно клиенту. 9 | 10 | По умолчанию, модуль может использовать информацию, доступную в 11 | запросе, чтобы сформировать пакет данных. В качестве ключа, 12 | используется часть URI, оставшаяся после отсечения совпавшей части 13 | location. 14 | 15 | location /cache/ { 16 | couchbase_pass; 17 | } 18 | 19 | В примере выше, если у сервера будет запрошен адрес 20 | `/cache/example.html`, то ключом будет `example.html`. 21 | 22 | Чтобы выбрать команду, оправляемую на Couchbase Server, используется 23 | HTTP method. Таблица ниже показывает соответствие команд методам. 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 |
HTTPCouchbase
GET, HEADget
POSTadd
PUTset
DELETEdelete
32 | 33 | И, наконец, для того, чтобы выбрать значение (для операций, изменяющих 34 | его) ипользуется тело HTTP запроса. 35 | 36 | Информацию о том, как можно переопределить вышеприведённое поведение с 37 | помощью переменных, можно найти ниже: [set $couchbase_cmd][4], [set 38 | $couchbase_key][5], [set $couchbase_val][6]. 39 | 40 | ## Директивы конфигурации 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 |
синтаксис:couchbase_pass address bucket=value username=value password=value;
значение по умолчанию:couchbase_pass localhost:8091 bucket=default;
строгость:обязательная
контекст:location, if in location, limit_except
61 | 62 | Задаёт параметры для подключения к кластеру. Параметр `address` 63 | представляет собой пары `host:port`, разделённые запятой, причём 64 | `:port` может быть опущен, тогда будет использован порт по умолчанию: 65 | 8091. 66 | 67 | location /cache/ { 68 | couchbase_pass example.com:8091,example.org bucket=app; 69 | } 70 | 71 | * * * 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 |
синтаксис:couchbase_connect_timeout время;
значение по умолчанию:couchbase_connect_timeout 2.5s;
строгость:необязательная
контекст:location
92 | 93 | Задаёт таймаут для установки подключения к Couchbase Server. 94 | 95 | * * * 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 |
синтаксис:couchbase_timeout время;
значение по умолчанию:couchbase_timeout 2.5s;
строгость:необязательная
контекст:location
116 | 117 | Задаёт таймаут для операций обмена данными с Couchbase Server. 118 | 119 | ## Переменные 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 |
синтаксис:set $couchbase_cmd команда;
значение по умолчанию:-
строгость:необязательная
136 | 137 | Устанавливает тип команды, выполняемой для данного location. Например, 138 | команда может быть получена из query string (`/cache/?cmd=append`): 139 | 140 | location /cache/ { 141 | set $couchbase_cmd $arg_cmd; 142 | couchbase_pass; 143 | } 144 | 145 | Или можно зафиксировать команду в виде строки: 146 | 147 | location /cache/ { 148 | set $couchbase_cmd 'append'; 149 | couchbase_pass; 150 | } 151 | 152 | Для версии 0.3.1 поддерживается следующий набор команд: `get`, `set`, 153 | `add`, `replace`, `append`, `prepend`, `delete`. Про соответствие HTTP 154 | методов командам можно прочитать выше в разделе "Описание". 155 | 156 | * * * 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 |
синтаксис:set $couchbase_key значение;
значение по умолчанию:-
строгость:необязательная
173 | 174 | Управляет выбором ключа для документа. Например, ключ может быть 175 | получен из query string (`/cache/?key=foo`): 176 | 177 | location /cache/ { 178 | set $couchbase_key $arg_key; 179 | couchbase_pass; 180 | } 181 | 182 | Или можно зафиксировать ключ в виде строки: 183 | 184 | location /cache/ { 185 | set $couchbase_key 'foo'; 186 | couchbase_pass; 187 | } 188 | 189 | * * * 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 |
синтаксис:set $couchbase_val значение;
значение по умолчанию:-
строгость:необязательная
206 | 207 | Управляет выбором значения для документа. Например, значение может быть 208 | получено из query string (`/cache/?value=foo%0a`): 209 | 210 | location /cache/ { 211 | set $couchbase_val $arg_val; 212 | couchbase_pass; 213 | } 214 | 215 | Или можно зафиксировать значение в виде строки: 216 | 217 | location /cache/ { 218 | set $couchbase_val 'foo%a'; 219 | couchbase_pass; 220 | } 221 | 222 | Обратите внимание, что если значение приходит из переменной 223 | `$couchbase_key`, оно будет переведено из URL-кодировки. 224 | 225 | * * * 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 |
синтаксис:set $couchbase_cas значение;
значение по умолчанию:-
строгость:необязательная
242 | 243 | Переменная хранит CAS значение документа. Это значение меняется при 244 | каждом изменении документа. Таким образом его можно использовать для 245 | оптимистический блокировки. 246 | 247 | location /cache/ { 248 | set $couchbase_key $arg_key; 249 | set $couchbase_val $arg_val; 250 | set $couchbase_cmd $arg_cmd; 251 | set $couchbase_cas $arg_cas; 252 | couchbase_pass; 253 | add_header X-CAS $couchbase_cas; 254 | } 255 | 256 | С такой конфигурацией для каждой операции будет установлен заголовок 257 | X-CAS со актуальным значением CAS. Это значение может 258 | быть использовано при последующем обновлении, и в случае, если кто-то 259 | успел изменить его, будет выдан код 409 Conflict, так что 260 | клиент должен будет обновить локальный документ и попробовать ещё с 261 | новым CAS. 262 | 263 | ## Системные требования 264 | 265 | Данный модуль тестировался на совместимость с версией nginx v1.3.7, 266 | v1.2.6 и v1.4.0. 267 | 268 | ## Скачать 269 | 270 | Последняя версия 0.3.1: [nginx-couchbase-module-0.3.1.tar.gz][7]. 271 | 272 | Репозиторий проекта: [https://github.com/couchbaselabs/couchbase-nginx-module][8] 273 | 274 | ## Как использовать 275 | 276 | Создать директорию для сборки, скачать и распаковать туда все 277 | зависимости: 278 | 279 | mkdir nginx-couchbase 280 | cd nginx-couchbase 281 | wget http://nginx.org/download/nginx-1.3.7.tar.gz 282 | wget http://packages.couchbase.com/clients/c/libcouchbase-2.0.3nginx3.tar.gz 283 | wget http://packages.couchbase.com/clients/c/nginx-couchbase-module-0.3.1.tar.gz 284 | for i in *.tar.gz; do tar xvf $i; done 285 | 286 | Последовательность шагов ниже установит nginx с модулем в директорию 287 | `/opt/nginx-couchbase`: 288 | 289 | export PREFIX=/opt/nginx-couchbase 290 | 291 | cd libcouchbase-2.0.3nginx3 292 | ./configure --prefix=$PREFIX --enable-debug --disable-plugins --disable-tests --disable-couchbasemock 293 | make && sudo make install 294 | cd .. 295 | 296 | cd nginx-1.3.7 297 | export LIBCOUCHBASE_INCLUDE=$PREFIX/include 298 | export LIBCOUCHBASE_LIB=$PREFIX/lib 299 | ./configure --prefix=$PREFIX --with-debug --add-module=../nginx-couchbase-module-0.3.1 300 | make && sudo make install 301 | 302 | 303 | [1]: http://couchbase.com/download 304 | [2]: http://www.nginx.ru/ 305 | [3]: #couchbase_pass 306 | [4]: #set-couchbase_cmd 307 | [5]: #set-couchbase_key 308 | [6]: #set-couchbase_val 309 | [7]: http://packages.couchbase.com/clients/c/nginx-couchbase-module-0.3.1.tar.gz 310 | [8]: https://github.com/couchbaselabs/couchbase-nginx-module 311 | -------------------------------------------------------------------------------- /etc/nginx.conf: -------------------------------------------------------------------------------- 1 | # vim:ft=nginx: 2 | 3 | worker_processes 1; 4 | # daemon off; 5 | # debug_points abort; 6 | 7 | error_log logs/error.log debug; 8 | 9 | events { 10 | worker_connections 1024; 11 | } 12 | 13 | http { 14 | upstream ruby { 15 | server localhost:9292; 16 | } 17 | 18 | server { 19 | listen 8080; 20 | server_name localhost; 21 | 22 | location /lcb { 23 | set $couchbase_key $arg_key; 24 | set $couchbase_cmd $arg_cmd; 25 | set $couchbase_val $arg_val; 26 | set $couchbase_flags $arg_flags; 27 | set $couchbase_exptime $arg_exptime; 28 | couchbase_connect_timeout 6ms; 29 | couchbase_timeout 3ms; 30 | couchbase_pass localhost:8091,127.0.0.1:8091,localhost bucket=default; 31 | add_header X-Couchbase-CAS $couchbase_cas; 32 | add_header X-Couchbase-Flags $couchbase_flags; 33 | } 34 | 35 | location /cb { 36 | internal; 37 | set $couchbase_key $arg_key; 38 | set $couchbase_cmd $arg_cmd; 39 | set $couchbase_val $arg_val; 40 | couchbase_connect_timeout 6ms; 41 | couchbase_timeout 3ms; 42 | couchbase_pass localhost:8091,127.0.0.1:8091,localhost bucket=default; 43 | } 44 | 45 | location /e { 46 | set $key $uri$args; 47 | srcache_fetch GET /cb key=$key; 48 | srcache_store PUT /cb key=$key; 49 | srcache_store_statuses 200 301 302; 50 | echo "Hello, World!\n"; 51 | } 52 | 53 | location /c { 54 | set $key $uri$args; 55 | srcache_fetch GET /cb key=$key; 56 | srcache_store PUT /cb key=$key; 57 | srcache_store_statuses 200 301 302; 58 | proxy_pass http://ruby; 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /etc/nginx.stress.conf: -------------------------------------------------------------------------------- 1 | # vim:ft=nginx: 2 | 3 | worker_processes 2; 4 | 5 | error_log logs/error.log error; 6 | 7 | events { 8 | worker_connections 1024; 9 | } 10 | 11 | http { 12 | server { 13 | listen 8080; 14 | server_name localhost; 15 | 16 | location /lcb { 17 | set $couchbase_key $arg_key; 18 | set $couchbase_cmd $arg_cmd; 19 | set $couchbase_val $arg_val; 20 | set $couchbase_flags $arg_flags; 21 | set $couchbase_exptime $arg_exptime; 22 | couchbase_connect_timeout 6ms; 23 | couchbase_timeout 3ms; 24 | couchbase_pass localhost:8091,127.0.0.1:8091,localhost bucket=default; 25 | add_header X-Couchbase-CAS $couchbase_cas; 26 | add_header X-Couchbase-Flags $couchbase_flags; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/ddebug.h: -------------------------------------------------------------------------------- 1 | #ifndef DDEBUG_H 2 | #define DDEBUG_H 3 | 4 | #include 5 | 6 | #if defined(DDEBUG) && (DDEBUG) 7 | 8 | # if (NGX_HAVE_VARIADIC_MACROS) 9 | 10 | # define dd(...) \ 11 | fprintf(stderr, "couchbase *** %s:%d ", __func__, __LINE__); \ 12 | fprintf(stderr, __VA_ARGS__); \ 13 | fprintf(stderr, "\n") 14 | 15 | # else 16 | 17 | #include 18 | #include 19 | 20 | static void dd(const char *fmt, ...) 21 | { 22 | } 23 | 24 | # endif 25 | 26 | #else 27 | 28 | # if (NGX_HAVE_VARIADIC_MACROS) 29 | 30 | # define dd(...) 31 | 32 | # else 33 | 34 | #include 35 | 36 | static void dd(const char *fmt, ...) 37 | { 38 | } 39 | 40 | # endif 41 | 42 | #endif 43 | 44 | #endif /* DDEBUG_H */ 45 | -------------------------------------------------------------------------------- /src/ngx_lcb_callbacks.c: -------------------------------------------------------------------------------- 1 | /* -*- Mode: C; tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- */ 2 | /* 3 | * Copyright 2013 Couchbase, Inc. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | #ifndef DDEBUG 19 | #define DDEBUG 0 20 | #endif 21 | #include "ddebug.h" 22 | 23 | #include "ngx_lcb_module.h" 24 | 25 | #ifndef NGX_HTTP_UNPROCESSABLE_ENTITY 26 | #define NGX_HTTP_UNPROCESSABLE_ENTITY 422 27 | #endif 28 | 29 | typedef struct ngx_lcb_error_s { 30 | lcb_error_t rc; 31 | ngx_str_t errmsg; 32 | ngx_int_t status; 33 | } ngx_lcb_error_t; 34 | 35 | static ngx_lcb_error_t ngx_lcb_errors[] = { 36 | {LCB_SUCCESS, ngx_string("success"), NGX_HTTP_OK}, 37 | {LCB_AUTH_CONTINUE, ngx_string("auth_continue"), NGX_HTTP_INTERNAL_SERVER_ERROR}, 38 | {LCB_AUTH_ERROR, ngx_string("auth_error"), NGX_HTTP_INTERNAL_SERVER_ERROR}, 39 | {LCB_DELTA_BADVAL, ngx_string("delta_badval"), NGX_HTTP_UNPROCESSABLE_ENTITY}, 40 | {LCB_E2BIG, ngx_string("e2big"), NGX_HTTP_INTERNAL_SERVER_ERROR}, 41 | {LCB_EBUSY, ngx_string("ebusy"), NGX_HTTP_INTERNAL_SERVER_ERROR}, 42 | {LCB_EINTERNAL, ngx_string("einternal"), NGX_HTTP_INTERNAL_SERVER_ERROR}, 43 | {LCB_EINVAL, ngx_string("einval"), NGX_HTTP_INTERNAL_SERVER_ERROR}, 44 | {LCB_ENOMEM, ngx_string("enomem"), NGX_HTTP_INTERNAL_SERVER_ERROR}, 45 | {LCB_ERANGE, ngx_string("erange"), NGX_HTTP_INTERNAL_SERVER_ERROR}, 46 | {LCB_ERROR, ngx_string("error"), NGX_HTTP_INTERNAL_SERVER_ERROR}, 47 | {LCB_ETMPFAIL, ngx_string("etmp_fail"), NGX_HTTP_INTERNAL_SERVER_ERROR}, 48 | {LCB_KEY_EEXISTS, ngx_string("key_eexists"), NGX_HTTP_CONFLICT}, 49 | {LCB_KEY_ENOENT, ngx_string("key_enoent"), NGX_HTTP_NOT_FOUND}, 50 | {LCB_DLOPEN_FAILED, ngx_string("dlopen_failed"), NGX_HTTP_INTERNAL_SERVER_ERROR}, 51 | {LCB_DLSYM_FAILED, ngx_string("dlsym_failed"), NGX_HTTP_INTERNAL_SERVER_ERROR}, 52 | {LCB_NETWORK_ERROR, ngx_string("network_error"), NGX_HTTP_INTERNAL_SERVER_ERROR}, 53 | {LCB_NOT_MY_VBUCKET, ngx_string("not_my_vbucket"), NGX_HTTP_INTERNAL_SERVER_ERROR}, 54 | {LCB_NOT_STORED, ngx_string("not_stored"), NGX_HTTP_UNPROCESSABLE_ENTITY}, 55 | {LCB_NOT_SUPPORTED, ngx_string("not_supported"), NGX_HTTP_INTERNAL_SERVER_ERROR}, 56 | {LCB_UNKNOWN_COMMAND, ngx_string("unknown_command"), NGX_HTTP_INTERNAL_SERVER_ERROR}, 57 | {LCB_UNKNOWN_HOST, ngx_string("unknown_host"), NGX_HTTP_INTERNAL_SERVER_ERROR}, 58 | {LCB_PROTOCOL_ERROR, ngx_string("protocol_error"), NGX_HTTP_INTERNAL_SERVER_ERROR}, 59 | {LCB_ETIMEDOUT, ngx_string("etimeout"), NGX_HTTP_REQUEST_TIME_OUT}, 60 | {LCB_CONNECT_ERROR, ngx_string("connect_error"), NGX_HTTP_INTERNAL_SERVER_ERROR}, 61 | {LCB_BUCKET_ENOENT, ngx_string("bucket_enoent"), NGX_HTTP_NOT_FOUND}, 62 | {LCB_CLIENT_ENOMEM, ngx_string("client_enomem"), NGX_HTTP_INTERNAL_SERVER_ERROR}, 63 | {LCB_CLIENT_ETMPFAIL, ngx_string("client_etmpfail"), NGX_HTTP_INTERNAL_SERVER_ERROR}, 64 | {LCB_EBADHANDLE, ngx_string("ebadhandle"), NGX_HTTP_INTERNAL_SERVER_ERROR}, 65 | {LCB_SERVER_BUG, ngx_string("server_bug"), NGX_HTTP_INTERNAL_SERVER_ERROR}, 66 | {LCB_PLUGIN_VERSION_MISMATCH, ngx_string("plugin_version_mismatch"), NGX_HTTP_INTERNAL_SERVER_ERROR}, 67 | 68 | {0, ngx_null_string, NGX_HTTP_INTERNAL_SERVER_ERROR} 69 | }; 70 | 71 | 72 | static void 73 | ngx_lcb_finalize_request(ngx_http_request_t *r, lcb_error_t rc) 74 | { 75 | if (rc == 0 76 | && !r->header_only 77 | #if (NGX_HTTP_CACHE) 78 | && !r->cached 79 | #endif 80 | ) { 81 | ngx_log_debug2(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, "will send special NGX_HTTP_LAST %s:%d", __FILE__, __LINE__); 82 | rc = ngx_http_send_special(r, NGX_HTTP_LAST); 83 | } 84 | ngx_http_finalize_request(r, rc); 85 | if (r->connection->data) { 86 | ngx_http_run_posted_requests(r->connection); 87 | } 88 | } 89 | 90 | static ngx_err_t 91 | cb_format_lcb_error(lcb_t instance, ngx_http_request_t *r, lcb_error_t rc, ngx_str_t *str) 92 | { 93 | ngx_lcb_error_t *e; 94 | const u_char *ptr; 95 | ngx_str_t error = ngx_string("unknown_error"); 96 | const u_char *reason = (const u_char *)"Unknown error code"; 97 | 98 | 99 | e = ngx_lcb_errors; 100 | r->headers_out.status = NGX_HTTP_INTERNAL_SERVER_ERROR; 101 | while (e->errmsg.data != NULL) { 102 | if (rc == e->rc) { 103 | error = e->errmsg; 104 | reason = (const u_char *)lcb_strerror(NULL, rc); 105 | r->headers_out.status = e->status; 106 | break; 107 | } 108 | e++; 109 | } 110 | 111 | str->len = error.len + ngx_strlen(reason) + 24; 112 | str->data = ngx_pnalloc(r->pool, str->len); 113 | if (str->data == NULL) { 114 | ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, 115 | "couchbase(%p): failed to allocate buffer while formatting libcouchbase error", instance); 116 | return NGX_ERROR; 117 | } 118 | ptr = ngx_sprintf(str->data, "{\"error\":\"%V\",\"reason\":\"%s\"}", &error, reason); 119 | if ((size_t)(ptr - str->data) != str->len) { 120 | ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, 121 | "couchbase(%p): failed to format libcouchbase error", instance); 122 | return NGX_ERROR; 123 | } 124 | return NGX_OK; 125 | } 126 | 127 | void 128 | ngx_lcb_configuration_callback(lcb_t instance, lcb_configuration_t config) 129 | { 130 | if (config == LCB_CONFIGURATION_NEW) { 131 | ngx_lcb_connection_t *conn; 132 | ngx_lcb_loc_conf_t *cblcf; 133 | ngx_http_request_t **r; 134 | ngx_uint_t ii; 135 | 136 | conn = (ngx_lcb_connection_t *)lcb_get_cookie(instance); 137 | conn->connected = 1; 138 | if (conn->backlog.nelts < 1) { 139 | ngx_log_error(NGX_LOG_WARN, conn->log, 0, 140 | "couchbase(%p): configuration callback without delayed responses", 141 | (void *)instance); 142 | return; 143 | } 144 | r = conn->backlog.elts; 145 | cblcf = ngx_http_get_module_loc_conf(r[0], ngx_http_couchbase_module); 146 | (void)lcb_set_timeout(instance, cblcf->timeout * 1000); /* in usec */ 147 | ngx_log_debug2(NGX_LOG_DEBUG_HTTP, r[0]->connection->log, 0, 148 | "couchbase(%p): the instance has been connected. timeout:%Mms", 149 | (void *)instance, cblcf->timeout); 150 | 151 | for (ii = 0; ii < conn->backlog.nelts; ++ii) { 152 | ngx_lcb_process(r[ii]); 153 | } 154 | conn->backlog.nelts = 0; 155 | } 156 | } 157 | 158 | ngx_err_t 159 | ngx_lcb_request_set_cas(lcb_t instance, ngx_http_request_t *r, lcb_cas_t cas) 160 | { 161 | ngx_http_variable_value_t *cas_vv; 162 | 163 | cas_vv = ngx_http_get_indexed_variable(r, ngx_lcb_cas_idx); 164 | if (cas_vv == NULL) { 165 | return NGX_ERROR; 166 | } 167 | if (cas_vv->not_found) { 168 | cas_vv->not_found = 0; 169 | cas_vv->valid = 1; 170 | cas_vv->no_cacheable = 0; 171 | } 172 | cas_vv->data = ngx_pnalloc(r->pool, NGX_UINT64_T_LEN); 173 | if (cas_vv->data == NULL) { 174 | ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, 175 | "couchbase(%p): failed to allocate buffer for $couchbase_cas variable", instance); 176 | return NGX_ERROR; 177 | } 178 | cas_vv->len = ngx_sprintf(cas_vv->data, "%uL", (uint64_t)cas) - cas_vv->data; 179 | return NGX_OK; 180 | } 181 | 182 | ngx_err_t 183 | ngx_lcb_request_set_flags(lcb_t instance, ngx_http_request_t *r, lcb_uint32_t flags) 184 | { 185 | ngx_http_variable_value_t *flags_vv; 186 | 187 | flags_vv = ngx_http_get_indexed_variable(r, ngx_lcb_flags_idx); 188 | if (flags_vv == NULL) { 189 | return NGX_ERROR; 190 | } 191 | if (flags_vv->not_found) { 192 | flags_vv->not_found = 0; 193 | flags_vv->valid = 1; 194 | flags_vv->no_cacheable = 0; 195 | } 196 | flags_vv->data = ngx_pnalloc(r->pool, NGX_UINT32_T_LEN); 197 | if (flags_vv->data == NULL) { 198 | ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, 199 | "couchbase(%p): failed to allocate buffer for $couchbase_flags variable", instance); 200 | return NGX_ERROR; 201 | } 202 | flags_vv->len = ngx_sprintf(flags_vv->data, "%ui", (ngx_uint_t)flags) - flags_vv->data; 203 | return NGX_OK; 204 | } 205 | 206 | void 207 | ngx_lcb_store_callback(lcb_t instance, const void *cookie, 208 | lcb_storage_t operation, lcb_error_t error, 209 | const lcb_store_resp_t *item) 210 | { 211 | ngx_http_request_t *r = (ngx_http_request_t *)cookie; 212 | ngx_chain_t out; 213 | ngx_buf_t *b; 214 | ngx_int_t rc; 215 | 216 | ngx_log_debug5(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, 217 | "couchbase(%p): store response \"%*s\", status: 0x%02xd, operation: 0x%02xd", 218 | (void *)instance, item->v.v0.nkey, item->v.v0.key, error, operation); 219 | 220 | b = ngx_pcalloc(r->pool, sizeof(ngx_buf_t)); 221 | if (b == NULL) { 222 | ngx_http_finalize_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR); 223 | return; 224 | } 225 | out.buf = b; 226 | out.next = NULL; 227 | b->memory = 1; 228 | b->last_buf = 1; 229 | 230 | if (error == LCB_SUCCESS) { 231 | if (ngx_lcb_request_set_cas(instance, r, item->v.v0.cas) != NGX_OK) { 232 | ngx_http_finalize_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR); 233 | return; 234 | } 235 | r->headers_out.status = NGX_HTTP_CREATED; 236 | r->headers_out.content_length_n = 0; 237 | r->header_only = 1; 238 | } else { 239 | ngx_str_t errstr; 240 | 241 | rc = cb_format_lcb_error(instance, r, error, &errstr); 242 | if (rc != NGX_OK) { 243 | ngx_http_finalize_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR); 244 | return; 245 | } 246 | b->pos = errstr.data; 247 | b->last = errstr.data + errstr.len; 248 | r->headers_out.content_length_n = errstr.len; 249 | } 250 | 251 | rc = ngx_http_send_header(r); 252 | if (rc == NGX_ERROR || rc > NGX_OK) { 253 | ngx_http_finalize_request(r, rc); 254 | return; 255 | } 256 | if (!r->header_only) { 257 | rc = ngx_http_output_filter(r, &out); 258 | } 259 | ngx_lcb_finalize_request(r, rc); 260 | } 261 | 262 | void 263 | ngx_lcb_remove_callback(lcb_t instance, const void *cookie, 264 | lcb_error_t error, const lcb_remove_resp_t *item) 265 | { 266 | ngx_http_request_t *r = (ngx_http_request_t *)cookie; 267 | ngx_chain_t out; 268 | ngx_buf_t *b; 269 | ngx_int_t rc; 270 | 271 | ngx_log_debug4(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, 272 | "couchbase(%p): remove response \"%*s\", status: 0x%02xd", 273 | (void *)instance, item->v.v0.nkey, item->v.v0.key, error); 274 | 275 | b = ngx_pcalloc(r->pool, sizeof(ngx_buf_t)); 276 | if (b == NULL) { 277 | ngx_http_finalize_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR); 278 | return; 279 | } 280 | out.buf = b; 281 | out.next = NULL; 282 | b->memory = 1; 283 | b->last_buf = 1; 284 | 285 | if (error == LCB_SUCCESS) { 286 | if (ngx_lcb_request_set_cas(instance, r, (uint64_t)item->v.v0.cas) != NGX_OK) { 287 | ngx_http_finalize_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR); 288 | return; 289 | } 290 | r->headers_out.status = NGX_HTTP_OK; 291 | r->headers_out.content_length_n = 0; 292 | r->header_only = 1; 293 | } else { 294 | ngx_str_t errstr; 295 | 296 | rc = cb_format_lcb_error(instance, r, error, &errstr); 297 | if (rc != NGX_OK) { 298 | ngx_http_finalize_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR); 299 | return; 300 | } 301 | b->pos = errstr.data; 302 | b->last = errstr.data + errstr.len; 303 | r->headers_out.content_length_n = errstr.len; 304 | } 305 | 306 | rc = ngx_http_send_header(r); 307 | if (rc == NGX_ERROR || rc > NGX_OK) { 308 | ngx_http_finalize_request(r, rc); 309 | return; 310 | } 311 | if (!r->header_only) { 312 | rc = ngx_http_output_filter(r, &out); 313 | } 314 | ngx_lcb_finalize_request(r, rc); 315 | } 316 | 317 | void 318 | ngx_lcb_get_callback(lcb_t instance, const void *cookie, lcb_error_t error, 319 | const lcb_get_resp_t *item) 320 | { 321 | ngx_http_request_t *r = (ngx_http_request_t *)cookie; 322 | ngx_chain_t out; 323 | ngx_buf_t *b; 324 | ngx_int_t rc; 325 | 326 | ngx_log_debug4(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, 327 | "couchbase(%p): get response \"%*s\", status: 0x%02xd", 328 | (void *)instance, item->v.v0.nkey, item->v.v0.key, error); 329 | 330 | b = ngx_pcalloc(r->pool, sizeof(ngx_buf_t)); 331 | if (b == NULL) { 332 | ngx_http_finalize_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR); 333 | return; 334 | } 335 | out.buf = b; 336 | out.next = NULL; 337 | b->memory = 1; 338 | b->last_buf = 1; 339 | 340 | if (error == LCB_SUCCESS) { 341 | if (ngx_lcb_request_set_cas(instance, r, (uint64_t)item->v.v0.cas) != NGX_OK) { 342 | ngx_http_finalize_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR); 343 | return; 344 | } 345 | if (ngx_lcb_request_set_flags(instance, r, item->v.v0.flags) != NGX_OK) { 346 | ngx_http_finalize_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR); 347 | return; 348 | } 349 | b->pos = (u_char *)item->v.v0.bytes; 350 | b->last = (u_char *)item->v.v0.bytes + item->v.v0.nbytes; 351 | r->headers_out.content_length_n = item->v.v0.nbytes; 352 | r->headers_out.status = NGX_HTTP_OK; 353 | } else { 354 | ngx_str_t errstr; 355 | 356 | rc = cb_format_lcb_error(instance, r, error, &errstr); 357 | if (rc != NGX_OK) { 358 | ngx_log_error(NGX_LOG_ERR, r->connection->log, rc, 359 | "couchbase: failed to format libcouchbase error 0x%02xd", rc); 360 | return; 361 | } 362 | b->pos = errstr.data; 363 | b->last = errstr.data + errstr.len; 364 | r->headers_out.content_length_n = errstr.len; 365 | } 366 | 367 | rc = ngx_http_send_header(r); 368 | if (rc == NGX_ERROR || rc > NGX_OK) { 369 | ngx_http_finalize_request(r, rc); 370 | return; 371 | } 372 | if (!r->header_only) { 373 | rc = ngx_http_output_filter(r, &out); 374 | } else { 375 | rc = NGX_DONE; 376 | } 377 | ngx_lcb_finalize_request(r, rc); 378 | } 379 | -------------------------------------------------------------------------------- /src/ngx_lcb_callbacks.h: -------------------------------------------------------------------------------- 1 | /* -*- Mode: C; tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- */ 2 | /* 3 | * Copyright 2013 Couchbase, Inc. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | #ifndef NGX_HTTP_COUCHBASE_CALLBACKS_H 19 | #define NGX_HTTP_COUCHBASE_CALLBACKS_H 20 | 21 | void ngx_lcb_get_callback(lcb_t instance, const void *cookie, lcb_error_t error, const lcb_get_resp_t *item); 22 | void ngx_lcb_remove_callback(lcb_t instance, const void *cookie, lcb_error_t error, const lcb_remove_resp_t *item); 23 | void ngx_lcb_store_callback(lcb_t instance, const void *cookie, lcb_storage_t operation, lcb_error_t error, const lcb_store_resp_t *item); 24 | void ngx_lcb_configuration_callback(lcb_t instance, lcb_configuration_t config); 25 | 26 | #endif /* NGX_HTTP_COUCHBASE_CALLBACKS_H */ 27 | 28 | -------------------------------------------------------------------------------- /src/ngx_lcb_module.c: -------------------------------------------------------------------------------- 1 | /* -*- Mode: C; tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- */ 2 | /* 3 | * Copyright 2013 Couchbase, Inc. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | #ifndef DDEBUG 19 | #define DDEBUG 0 20 | #endif 21 | #include "ddebug.h" 22 | 23 | #include "ngx_lcb_module.h" 24 | 25 | static ngx_int_t ngx_lcb_init_process(ngx_cycle_t *cycle); 26 | static void ngx_lcb_exit_process(ngx_cycle_t *cycle); 27 | static void *ngx_lcb_create_main_conf(ngx_conf_t *cf); 28 | static void *ngx_lcb_create_loc_conf(ngx_conf_t *cf); 29 | static char *ngx_lcb_merge_loc_conf(ngx_conf_t *cf, void *prev, void *conf); 30 | static char *ngx_lcb_pass(ngx_conf_t *cf, ngx_command_t *cmd, void *conf); 31 | static ngx_int_t ngx_lcb_postconf(ngx_conf_t *cf); 32 | 33 | static ngx_flag_t ngx_lcb_enabled = 0; 34 | 35 | static ngx_str_t ngx_lcb_cmd = ngx_string("couchbase_cmd"); 36 | static ngx_str_t ngx_lcb_key = ngx_string("couchbase_key"); 37 | static ngx_str_t ngx_lcb_val = ngx_string("couchbase_val"); 38 | static ngx_str_t ngx_lcb_cas = ngx_string("couchbase_cas"); 39 | static ngx_str_t ngx_lcb_flags = ngx_string("couchbase_flags"); 40 | static ngx_str_t ngx_lcb_exptime = ngx_string("couchbase_exptime"); 41 | ngx_int_t ngx_lcb_cmd_idx; 42 | ngx_int_t ngx_lcb_key_idx; 43 | ngx_int_t ngx_lcb_val_idx; 44 | ngx_int_t ngx_lcb_cas_idx; 45 | ngx_int_t ngx_lcb_flags_idx; 46 | ngx_int_t ngx_lcb_exptime_idx; 47 | 48 | enum ngx_lcb_cmd { 49 | ngx_lcb_cmd_add = 0x01, /* LCB_ADD */ 50 | ngx_lcb_cmd_replace = 0x02, /* LCB_REPLACE */ 51 | ngx_lcb_cmd_set = 0x03, /* LCB_SET */ 52 | ngx_lcb_cmd_append = 0x04, /* LCB_APPEND */ 53 | ngx_lcb_cmd_prepend = 0x05, /* LCB_PREPEND */ 54 | ngx_lcb_cmd_get, 55 | ngx_lcb_cmd_delete 56 | }; 57 | 58 | static struct ngx_lcb_cookie_s lcb_cookie; 59 | static ngx_array_t lcb_connections; /* ngx_lcb_connection_t */ 60 | static ngx_lcb_connection_t *ngx_http_get_couchbase_connection(ngx_str_t name); 61 | 62 | static ngx_command_t ngx_lcb_commands[] = { 63 | 64 | { 65 | ngx_string("couchbase_pass"), 66 | NGX_HTTP_LOC_CONF | NGX_HTTP_LIF_CONF | NGX_HTTP_LMT_CONF | NGX_CONF_TAKE1234, 67 | ngx_lcb_pass, 68 | NGX_HTTP_LOC_CONF_OFFSET, 69 | 0, 70 | NULL 71 | }, 72 | 73 | { 74 | ngx_string("couchbase_connect_timeout"), 75 | NGX_HTTP_LOC_CONF | NGX_CONF_TAKE1, 76 | ngx_conf_set_msec_slot, 77 | NGX_HTTP_LOC_CONF_OFFSET, 78 | offsetof(ngx_lcb_loc_conf_t, connect_timeout), 79 | NULL 80 | }, 81 | 82 | { 83 | ngx_string("couchbase_timeout"), 84 | NGX_HTTP_LOC_CONF | NGX_CONF_TAKE1, 85 | ngx_conf_set_msec_slot, 86 | NGX_HTTP_LOC_CONF_OFFSET, 87 | offsetof(ngx_lcb_loc_conf_t, timeout), 88 | NULL 89 | }, 90 | 91 | ngx_null_command 92 | }; 93 | 94 | static ngx_http_module_t ngx_lcb_module_ctx = { 95 | NULL, /* preconfiguration */ 96 | ngx_lcb_postconf, /* postconfiguration */ 97 | 98 | ngx_lcb_create_main_conf, /* create main configuration */ 99 | NULL, /* init main configuration */ 100 | 101 | NULL, /* create server configuration */ 102 | NULL, /* merge server configuration */ 103 | 104 | ngx_lcb_create_loc_conf, /* create location configuration */ 105 | ngx_lcb_merge_loc_conf /* merge location configuration */ 106 | }; 107 | 108 | ngx_module_t ngx_http_couchbase_module = { 109 | NGX_MODULE_V1, 110 | &ngx_lcb_module_ctx, /* module context */ 111 | ngx_lcb_commands, /* module directives */ 112 | NGX_HTTP_MODULE, /* module type */ 113 | NULL, /* init master */ 114 | NULL, /* init module */ 115 | ngx_lcb_init_process, /* init process */ 116 | NULL, /* init thread */ 117 | NULL, /* exit thread */ 118 | ngx_lcb_exit_process, /* exit process */ 119 | NULL, /* exit master */ 120 | NGX_MODULE_V1_PADDING 121 | }; 122 | 123 | static ngx_err_t 124 | ngx_lcb_atocas(u_char *line, size_t n, lcb_cas_t *cas) 125 | { 126 | lcb_cas_t value; 127 | 128 | if (n == 0 || n > NGX_UINT64_T_LEN) { 129 | return NGX_ERROR; 130 | } 131 | 132 | for (value = 0; n--; line++) { 133 | if (*line < '0' || *line > '9') { 134 | return NGX_ERROR; 135 | } 136 | 137 | value = value * 10 + (*line - '0'); 138 | } 139 | 140 | *cas = value; 141 | return NGX_OK; 142 | } 143 | 144 | ngx_int_t 145 | ngx_lcb_process(ngx_http_request_t *r) 146 | { 147 | lcb_error_t err = LCB_NOT_SUPPORTED; 148 | ngx_http_variable_value_t *vv; 149 | ngx_str_t key, val; 150 | enum ngx_lcb_cmd opcode; 151 | ngx_http_core_loc_conf_t *clcf; 152 | ngx_lcb_connection_t *conn; 153 | lcb_cas_t cas = 0; 154 | lcb_uint32_t flags = 0, exptime = 0; 155 | ngx_int_t need_free_value = 0; 156 | 157 | clcf = ngx_http_get_module_loc_conf(r, ngx_http_core_module); 158 | conn = ngx_http_get_couchbase_connection(clcf->name); 159 | if (conn == NULL) { 160 | ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, 161 | "couchbase: connection not found: \"%V\"", &clcf->name); 162 | return NGX_HTTP_INTERNAL_SERVER_ERROR; 163 | } 164 | 165 | /* setup command: use variable or fallback to HTTP method */ 166 | vv = ngx_http_get_indexed_variable(r, ngx_lcb_cmd_idx); 167 | if (vv == NULL) { 168 | ngx_http_finalize_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR); 169 | return NGX_HTTP_INTERNAL_SERVER_ERROR; 170 | } 171 | if (vv->not_found || vv->len == 0) { 172 | if (r->method & (NGX_HTTP_GET | NGX_HTTP_HEAD)) { 173 | opcode = ngx_lcb_cmd_get; 174 | } else if (r->method == NGX_HTTP_POST) { 175 | opcode = ngx_lcb_cmd_add; 176 | } else if (r->method == NGX_HTTP_PUT) { 177 | opcode = ngx_lcb_cmd_set; 178 | } else if (r->method == NGX_HTTP_DELETE) { 179 | opcode = ngx_lcb_cmd_delete; 180 | } else { 181 | ngx_log_error(NGX_LOG_ALERT, r->connection->log, 0, 182 | "ngx_memc: $memc_cmd variable requires explicit " 183 | "assignment for HTTP request method %V", 184 | &r->method_name); 185 | ngx_http_finalize_request(r, NGX_HTTP_BAD_REQUEST); 186 | return NGX_HTTP_BAD_REQUEST; 187 | } 188 | } else { 189 | if (ngx_strncmp(vv->data, "get", 3) == 0) { 190 | opcode = ngx_lcb_cmd_get; 191 | } else if (ngx_strncmp(vv->data, "add", 3) == 0) { 192 | opcode = ngx_lcb_cmd_add; 193 | } else if (ngx_strncmp(vv->data, "replace", 7) == 0) { 194 | opcode = ngx_lcb_cmd_replace; 195 | } else if (ngx_strncmp(vv->data, "set", 3) == 0) { 196 | opcode = ngx_lcb_cmd_set; 197 | } else if (ngx_strncmp(vv->data, "append", 6) == 0) { 198 | opcode = ngx_lcb_cmd_append; 199 | } else if (ngx_strncmp(vv->data, "prepend", 7) == 0) { 200 | opcode = ngx_lcb_cmd_prepend; 201 | } else if (ngx_strncmp(vv->data, "delete", 6) == 0) { 202 | opcode = ngx_lcb_cmd_delete; 203 | } else { 204 | ngx_log_error(NGX_LOG_ALERT, r->connection->log, 0, 205 | "ngx_memc: unknown $couchbase_cmd \"%v\"", vv); 206 | ngx_http_finalize_request(r, NGX_HTTP_BAD_REQUEST); 207 | return NGX_HTTP_BAD_REQUEST; 208 | } 209 | } 210 | 211 | /* setup key: use variable or fallback to URI */ 212 | vv = ngx_http_get_indexed_variable(r, ngx_lcb_key_idx); 213 | if (vv == NULL) { 214 | ngx_http_finalize_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR); 215 | return NGX_HTTP_INTERNAL_SERVER_ERROR; 216 | } 217 | if (vv->not_found || vv->len == 0) { 218 | size_t loc_len; 219 | 220 | loc_len = r->valid_location ? clcf->name.len : 0; 221 | key.data = r->uri.data + loc_len; 222 | key.len = r->uri.len - loc_len; 223 | } else { 224 | u_char *dst, *src; 225 | key.data = ngx_palloc(r->pool, vv->len); 226 | if (key.data == NULL) { 227 | ngx_http_finalize_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR); 228 | return NGX_HTTP_INTERNAL_SERVER_ERROR; 229 | } 230 | dst = key.data; 231 | src = vv->data; 232 | ngx_unescape_uri(&dst, &src, vv->len, 0); 233 | *dst = 0; 234 | key.len = dst - key.data; 235 | } 236 | 237 | /* setup value: use variable or fallback to HTTP body */ 238 | ngx_str_null(&val); 239 | if (opcode >= ngx_lcb_cmd_add && opcode <= ngx_lcb_cmd_prepend) { 240 | vv = ngx_http_get_indexed_variable(r, ngx_lcb_val_idx); 241 | if (vv == NULL) { 242 | ngx_http_finalize_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR); 243 | return NGX_HTTP_INTERNAL_SERVER_ERROR; 244 | } 245 | if (vv->not_found || vv->len == 0) { 246 | if (r->request_body == NULL || r->request_body->bufs == NULL) { 247 | ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, 248 | "neither the \"$couchbase_value\" variable " 249 | "nor the request body is available"); 250 | ngx_http_finalize_request(r, NGX_HTTP_BAD_REQUEST); 251 | return NGX_HTTP_BAD_REQUEST; 252 | } else { 253 | /* copy body to the buffer */ 254 | ngx_chain_t *cl; 255 | u_char *p; 256 | 257 | val.len = 0; 258 | for (cl = r->request_body->bufs; cl; cl = cl->next) { 259 | val.len += ngx_buf_size(cl->buf); 260 | } 261 | p = val.data = ngx_palloc(r->pool, val.len); 262 | if (p == NULL) { 263 | ngx_http_finalize_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR); 264 | return NGX_HTTP_INTERNAL_SERVER_ERROR; 265 | } 266 | for (cl = r->request_body->bufs; cl; cl = cl->next) { 267 | p = ngx_copy(p, cl->buf->pos, cl->buf->last - cl->buf->pos); 268 | } 269 | vv = NULL; 270 | } 271 | } else { 272 | u_char *dst, *src; 273 | val.data = ngx_palloc(r->pool, vv->len); 274 | if (val.data == NULL) { 275 | ngx_http_finalize_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR); 276 | return NGX_HTTP_INTERNAL_SERVER_ERROR; 277 | } 278 | need_free_value = 1; 279 | dst = val.data; 280 | src = vv->data; 281 | ngx_unescape_uri(&dst, &src, vv->len, 0); 282 | *dst = 0; 283 | val.len = dst - val.data; 284 | } 285 | } 286 | 287 | /* setup CAS value */ 288 | vv = ngx_http_get_indexed_variable(r, ngx_lcb_cas_idx); 289 | if (vv == NULL) { 290 | ngx_http_finalize_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR); 291 | return NGX_HTTP_INTERNAL_SERVER_ERROR; 292 | } 293 | if (!vv->not_found && vv->len > 0) { 294 | if (ngx_lcb_atocas(vv->data, vv->len, &cas) != NGX_OK) { 295 | ngx_http_finalize_request(r, NGX_HTTP_BAD_REQUEST); 296 | return NGX_HTTP_BAD_REQUEST; 297 | } 298 | } 299 | 300 | /* setup flags value */ 301 | vv = ngx_http_get_indexed_variable(r, ngx_lcb_flags_idx); 302 | if (vv == NULL) { 303 | ngx_http_finalize_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR); 304 | return NGX_HTTP_INTERNAL_SERVER_ERROR; 305 | } 306 | if (!vv->not_found && vv->len > 0) { 307 | flags = ngx_atoi(vv->data, vv->len); 308 | } 309 | 310 | /* setup expiration value */ 311 | vv = ngx_http_get_indexed_variable(r, ngx_lcb_exptime_idx); 312 | if (vv == NULL) { 313 | ngx_http_finalize_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR); 314 | return NGX_HTTP_INTERNAL_SERVER_ERROR; 315 | } 316 | if (!vv->not_found && vv->len > 0) { 317 | exptime = ngx_atoi(vv->data, vv->len); 318 | } 319 | 320 | switch (opcode) { 321 | case ngx_lcb_cmd_get: { 322 | lcb_get_cmd_t cmd; 323 | const lcb_get_cmd_t *commands[1]; 324 | 325 | commands[0] = &cmd; 326 | memset(&cmd, 0, sizeof(cmd)); 327 | cmd.v.v0.key = key.data; 328 | cmd.v.v0.nkey = key.len; 329 | cmd.v.v0.exptime = exptime; 330 | err = lcb_get(conn->lcb, r, 1, commands); 331 | ngx_log_debug3(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, 332 | "couchbase(%p): get request \"%*s\"", 333 | (void *)conn->lcb, cmd.v.v0.nkey, cmd.v.v0.key); 334 | } 335 | break; 336 | case ngx_lcb_cmd_add: 337 | case ngx_lcb_cmd_replace: 338 | case ngx_lcb_cmd_set: 339 | case ngx_lcb_cmd_append: 340 | case ngx_lcb_cmd_prepend: { 341 | lcb_store_cmd_t cmd; 342 | const lcb_store_cmd_t *commands[1]; 343 | 344 | commands[0] = &cmd; 345 | memset(&cmd, 0, sizeof(cmd)); 346 | 347 | cmd.v.v0.operation = opcode; 348 | cmd.v.v0.key = key.data; 349 | cmd.v.v0.nkey = key.len; 350 | cmd.v.v0.bytes = val.data; 351 | cmd.v.v0.nbytes = val.len; 352 | cmd.v.v0.cas = cas; 353 | cmd.v.v0.flags = flags; 354 | cmd.v.v0.exptime = exptime; 355 | err = lcb_store(conn->lcb, r, 1, commands); 356 | ngx_log_debug4(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, 357 | "couchbase(%p): store request \"%*s\", operation: 0x%02xd", 358 | (void *)conn->lcb, cmd.v.v0.nkey, cmd.v.v0.key, cmd.v.v0.operation); 359 | } 360 | break; 361 | case ngx_lcb_cmd_delete: { 362 | lcb_remove_cmd_t cmd; 363 | const lcb_remove_cmd_t *commands[1]; 364 | 365 | commands[0] = &cmd; 366 | memset(&cmd, 0, sizeof(cmd)); 367 | cmd.v.v0.key = key.data; 368 | cmd.v.v0.nkey = key.len; 369 | err = lcb_remove(conn->lcb, r, 1, commands); 370 | ngx_log_debug3(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, 371 | "couchbase(%p): remove request \"%*s\"", 372 | (void *)conn->lcb, cmd.v.v0.nkey, cmd.v.v0.key); 373 | } 374 | break; 375 | } 376 | if (need_free_value) { 377 | ngx_pfree(r->pool, val.data); 378 | } 379 | if (err != LCB_SUCCESS) { 380 | ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, 381 | "couchbase: failed to schedule couchbase request: %s", 382 | lcb_strerror(conn->lcb, err)); 383 | ngx_http_finalize_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR); 384 | return NGX_ERROR; 385 | } 386 | return NGX_OK; 387 | } 388 | 389 | static void 390 | ngx_lcb_upstream_init(ngx_http_request_t *r) 391 | { 392 | ngx_http_core_loc_conf_t *clcf; 393 | ngx_lcb_connection_t *conn; 394 | ngx_connection_t *c; 395 | 396 | c = r->connection; 397 | if (c->read->timer_set) { 398 | ngx_del_timer(c->read); 399 | } 400 | if (ngx_event_flags & NGX_USE_CLEAR_EVENT) { 401 | if (!c->write->active) { 402 | if (ngx_add_event(c->write, NGX_WRITE_EVENT, NGX_CLEAR_EVENT) == NGX_ERROR) { 403 | ngx_http_finalize_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR); 404 | return; 405 | } 406 | } 407 | } 408 | 409 | clcf = ngx_http_get_module_loc_conf(r, ngx_http_core_module); 410 | conn = ngx_http_get_couchbase_connection(clcf->name); 411 | if (conn == NULL) { 412 | ngx_log_error(NGX_LOG_ERR, c->log, 0, 413 | "couchbase: connection not found: \"%V\"", &clcf->name); 414 | ngx_http_finalize_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR); 415 | return; 416 | } 417 | if (conn->connected) { 418 | /* the instance has been connected */ 419 | ngx_log_debug3(NGX_LOG_DEBUG_HTTP, c->log, 0, 420 | "couchbase(%p): connection to \"%s:%s\" ready. process request", 421 | (void *)conn->lcb, lcb_get_host(conn->lcb), lcb_get_port(conn->lcb)); 422 | ngx_lcb_process(r); 423 | } else { 424 | lcb_error_t err; 425 | ngx_http_request_t **rp; 426 | 427 | rp = ngx_array_push(&conn->backlog); 428 | if (rp == NULL) { 429 | ngx_http_finalize_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR); 430 | return; 431 | } 432 | *rp = r; 433 | if (!conn->connect_scheduled) { 434 | ngx_log_debug3(NGX_LOG_DEBUG_HTTP, c->log, 0, 435 | "couchbase(%p): connecting to \"%s:%s\"", 436 | (void *)conn->lcb, lcb_get_host(conn->lcb), lcb_get_port(conn->lcb)); 437 | err = lcb_connect(conn->lcb); 438 | if (err != LCB_SUCCESS) { 439 | ngx_log_error(NGX_LOG_EMERG, c->log, 0, 440 | "couchbase(%p): failed to initiate connection: 0x%02xd \"%s\"", 441 | conn->lcb, err, lcb_strerror(NULL, err)); 442 | ngx_http_finalize_request(r, NGX_HTTP_INTERNAL_SERVER_ERROR); 443 | return; 444 | } 445 | conn->connect_scheduled = 1; 446 | } 447 | } 448 | } 449 | 450 | 451 | static ngx_int_t 452 | ngx_lcb_create_request(ngx_http_request_t *r) 453 | { 454 | ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, 455 | "couchbase: create_request"); 456 | r->upstream->request_bufs = NULL; 457 | return NGX_OK; 458 | } 459 | 460 | static ngx_int_t 461 | ngx_lcb_reinit_request(ngx_http_request_t *r) 462 | { 463 | ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, 464 | "couchbase: reinit_request"); 465 | return NGX_OK; 466 | } 467 | 468 | static void 469 | ngx_lcb_abort_request(ngx_http_request_t *r) 470 | { 471 | ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, 472 | "couchbase: abort_request"); 473 | } 474 | 475 | static void 476 | ngx_lcb_finalize_request(ngx_http_request_t *r, ngx_int_t rc) 477 | { 478 | ngx_log_debug0(NGX_LOG_DEBUG_HTTP, r->connection->log, 0, 479 | "couchbase: finalize_request"); 480 | (void)rc; 481 | } 482 | 483 | static ngx_int_t 484 | ngx_lcb_process_header(ngx_http_request_t *r) 485 | { 486 | ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, 487 | "ngx_lcb_process_header should not be called" 488 | " by the upstream"); 489 | return NGX_ERROR; 490 | } 491 | 492 | static ngx_int_t 493 | ngx_lcb_input_filter_init(void *data) 494 | { 495 | ngx_http_request_t *r = data; 496 | 497 | ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, 498 | "ngx_lcb_input_filter_init should not be called" 499 | " by the upstream"); 500 | return NGX_ERROR; 501 | } 502 | 503 | 504 | static ngx_int_t 505 | ngx_lcb_input_filter(void *data, ssize_t bytes) 506 | { 507 | ngx_http_request_t *r = data; 508 | 509 | (void)bytes; 510 | ngx_log_error(NGX_LOG_ERR, r->connection->log, 0, 511 | "ngx_lcb_input_filter should not be called" 512 | " by the upstream"); 513 | return NGX_ERROR; 514 | } 515 | 516 | static ngx_int_t 517 | ngx_lcb_handler(ngx_http_request_t *r) 518 | { 519 | ngx_int_t rc; 520 | ngx_http_upstream_t *u; 521 | ngx_lcb_loc_conf_t *llcf; 522 | 523 | llcf = ngx_http_get_module_loc_conf(r, ngx_http_couchbase_module); 524 | if (ngx_http_upstream_create(r) != NGX_OK) { 525 | return NGX_HTTP_INTERNAL_SERVER_ERROR; 526 | } 527 | 528 | u = r->upstream; 529 | u->conf = &llcf->upstream; 530 | u->schema.len = sizeof("couchbase://") - 1; 531 | u->schema.data = (u_char *) "couchbase://"; 532 | u->output.tag = (ngx_buf_tag_t) &ngx_http_couchbase_module; 533 | u->create_request = ngx_lcb_create_request; 534 | u->reinit_request = ngx_lcb_reinit_request; 535 | u->process_header = ngx_lcb_process_header; 536 | u->abort_request = ngx_lcb_abort_request; 537 | u->finalize_request = ngx_lcb_finalize_request; 538 | u->input_filter_init = ngx_lcb_input_filter_init; 539 | u->input_filter = ngx_lcb_input_filter; 540 | u->input_filter_ctx = NULL; 541 | 542 | rc = ngx_http_read_client_request_body(r, ngx_lcb_upstream_init); 543 | if (rc >= NGX_HTTP_SPECIAL_RESPONSE) { 544 | ngx_http_finalize_request(r, rc); 545 | } 546 | 547 | return NGX_DONE; 548 | } 549 | 550 | static void * 551 | ngx_lcb_create_loc_conf(ngx_conf_t *cf) 552 | { 553 | ngx_lcb_loc_conf_t *conf; 554 | 555 | conf = ngx_pcalloc(cf->pool, sizeof(ngx_lcb_loc_conf_t)); 556 | if (conf == NULL) { 557 | return NULL; 558 | } 559 | conf->connect_timeout = NGX_CONF_UNSET_MSEC; 560 | conf->timeout = NGX_CONF_UNSET_MSEC; 561 | 562 | conf->upstream.connect_timeout = NGX_CONF_UNSET_MSEC; 563 | conf->upstream.send_timeout = NGX_CONF_UNSET_MSEC; 564 | conf->upstream.read_timeout = NGX_CONF_UNSET_MSEC; 565 | conf->upstream.buffer_size = NGX_CONF_UNSET_SIZE; 566 | conf->upstream.intercept_errors = 1; 567 | conf->upstream.intercept_404 = 1; 568 | /* 569 | * set by ngx_pcalloc(): 570 | * 571 | * conf->cmds_allowed = NULL; 572 | * conf->upstream.bufs.num = 0; 573 | * conf->upstream.next_upstream = 0; 574 | * conf->upstream.temp_path = NULL; 575 | * conf->upstream.uri = { 0, NULL }; 576 | * conf->upstream.location = NULL; 577 | * conf->upstream.cyclic_temp_file = 0; 578 | * conf->upstream.buffering = 0; 579 | * conf->upstream.ignore_client_abort = 0; 580 | * conf->upstream.send_lowat = 0; 581 | * conf->upstream.bufs.num = 0; 582 | * conf->upstream.busy_buffers_size = 0; 583 | * conf->upstream.max_temp_file_size = 0; 584 | * conf->upstream.temp_file_write_size = 0; 585 | * conf->upstream.pass_request_headers = 0; 586 | * conf->upstream.pass_request_body = 0; 587 | */ 588 | 589 | return conf; 590 | } 591 | 592 | static char * 593 | ngx_lcb_merge_loc_conf(ngx_conf_t *cf, void *prev, void *conf) 594 | { 595 | ngx_lcb_loc_conf_t *parent = prev; 596 | ngx_lcb_loc_conf_t *child = conf; 597 | ngx_lcb_loc_conf_t **confp; 598 | ngx_lcb_main_conf_t *cmcf; 599 | 600 | ngx_conf_merge_msec_value(child->connect_timeout, parent->connect_timeout, 2500); 601 | ngx_conf_merge_msec_value(child->timeout, parent->timeout, 2500); 602 | ngx_conf_merge_msec_value(child->upstream.connect_timeout, 603 | parent->upstream.connect_timeout, 2500); 604 | ngx_conf_merge_msec_value(child->upstream.send_timeout, 605 | parent->upstream.send_timeout, 2500); 606 | ngx_conf_merge_msec_value(child->upstream.read_timeout, 607 | parent->upstream.read_timeout, 2500); 608 | ngx_conf_merge_size_value(child->upstream.buffer_size, 609 | parent->upstream.buffer_size, 610 | (size_t) ngx_pagesize); 611 | ngx_conf_merge_bitmask_value(child->upstream.next_upstream, 612 | parent->upstream.next_upstream, 613 | (NGX_CONF_BITMASK_SET 614 | | NGX_HTTP_UPSTREAM_FT_ERROR 615 | | NGX_HTTP_UPSTREAM_FT_TIMEOUT)); 616 | if (child->upstream.next_upstream & NGX_HTTP_UPSTREAM_FT_OFF) { 617 | child->upstream.next_upstream = NGX_CONF_BITMASK_SET 618 | | NGX_HTTP_UPSTREAM_FT_OFF; 619 | } 620 | if (child->upstream.upstream == NULL) { 621 | child->upstream.upstream = parent->upstream.upstream; 622 | } 623 | 624 | cmcf = ngx_http_conf_get_module_main_conf(cf, ngx_http_couchbase_module); 625 | if (child->name.data) { 626 | confp = ngx_array_push(&cmcf->connection_confs); 627 | *confp = child; 628 | } 629 | return NGX_CONF_OK; 630 | } 631 | 632 | /* parse couchbase_pass arguments. 633 | * full form is: 634 | * 635 | * couchbase_pass host:port bucket=val user=val password=val 636 | */ 637 | static char * 638 | ngx_lcb_scan_options(ngx_conf_t *cf, struct lcb_create_st *options) 639 | { 640 | ngx_str_t *value; 641 | size_t ii, len; 642 | char *ptr; 643 | 644 | if (cf->args->nelts < 1) { 645 | ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, 646 | "couchbase: address argument required for couchbase_pass"); 647 | return NGX_CONF_ERROR; 648 | } 649 | options->version = 0; 650 | 651 | value = cf->args->elts; 652 | ii = 1; 653 | ptr = ngx_pcalloc(cf->pool, sizeof(char) * (value[1].len + 1)); 654 | if (ptr == NULL) { 655 | goto nomem; 656 | } 657 | /* HACK nginx has special meaning for ';' therefore we are using 658 | * comma as separator for multiple bootstrap hosts. */ 659 | for (ii = 0; ii < value[1].len; ++ii) { 660 | ptr[ii] = value[1].data[ii]; 661 | if (ptr[ii] == ',') { 662 | ptr[ii] = ';'; 663 | } 664 | } 665 | options->v.v0.host = ptr; 666 | /* optional arguments */ 667 | for (ii = 2; ii < cf->args->nelts; ii++) { 668 | 669 | if (ngx_strncmp(value[ii].data, "bucket=", sizeof("bucket=") - 1) == 0) { 670 | len = value[ii].len - (sizeof("bucket=") - 1); 671 | ptr = ngx_pcalloc(cf->pool, sizeof(char) * (len + 1)); 672 | if (ptr == NULL) { 673 | goto nomem; 674 | } 675 | ngx_memcpy(ptr, &value[ii].data[sizeof("bucket=") - 1], len); 676 | options->v.v0.bucket = ptr; 677 | continue; 678 | } 679 | 680 | if (ngx_strncmp(value[ii].data, "user=", sizeof("user=") - 1) == 0) { 681 | len = value[ii].len - (sizeof("user=") - 1); 682 | ptr = ngx_pcalloc(cf->pool, sizeof(char) * (len + 1)); 683 | if (ptr == NULL) { 684 | goto nomem; 685 | } 686 | ngx_memcpy(ptr, &value[ii].data[sizeof("user=") - 1], len); 687 | options->v.v0.user = ptr; 688 | continue; 689 | } 690 | 691 | if (ngx_strncmp(value[ii].data, "password=", sizeof("password=") - 1) == 0) { 692 | len = value[ii].len - (sizeof("password=") - 1); 693 | ptr = ngx_pcalloc(cf->pool, sizeof(char) * (len + 1)); 694 | if (ptr == NULL) { 695 | goto nomem; 696 | } 697 | ngx_memcpy(ptr, &value[ii].data[sizeof("password=") - 1], len); 698 | options->v.v0.passwd = ptr; 699 | continue; 700 | } 701 | 702 | goto invalid; 703 | } 704 | return NGX_CONF_OK; 705 | 706 | nomem: 707 | ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, 708 | "couchbase: failed to allocate memory for \"%V\" in %s:%ui", &value[ii]); 709 | return NGX_CONF_ERROR; 710 | 711 | invalid: 712 | ngx_conf_log_error(NGX_LOG_EMERG, cf, 0, 713 | "couchbase: invalid parameter \"%V\"", &value[ii]); 714 | return NGX_CONF_ERROR; 715 | } 716 | 717 | 718 | static char * 719 | ngx_lcb_pass(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) 720 | { 721 | ngx_lcb_loc_conf_t *ccf = conf; 722 | ngx_http_core_loc_conf_t *clcf; 723 | char *rc; 724 | 725 | if (ccf->name.data) { 726 | return "is duplicate"; 727 | } 728 | ngx_lcb_enabled = 1; 729 | 730 | rc = ngx_lcb_scan_options(cf, &ccf->options); 731 | if (rc != NGX_CONF_OK) { 732 | return rc; 733 | } 734 | 735 | clcf = ngx_http_conf_get_module_loc_conf(cf, ngx_http_core_module); 736 | clcf->handler = ngx_lcb_handler; 737 | if (clcf->name.data[clcf->name.len - 1] == '/') { 738 | clcf->auto_redirect = 1; 739 | } 740 | ccf->name = clcf->name; 741 | 742 | (void)cmd; 743 | return NGX_CONF_OK; 744 | } 745 | 746 | static ngx_int_t 747 | ngx_lcb_variable_not_found(ngx_http_request_t *r, 748 | ngx_http_variable_value_t *v, 749 | uintptr_t data) 750 | { 751 | v->not_found = 1; 752 | (void)r; 753 | (void)data; 754 | return NGX_OK; 755 | } 756 | 757 | static ngx_int_t 758 | ngx_lcb_add_variable(ngx_conf_t *cf, ngx_str_t *name) 759 | { 760 | ngx_http_variable_t *v; 761 | 762 | v = ngx_http_add_variable(cf, name, NGX_HTTP_VAR_CHANGEABLE); 763 | if (v == NULL) { 764 | return NGX_ERROR; 765 | } 766 | 767 | v->get_handler = ngx_lcb_variable_not_found; 768 | 769 | return ngx_http_get_variable_index(cf, name); 770 | } 771 | 772 | static ngx_int_t 773 | ngx_lcb_postconf(ngx_conf_t *cf) 774 | { 775 | if (!ngx_lcb_enabled) { 776 | return NGX_OK; 777 | } 778 | 779 | ngx_lcb_cmd_idx = ngx_lcb_add_variable(cf, &ngx_lcb_cmd); 780 | if (ngx_lcb_cmd_idx == NGX_ERROR) { 781 | return NGX_ERROR; 782 | } 783 | ngx_lcb_key_idx = ngx_lcb_add_variable(cf, &ngx_lcb_key); 784 | if (ngx_lcb_key_idx == NGX_ERROR) { 785 | return NGX_ERROR; 786 | } 787 | ngx_lcb_val_idx = ngx_lcb_add_variable(cf, &ngx_lcb_val); 788 | if (ngx_lcb_val_idx == NGX_ERROR) { 789 | return NGX_ERROR; 790 | } 791 | ngx_lcb_cas_idx = ngx_lcb_add_variable(cf, &ngx_lcb_cas); 792 | if (ngx_lcb_cas_idx == NGX_ERROR) { 793 | return NGX_ERROR; 794 | } 795 | ngx_lcb_flags_idx = ngx_lcb_add_variable(cf, &ngx_lcb_flags); 796 | if (ngx_lcb_flags_idx == NGX_ERROR) { 797 | return NGX_ERROR; 798 | } 799 | ngx_lcb_exptime_idx = ngx_lcb_add_variable(cf, &ngx_lcb_exptime); 800 | if (ngx_lcb_exptime_idx == NGX_ERROR) { 801 | return NGX_ERROR; 802 | } 803 | return NGX_OK; 804 | } 805 | 806 | void 807 | ngx_lcb_error_callback(lcb_t instance, lcb_error_t error, const char *errinfo) 808 | { 809 | ngx_log_error(NGX_LOG_EMERG, lcb_cookie.log, 0, 810 | "couchbase(%p): general error: %s (0x%xd), %s", instance, 811 | lcb_strerror(instance, error), error, errinfo); 812 | exit(-1); 813 | } 814 | 815 | static ngx_int_t 816 | ngx_lcb_init_process(ngx_cycle_t *cycle) 817 | { 818 | ngx_lcb_main_conf_t *cmcf; 819 | struct lcb_create_io_ops_st options; 820 | lcb_error_t err; 821 | ngx_int_t rc; 822 | ngx_uint_t i; 823 | ngx_lcb_connection_t *conn; 824 | ngx_lcb_loc_conf_t **ccfp; 825 | 826 | /* initialize libcouchbase IO plugin */ 827 | memset(&options, 0, sizeof(options)); 828 | options.version = 2; 829 | options.v.v2.create = ngx_lcb_create_io_opts; 830 | options.v.v2.cookie = &lcb_cookie; 831 | err = lcb_create_io_ops(&lcb_cookie.io, &options); 832 | if (err != LCB_SUCCESS) { 833 | ngx_log_error(NGX_LOG_ALERT, cycle->log, 0, 834 | "couchbase: failed to create IO object for libcouchbase: 0x%02xd \"%s\"", 835 | err, lcb_strerror(NULL, err)); 836 | return NGX_ERROR; 837 | } 838 | 839 | lcb_cookie.log = cycle->log; 840 | lcb_cookie.pool = cycle->pool; 841 | 842 | /* materialize upstream connections */ 843 | rc = ngx_array_init(&lcb_connections, cycle->pool, 4, sizeof(ngx_lcb_connection_t)); 844 | if (rc != NGX_OK) { 845 | return NGX_ERROR; 846 | } 847 | cmcf = ngx_http_cycle_get_module_main_conf(cycle, ngx_http_couchbase_module); 848 | ccfp = cmcf->connection_confs.elts; 849 | for (i = 0; i < cmcf->connection_confs.nelts; i++) { 850 | struct lcb_create_st opts = ccfp[i]->options; 851 | 852 | conn = ngx_array_push(&lcb_connections); 853 | if (conn == NULL) { 854 | return NGX_ERROR; 855 | } 856 | conn->log = cycle->log; 857 | conn->name = ccfp[i]->name; 858 | rc = ngx_array_init(&conn->backlog, cycle->pool, 4, sizeof(ngx_http_request_t *)); 859 | if (rc != NGX_OK) { 860 | return NGX_ERROR; 861 | } 862 | opts.v.v0.io = lcb_cookie.io; 863 | err = lcb_create(&conn->lcb, &opts); 864 | if (err != LCB_SUCCESS) { 865 | ngx_log_error(NGX_LOG_EMERG, cycle->log, 0, 866 | "couchbase: failed to create libcouchbase instance: 0x%02xd \"%s\"", 867 | err, lcb_strerror(NULL, err)); 868 | return NGX_ERROR; 869 | } 870 | (void)lcb_set_error_callback(conn->lcb, ngx_lcb_error_callback); 871 | (void)lcb_set_timeout(conn->lcb, ccfp[i]->connect_timeout * 1000); /* in usec */ 872 | (void)lcb_set_get_callback(conn->lcb, ngx_lcb_get_callback); 873 | (void)lcb_set_store_callback(conn->lcb, ngx_lcb_store_callback); 874 | (void)lcb_set_remove_callback(conn->lcb, ngx_lcb_remove_callback); 875 | (void)lcb_set_configuration_callback(conn->lcb, ngx_lcb_configuration_callback); 876 | (void)lcb_set_cookie(conn->lcb, conn); 877 | ngx_log_debug7(NGX_LOG_DEBUG_HTTP, cycle->log, 0, 878 | "couchbase(%p): configured connection \"%V\": connect_timeout:%Mms " 879 | "address:%s bucket:%s user:%s password:%s", 880 | conn->lcb, &conn->name, ccfp[i]->connect_timeout, 881 | opts.v.v0.host ? opts.v.v0.host : "(null)", 882 | opts.v.v0.bucket ? opts.v.v0.bucket : "(null)", 883 | opts.v.v0.user ? opts.v.v0.user : "(null)", 884 | opts.v.v0.passwd ? opts.v.v0.passwd : "(null)"); 885 | } 886 | return NGX_OK; 887 | } 888 | 889 | static void 890 | ngx_lcb_exit_process(ngx_cycle_t *cycle) 891 | { 892 | lcb_destroy_io_ops(lcb_cookie.io); 893 | (void)cycle; 894 | } 895 | 896 | static void * 897 | ngx_lcb_create_main_conf(ngx_conf_t *cf) 898 | { 899 | ngx_lcb_main_conf_t *cmcf; 900 | ngx_int_t rc; 901 | 902 | cmcf = ngx_pcalloc(cf->pool, sizeof(ngx_lcb_main_conf_t)); 903 | if (cmcf == NULL) { 904 | return NULL; 905 | } 906 | 907 | rc = ngx_array_init(&cmcf->connection_confs, cf->pool, 4, 908 | sizeof(ngx_lcb_loc_conf_t *)); 909 | if (rc != NGX_OK) { 910 | return NULL; 911 | } 912 | 913 | return cmcf; 914 | } 915 | 916 | static ngx_lcb_connection_t * 917 | ngx_http_get_couchbase_connection(ngx_str_t name) 918 | { 919 | ngx_lcb_connection_t *conn; 920 | ngx_uint_t i; 921 | 922 | conn = lcb_connections.elts; 923 | for (i = 0; i < lcb_connections.nelts; i++) { 924 | if (name.len == conn[i].name.len && 925 | ngx_strncmp(name.data, conn[i].name.data, name.len) == 0) { 926 | return &conn[i]; 927 | } 928 | } 929 | 930 | return NULL; 931 | } 932 | -------------------------------------------------------------------------------- /src/ngx_lcb_module.h: -------------------------------------------------------------------------------- 1 | /* -*- Mode: C; tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- */ 2 | /* 3 | * Copyright 2013 Couchbase, Inc. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | #ifndef NGX_HTTP_COUCHBASE_MODULE_H 19 | #define NGX_HTTP_COUCHBASE_MODULE_H 20 | 21 | #include 22 | #include 23 | #include 24 | 25 | #include "ngx_lcb_callbacks.h" 26 | #include "ngx_lcb_plugin.h" 27 | 28 | typedef struct ngx_lcb_loc_conf_s { 29 | ngx_str_t name; 30 | ngx_msec_t connect_timeout; 31 | ngx_msec_t timeout; 32 | struct lcb_create_st options; 33 | ngx_http_upstream_conf_t upstream; 34 | } ngx_lcb_loc_conf_t; 35 | 36 | typedef struct { 37 | ngx_array_t connection_confs; /* ngx_lcb_loc_conf_t */ 38 | } ngx_lcb_main_conf_t; 39 | 40 | extern ngx_module_t ngx_http_couchbase_module; 41 | 42 | extern ngx_int_t ngx_lcb_cmd_idx; 43 | extern ngx_int_t ngx_lcb_key_idx; 44 | extern ngx_int_t ngx_lcb_val_idx; 45 | extern ngx_int_t ngx_lcb_cas_idx; 46 | extern ngx_int_t ngx_lcb_flags_idx; 47 | extern ngx_int_t ngx_lcb_exptime_idx; 48 | 49 | ngx_int_t ngx_lcb_process(ngx_http_request_t *r); 50 | 51 | typedef struct ngx_lcb_connection_s { 52 | ngx_str_t name; 53 | lcb_t lcb; 54 | ngx_log_t *log; 55 | ngx_array_t backlog; /* the list of postponed (ngx_http_request_t *) */ 56 | unsigned connected:1; 57 | unsigned connect_scheduled:1; 58 | } ngx_lcb_connection_t; 59 | 60 | #endif /* NGX_HTTP_COUCHBASE_MODULE_H */ 61 | 62 | -------------------------------------------------------------------------------- /src/ngx_lcb_plugin.c: -------------------------------------------------------------------------------- 1 | /* -*- Mode: C; tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- */ 2 | /* 3 | * Copyright 2013 Couchbase, Inc. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | /** 19 | * This file contains IO operations that use nginx 20 | * 21 | * @author Sergey Avseyev 22 | */ 23 | 24 | #ifndef DDEBUG 25 | #define DDEBUG 0 26 | #endif 27 | #include "ddebug.h" 28 | 29 | #include "ngx_lcb_module.h" 30 | 31 | typedef void (*ngx_lcb_handler_pt)(lcb_socket_t sock, short which, void *data); 32 | 33 | struct ngx_lcb_context_s { 34 | ngx_peer_connection_t *peer; 35 | ngx_lcb_handler_pt handler; 36 | short handler_mask; 37 | void *handler_data; 38 | struct addrinfo *root_ai; 39 | struct addrinfo *curr_ai; 40 | lcb_io_plugin_connect_cb conn_handler; 41 | void *conn_data; 42 | ngx_chain_t *io_chains; 43 | ngx_buf_t *io_bufs; 44 | lcb_uint32_t io_len; 45 | }; 46 | typedef struct ngx_lcb_context_s ngx_lcb_context_t; 47 | 48 | #define to_socket(X) ((lcb_socket_t)(intptr_t)(X)) 49 | #define from_socket(X) ((ngx_lcb_context_t *)(intptr_t)(X)) 50 | 51 | /* XXX it should use instance-level setting somehow */ 52 | static int common_getaddrinfo(const char *hostname, 53 | const char *servname, 54 | struct addrinfo **res) 55 | { 56 | struct addrinfo hints; 57 | memset(&hints, 0, sizeof(hints)); 58 | hints.ai_flags = AI_PASSIVE; 59 | hints.ai_socktype = SOCK_STREAM; 60 | hints.ai_family = AF_INET; 61 | return getaddrinfo(hostname, servname, &hints, res); 62 | } 63 | 64 | static void 65 | ngx_lcb_close(lcb_io_opt_t io, lcb_socket_t sock); 66 | 67 | /* allocate ngx_peer_connection_t struct */ 68 | static lcb_socket_t 69 | ngx_lcb_socket(lcb_io_opt_t io, const char *hostname, const char *servname) 70 | { 71 | ngx_lcb_context_t *ctx; 72 | ngx_lcb_cookie_t cookie = io->v.v0.cookie; 73 | 74 | ctx = ngx_pcalloc(cookie->pool, sizeof(ngx_lcb_context_t)); 75 | if (ctx == NULL) { 76 | return to_socket(-1); 77 | } 78 | ctx->io_len = io->v.v0.iov_max; 79 | ctx->io_chains = ngx_pcalloc(cookie->pool, sizeof(ngx_chain_t) * ctx->io_len); 80 | ctx->io_bufs = ngx_pcalloc(cookie->pool, sizeof(ngx_buf_t) * ctx->io_len); 81 | ctx->peer = ngx_pcalloc(cookie->pool, sizeof(ngx_peer_connection_t)); 82 | if (ctx->io_chains == NULL || ctx->io_bufs == NULL || ctx->peer == NULL) { 83 | ngx_lcb_close(io, to_socket(ctx)); 84 | return to_socket(-1); 85 | } 86 | ctx->peer->log = cookie->log; 87 | ctx->peer->log_error = NGX_ERROR_ERR; 88 | ctx->peer->get = ngx_event_get_peer; 89 | if (common_getaddrinfo(hostname, servname, &ctx->root_ai) != 0) { 90 | ngx_lcb_close(io, to_socket(ctx)); 91 | return to_socket(-1); 92 | } 93 | ctx->curr_ai = ctx->root_ai; 94 | 95 | return to_socket(ctx); 96 | } 97 | 98 | static void 99 | ngx_lcb_close(lcb_io_opt_t io, lcb_socket_t sock) 100 | { 101 | ngx_lcb_cookie_t cookie = io->v.v0.cookie; 102 | ngx_lcb_context_t *ctx = from_socket(sock); 103 | 104 | if (ctx->peer->connection != NULL) { 105 | ngx_close_connection(ctx->peer->connection); 106 | ctx->peer->connection = NULL; 107 | } 108 | ngx_pfree(cookie->pool, ctx->peer); 109 | ngx_pfree(cookie->pool, ctx->io_chains); 110 | ngx_pfree(cookie->pool, ctx->io_bufs); 111 | ngx_pfree(cookie->pool, ctx); 112 | } 113 | 114 | static void ngx_lcb_handler_thunk(ngx_event_t *ev) 115 | { 116 | ngx_connection_t *conn = ev->data; 117 | ngx_lcb_context_t *ctx = conn->data; 118 | int which = 0; 119 | 120 | if (ev->write) { 121 | which |= LCB_WRITE_EVENT; 122 | } else { 123 | which |= LCB_READ_EVENT; 124 | } 125 | 126 | if (ctx->handler_mask & which) { 127 | ctx->handler(to_socket(ctx), which, ctx->handler_data); 128 | } 129 | } 130 | 131 | LIBCOUCHBASE_API 132 | int ngx_lcb_connect(lcb_io_opt_t io, 133 | lcb_socket_t sock, 134 | const struct sockaddr *name, 135 | unsigned int namelen) 136 | { 137 | ngx_lcb_cookie_t cookie = io->v.v0.cookie; 138 | ngx_lcb_context_t *ctx = from_socket(sock); 139 | ngx_peer_connection_t *peer = ctx->peer; 140 | size_t len; 141 | 142 | peer->sockaddr = (struct sockaddr *)name; 143 | peer->socklen = namelen; 144 | /* FIXME free peer->name later */ 145 | peer->name = ngx_pnalloc(cookie->pool, sizeof(ngx_str_t)); 146 | if (peer->name == NULL) { 147 | return -1; 148 | } 149 | len = NGX_INET_ADDRSTRLEN + sizeof(":65535") - 1; 150 | peer->name->data = ngx_pnalloc(cookie->pool, len); 151 | if (peer->name->data == NULL) { 152 | ngx_pfree(cookie->pool, peer->name); 153 | return -1; 154 | } 155 | peer->name->len = ngx_sock_ntop(peer->sockaddr, peer->name->data, len, 1); 156 | return 0; 157 | } 158 | 159 | static void ngx_lcb_connect_handler_thunk(ngx_event_t *ev) 160 | { 161 | ngx_connection_t *conn = ev->data; 162 | ngx_lcb_context_t *ctx = conn->data; 163 | 164 | conn->read->handler = ngx_lcb_handler_thunk; 165 | conn->write->handler = ngx_lcb_handler_thunk; 166 | ctx->conn_handler(LCB_SUCCESS, ctx->conn_data); 167 | 168 | /* check for broken connection */ 169 | } 170 | 171 | LIBCOUCHBASE_API 172 | void ngx_lcb_connect_peer(lcb_io_opt_t io, 173 | lcb_socket_t sock, 174 | void *event, 175 | void *cb_data, 176 | lcb_io_plugin_connect_cb handler) 177 | { 178 | ngx_lcb_context_t *ctx = from_socket(sock); 179 | ngx_peer_connection_t *peer = ctx->peer; 180 | ngx_int_t rc; 181 | 182 | if (peer->sockaddr == NULL) { 183 | if (ngx_lcb_connect(io, sock, ctx->curr_ai->ai_addr, 184 | ctx->curr_ai->ai_addrlen) != 0) { 185 | handler(LCB_CONNECT_ERROR, cb_data); 186 | return; 187 | } 188 | } 189 | rc = ngx_event_connect_peer(peer); 190 | if (rc == NGX_ERROR || rc == NGX_BUSY || rc == NGX_DECLINED) { 191 | io->v.v0.error = ngx_socket_errno; 192 | handler(LCB_CONNECT_ERROR, cb_data); 193 | return; 194 | } 195 | peer->connection->data = ctx; 196 | if (rc == NGX_AGAIN) { 197 | peer->connection->write->handler = ngx_lcb_connect_handler_thunk; 198 | peer->connection->read->handler = ngx_lcb_connect_handler_thunk; 199 | ctx->conn_data = cb_data; 200 | ctx->conn_handler = handler; 201 | ngx_add_timer(peer->connection->write, 300); /* ms */ 202 | return; 203 | } 204 | peer->connection->read->handler = ngx_lcb_handler_thunk; 205 | peer->connection->write->handler = ngx_lcb_handler_thunk; 206 | handler(LCB_SUCCESS, cb_data); 207 | (void)event; 208 | } 209 | 210 | static lcb_ssize_t 211 | ngx_lcb_recv(lcb_io_opt_t io, lcb_socket_t sock, void *buf, lcb_size_t nbuf, int flags) 212 | { 213 | ngx_lcb_context_t *ctx = from_socket(sock); 214 | ssize_t ret; 215 | 216 | ret = ctx->peer->connection->recv(ctx->peer->connection, buf, nbuf); 217 | if (ret < 0) { 218 | io->v.v0.error = ngx_socket_errno; 219 | } 220 | 221 | (void)flags; 222 | return ret; 223 | } 224 | 225 | static ngx_chain_t * 226 | iovec2chain(ngx_lcb_context_t *ctx, struct lcb_iovec_st *iov, 227 | lcb_size_t niov, int recv) 228 | { 229 | ngx_chain_t *cc = ctx->io_chains; 230 | ngx_buf_t *bb = ctx->io_bufs; 231 | lcb_size_t ii; 232 | 233 | if (niov > ctx->io_len) { 234 | niov = ctx->io_len; 235 | } 236 | for (ii = 0; ii < niov && iov[ii].iov_len != 0; ++ii) { 237 | bb[ii].start = (u_char *)iov[ii].iov_base; 238 | bb[ii].end = (u_char *)iov[ii].iov_base + iov[ii].iov_len; 239 | bb[ii].pos = bb[ii].start; 240 | bb[ii].last = recv ? bb[ii].start : bb[ii].end; 241 | bb[ii].memory = 1; 242 | cc[ii].buf = bb + ii; 243 | cc[ii].next = cc + ii + 1; 244 | } 245 | cc[ii - 1].next = NULL; 246 | bb[ii - 1].last_buf = 1; 247 | 248 | return cc; 249 | } 250 | 251 | static lcb_ssize_t 252 | ngx_lcb_recvv(lcb_io_opt_t io, lcb_socket_t sock, struct lcb_iovec_st *iov, lcb_size_t niov) 253 | { 254 | ngx_lcb_context_t *ctx = from_socket(sock); 255 | ngx_chain_t *chain; 256 | ssize_t ret; 257 | 258 | chain = iovec2chain(ctx, iov, niov, 1); 259 | ret = ctx->peer->connection->recv_chain(ctx->peer->connection, chain); 260 | if (ret < 0) { 261 | io->v.v0.error = ngx_socket_errno; 262 | } 263 | return ret; 264 | } 265 | 266 | static lcb_ssize_t 267 | ngx_lcb_send(lcb_io_opt_t io, lcb_socket_t sock, const void *buf, lcb_size_t nbuf, int flags) 268 | { 269 | ngx_lcb_context_t *ctx = from_socket(sock); 270 | ssize_t ret; 271 | 272 | ret = ctx->peer->connection->send(ctx->peer->connection, (u_char *)buf, nbuf); 273 | if (ret < 0) { 274 | io->v.v0.error = ngx_socket_errno; 275 | } 276 | 277 | (void)flags; 278 | return ret; 279 | } 280 | 281 | static lcb_ssize_t 282 | ngx_lcb_sendv(lcb_io_opt_t io, lcb_socket_t sock, struct lcb_iovec_st *iov, lcb_size_t niov) 283 | { 284 | ngx_lcb_context_t *ctx = from_socket(sock); 285 | ngx_chain_t *chain, *rc; 286 | ssize_t old; 287 | 288 | old = ctx->peer->connection->sent; 289 | chain = iovec2chain(ctx, iov, niov, 0); 290 | rc = ctx->peer->connection->send_chain(ctx->peer->connection, chain, 0); 291 | if (rc == NGX_CHAIN_ERROR) { 292 | io->v.v0.error = ngx_socket_errno; 293 | } 294 | return ctx->peer->connection->sent - old; 295 | } 296 | 297 | static void 298 | ngx_lcb_timer_thunk(ngx_event_t *ev) 299 | { 300 | ngx_lcb_context_t *ctx = ev->data; 301 | 302 | ctx->handler(to_socket(-1), 0, ctx->handler_data); 303 | } 304 | 305 | static int 306 | ngx_lcb_timer_update(lcb_io_opt_t io, void *timer, lcb_uint32_t usec, void *data, ngx_lcb_handler_pt handler) 307 | { 308 | ngx_event_t *tm = timer; 309 | ngx_lcb_context_t *ctx; 310 | 311 | ctx = tm->data; 312 | ctx->handler = handler; 313 | ctx->handler_data = data; 314 | ngx_add_timer(tm, usec / 1000); 315 | (void)io; 316 | return 0; 317 | } 318 | 319 | void 320 | ngx_lcb_timer_delete(lcb_io_opt_t io, void *timer) 321 | { 322 | ngx_event_t *tm = timer; 323 | 324 | if (tm->timer_set) { 325 | ngx_del_timer(tm); 326 | } 327 | (void)io; 328 | } 329 | 330 | static void * 331 | ngx_lcb_timer_create(lcb_io_opt_t io) 332 | { 333 | ngx_lcb_cookie_t cookie = io->v.v0.cookie; 334 | ngx_event_t *tm; 335 | ngx_lcb_context_t *ctx; 336 | 337 | tm = ngx_pcalloc(cookie->pool, sizeof(ngx_event_t)); 338 | if (tm == NULL) { 339 | return NULL; 340 | } 341 | ctx = ngx_pcalloc(cookie->pool, sizeof(ngx_lcb_context_t)); 342 | if (ctx == NULL) { 343 | ngx_pfree(cookie->pool, tm); 344 | return NULL; 345 | } 346 | tm->handler = ngx_lcb_timer_thunk; 347 | tm->data = ctx; 348 | tm->log = cookie->log; 349 | return tm; 350 | } 351 | 352 | static void 353 | ngx_lcb_timer_destroy(lcb_io_opt_t io, void *timer) 354 | { 355 | ngx_lcb_cookie_t cookie = io->v.v0.cookie; 356 | ngx_event_t *tm = timer; 357 | 358 | ngx_pfree(cookie->pool, tm); 359 | } 360 | 361 | static int 362 | ngx_lcb_event_update(lcb_io_opt_t io, lcb_socket_t sock, void *event, short flags, void *data, ngx_lcb_handler_pt handler) 363 | { 364 | ngx_lcb_context_t *ctx = from_socket(sock); 365 | ngx_int_t f; 366 | 367 | ctx->handler = handler; 368 | ctx->handler_mask = flags; 369 | ctx->handler_data = data; 370 | 371 | if (ngx_event_flags & NGX_USE_CLEAR_EVENT) { 372 | /* kqueue */ 373 | f = NGX_CLEAR_EVENT; 374 | } else { 375 | /* select, poll, /dev/poll */ 376 | f = NGX_LEVEL_EVENT; 377 | } 378 | /* FIXME check return value */ 379 | if (flags & LCB_WRITE_EVENT) { 380 | ngx_add_event(ctx->peer->connection->write, NGX_WRITE_EVENT, f); 381 | } 382 | if (flags & LCB_READ_EVENT) { 383 | ngx_add_event(ctx->peer->connection->read, NGX_READ_EVENT, f); 384 | } 385 | 386 | (void)io; 387 | (void)event; 388 | return 0; 389 | } 390 | 391 | static void 392 | ngx_lcb_event_delete(lcb_io_opt_t io, lcb_socket_t sock, void *event) 393 | { 394 | ngx_lcb_context_t *ctx = from_socket(sock); 395 | 396 | ctx->handler = NULL; 397 | ctx->handler_mask = 0; 398 | ctx->handler_data = NULL; 399 | (void)event; 400 | (void)io; 401 | } 402 | 403 | static void * 404 | ngx_lcb_event_create_noop(lcb_io_opt_t io) 405 | { 406 | (void)io; 407 | return (void *)0xdeadbeef; 408 | } 409 | 410 | static void 411 | ngx_lcb_event_destroy_noop(lcb_io_opt_t io, void *event) 412 | { 413 | (void)io; 414 | (void)event; 415 | } 416 | 417 | static void 418 | ngx_lcb_noop(lcb_io_opt_t io) 419 | { 420 | ngx_lcb_cookie_t cookie = io->v.v0.cookie; 421 | (void)cookie; 422 | } 423 | 424 | static void 425 | ngx_lcb_destroy_io_opts(lcb_io_opt_t io) 426 | { 427 | free(io); 428 | } 429 | 430 | lcb_error_t 431 | ngx_lcb_create_io_opts(int version, lcb_io_opt_t *io, void *cookie) 432 | { 433 | lcb_io_opt_t ret; 434 | 435 | if (version != 0) { 436 | return LCB_PLUGIN_VERSION_MISMATCH; 437 | } 438 | ret = calloc(1, sizeof(*ret)); 439 | if (ret == NULL) { 440 | free(ret); 441 | return LCB_CLIENT_ENOMEM; 442 | } 443 | if (cookie == NULL) { 444 | return LCB_EINVAL; 445 | } else { 446 | ret->v.v0.cookie = cookie; 447 | } 448 | 449 | /* setup io iops! */ 450 | ret->version = 0; 451 | ret->dlhandle = NULL; 452 | ret->destructor = ngx_lcb_destroy_io_opts; 453 | /* consider that struct isn't allocated by the library, 454 | * `need_cleanup' flag might be set in lcb_create() */ 455 | ret->v.v0.need_cleanup = 0; 456 | ret->v.v0.recv = ngx_lcb_recv; 457 | ret->v.v0.send = ngx_lcb_send; 458 | ret->v.v0.recvv = ngx_lcb_recvv; 459 | ret->v.v0.sendv = ngx_lcb_sendv; 460 | ret->v.v0.socket = ngx_lcb_socket; 461 | ret->v.v0.close = ngx_lcb_close; 462 | ret->v.v0.connect = ngx_lcb_connect; 463 | ret->v.v0.connect_peer = ngx_lcb_connect_peer; 464 | 465 | ret->v.v0.delete_event = ngx_lcb_event_delete; 466 | ret->v.v0.update_event = ngx_lcb_event_update; 467 | 468 | ret->v.v0.create_timer = ngx_lcb_timer_create; 469 | ret->v.v0.destroy_timer = ngx_lcb_timer_destroy; 470 | ret->v.v0.delete_timer = ngx_lcb_timer_delete; 471 | ret->v.v0.update_timer = ngx_lcb_timer_update; 472 | 473 | ret->v.v0.create_event = ngx_lcb_event_create_noop; 474 | ret->v.v0.destroy_event = ngx_lcb_event_destroy_noop; 475 | ret->v.v0.run_event_loop = ngx_lcb_noop; 476 | ret->v.v0.stop_event_loop = ngx_lcb_noop; 477 | #if (IOV_MAX > 64) 478 | ret->v.v0.iov_max = 64; 479 | #else 480 | ret->v.v0.iov_max = IOV_MAX; 481 | #endif 482 | 483 | *io = ret; 484 | return LCB_SUCCESS; 485 | } 486 | -------------------------------------------------------------------------------- /src/ngx_lcb_plugin.h: -------------------------------------------------------------------------------- 1 | /* -*- Mode: C; tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- */ 2 | /* 3 | * Copyright 2013 Couchbase, Inc. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | #ifndef NGX_LCB_PLUGIN_H 19 | #define NGX_LCB_PLUGIN_H 1 20 | 21 | struct ngx_lcb_cookie_s { 22 | lcb_io_opt_t io; 23 | ngx_log_t *log; 24 | ngx_pool_t *pool; 25 | }; 26 | typedef struct ngx_lcb_cookie_s *ngx_lcb_cookie_t; 27 | 28 | lcb_error_t ngx_lcb_create_io_opts(int version, lcb_io_opt_t *io, void *cookie); 29 | 30 | #endif 31 | 32 | -------------------------------------------------------------------------------- /t/smoke.t: -------------------------------------------------------------------------------- 1 | # vi:filetype=perl 2 | 3 | use lib 'lib'; 4 | use Test::Nginx::Socket; 5 | 6 | $ENV{TEST_NGINX_COUCHBASE_HOST} ||= '127.0.0.1:8091'; 7 | 8 | plan tests => 68; 9 | run_tests(); 10 | 11 | __DATA__ 12 | 13 | === TEST 1: set only 14 | --- config 15 | location /cb { 16 | set $couchbase_cmd $arg_cmd; 17 | set $couchbase_key $arg_key; 18 | set $couchbase_val $arg_val; 19 | couchbase_pass $TEST_NGINX_COUCHBASE_HOST; 20 | } 21 | --- request eval 22 | my $key = "test1_" . time(); 23 | "GET /cb?cmd=set&key=$key&val=blah" 24 | --- error_code: 201 25 | --- response_body 26 | 27 | === TEST 2: add only 28 | --- config 29 | location /cb { 30 | set $couchbase_cmd $arg_cmd; 31 | set $couchbase_key $arg_key; 32 | set $couchbase_val $arg_val; 33 | couchbase_pass $TEST_NGINX_COUCHBASE_HOST; 34 | } 35 | --- request eval 36 | my $key = "test2_" . time(); 37 | "GET /cb?cmd=add&key=$key&val=blah" 38 | --- error_code: 201 39 | --- response_body 40 | 41 | === TEST 3: set and get 42 | --- config 43 | location /cb { 44 | set $couchbase_cmd $arg_cmd; 45 | set $couchbase_key $arg_key; 46 | set $couchbase_val $arg_val; 47 | couchbase_pass $TEST_NGINX_COUCHBASE_HOST; 48 | } 49 | --- request eval 50 | my $key = "test3_" . time(); 51 | [ 52 | "GET /cb?cmd=set&key=$key&val=blah", 53 | "GET /cb?cmd=get&key=$key" 54 | ] 55 | --- error_code eval 56 | [ 57 | 201, 58 | 200 59 | ] 60 | --- response_body eval 61 | [ 62 | "", 63 | "blah" 64 | ] 65 | 66 | === TEST 4: set and delete 67 | --- config 68 | location /cb { 69 | set $couchbase_cmd $arg_cmd; 70 | set $couchbase_key $arg_key; 71 | set $couchbase_val $arg_val; 72 | couchbase_pass $TEST_NGINX_COUCHBASE_HOST; 73 | } 74 | --- request eval 75 | my $key = "test4_" . time(); 76 | [ 77 | "GET /cb?cmd=set&key=$key&val=blah", 78 | "GET /cb?cmd=delete&key=$key" 79 | ] 80 | --- error_code eval 81 | [ 82 | 201, 83 | 200 84 | ] 85 | --- response_body eval 86 | [ 87 | "", 88 | "" 89 | ] 90 | 91 | === TEST 5: add after set 92 | --- config 93 | location /cb { 94 | set $couchbase_cmd $arg_cmd; 95 | set $couchbase_key $arg_key; 96 | set $couchbase_val $arg_val; 97 | couchbase_pass $TEST_NGINX_COUCHBASE_HOST; 98 | } 99 | --- request eval 100 | my $key = "test5_" . time(); 101 | [ 102 | "GET /cb?cmd=set&key=$key&val=blah", 103 | "GET /cb?cmd=add&key=$key&val=blah2", 104 | "GET /cb?cmd=get&key=$key", 105 | ] 106 | --- error_code eval 107 | [ 108 | 201, 109 | 409, 110 | 200 111 | ] 112 | --- response_body eval 113 | [ 114 | "", 115 | '{"error":"key_eexists","reason":"Key exists (with a different CAS value)"}', 116 | "blah" 117 | ] 118 | 119 | === TEST 6: handle URL encoding properly 120 | --- config 121 | location /cb { 122 | set $couchbase_cmd $arg_cmd; 123 | set $couchbase_key $arg_key; 124 | set $couchbase_val $arg_val; 125 | couchbase_pass $TEST_NGINX_COUCHBASE_HOST; 126 | } 127 | --- request eval 128 | my $key = "test6_" . time(); 129 | [ 130 | "GET /cb?cmd=set&key=$key&val=foo%20bar", 131 | "GET /cb?cmd=get&key=$key" 132 | ] 133 | --- error_code eval 134 | [ 135 | 201, 136 | 200 137 | ] 138 | --- response_body eval 139 | [ 140 | "", 141 | "foo bar" 142 | ] 143 | 144 | === TEST 7: use REST-like interface 145 | --- config 146 | location /cb { 147 | couchbase_pass $TEST_NGINX_COUCHBASE_HOST; 148 | } 149 | --- request eval 150 | my $key = "test7_" . time(); 151 | [ 152 | "POST /cb/$key\n\r\n\rhello world", 153 | "GET /cb/$key", 154 | "PUT /cb/$key\n\r\n\rHello, world!", 155 | "GET /cb/$key", 156 | "DELETE /cb/$key", 157 | ] 158 | --- error_code eval 159 | [ 160 | 201, 161 | 200, 162 | 201, 163 | 200, 164 | 200 165 | ] 166 | --- response_body eval 167 | [ 168 | "", 169 | "hello world", 170 | "", 171 | "Hello, world!", 172 | "" 173 | ] 174 | 175 | === TEST 8: returns 404 for unexisting keys 176 | --- config 177 | location /cb { 178 | set $couchbase_cmd $arg_cmd; 179 | set $couchbase_key $arg_key; 180 | set $couchbase_val $arg_val; 181 | couchbase_pass $TEST_NGINX_COUCHBASE_HOST; 182 | } 183 | --- request eval 184 | my $key = "test8_" . time(); 185 | [ 186 | "GET /cb/$key", 187 | "GET /cb/cmd=get&key=$key" 188 | ] 189 | --- error_code eval 190 | [ 191 | 404, 192 | 404 193 | ] 194 | --- response_body eval 195 | [ 196 | '{"error":"key_enoent","reason":"No such key"}', 197 | '{"error":"key_enoent","reason":"No such key"}' 198 | ] 199 | 200 | === TEST 9: strip location name in the URI key 201 | --- config 202 | location /cb/ { 203 | set $couchbase_cmd $arg_cmd; 204 | set $couchbase_key $arg_key; 205 | set $couchbase_val $arg_val; 206 | couchbase_pass $TEST_NGINX_COUCHBASE_HOST; 207 | } 208 | --- request eval 209 | my $key = "test9_" . time(); 210 | [ 211 | "GET /cb/?cmd=set&key=$key&val=value", 212 | "GET /cb/$key" 213 | ] 214 | --- error_code eval 215 | [ 216 | 201, 217 | 200 218 | ] 219 | --- response_body eval 220 | [ 221 | "", 222 | "value" 223 | ] 224 | 225 | === TEST 10: replace only 226 | --- config 227 | location /cb { 228 | set $couchbase_cmd $arg_cmd; 229 | set $couchbase_key $arg_key; 230 | set $couchbase_val $arg_val; 231 | couchbase_pass $TEST_NGINX_COUCHBASE_HOST; 232 | } 233 | --- request eval 234 | my $key = "test10_" . time(); 235 | "GET /cb?cmd=replace&key=$key&val=blah" 236 | --- error_code: 404 237 | --- response_body eval 238 | '{"error":"key_enoent","reason":"No such key"}' 239 | 240 | === TEST 11: replace after set 241 | --- config 242 | location /cb { 243 | set $couchbase_cmd $arg_cmd; 244 | set $couchbase_key $arg_key; 245 | set $couchbase_val $arg_val; 246 | couchbase_pass $TEST_NGINX_COUCHBASE_HOST; 247 | } 248 | --- request eval 249 | my $key = "test11_" . time(); 250 | [ 251 | "GET /cb?cmd=set&key=$key&val=blah", 252 | "GET /cb?cmd=replace&key=$key&val=blah2", 253 | "GET /cb?cmd=get&key=$key", 254 | ] 255 | --- error_code eval 256 | [ 257 | 201, 258 | 201, 259 | 200 260 | ] 261 | --- response_body eval 262 | [ 263 | "", 264 | "", 265 | "blah2" 266 | ] 267 | 268 | === TEST 12: append 269 | --- config 270 | location /cb { 271 | set $couchbase_cmd $arg_cmd; 272 | set $couchbase_key $arg_key; 273 | set $couchbase_val $arg_val; 274 | couchbase_pass $TEST_NGINX_COUCHBASE_HOST; 275 | } 276 | --- request eval 277 | my $key = "test12_" . time(); 278 | [ 279 | "GET /cb?cmd=set&key=$key&val=aaa", 280 | "GET /cb?cmd=append&key=$key&val=bbb", 281 | "GET /cb?cmd=get&key=$key", 282 | ] 283 | --- error_code eval 284 | [ 285 | 201, 286 | 201, 287 | 200 288 | ] 289 | --- response_body eval 290 | [ 291 | "", 292 | "", 293 | "aaabbb" 294 | ] 295 | 296 | === TEST 13: prepend 297 | --- config 298 | location /cb { 299 | set $couchbase_cmd $arg_cmd; 300 | set $couchbase_key $arg_key; 301 | set $couchbase_val $arg_val; 302 | couchbase_pass $TEST_NGINX_COUCHBASE_HOST; 303 | } 304 | --- request eval 305 | my $key = "test5_" . time(); 306 | [ 307 | "GET /cb?cmd=set&key=$key&val=aaa", 308 | "GET /cb?cmd=prepend&key=$key&val=bbb", 309 | "GET /cb?cmd=get&key=$key", 310 | ] 311 | --- error_code eval 312 | [ 313 | 201, 314 | 201, 315 | 200 316 | ] 317 | --- response_body eval 318 | [ 319 | "", 320 | "", 321 | "bbbaaa" 322 | ] 323 | 324 | === TEST 14: it sets header with CAS 325 | --- skip_nginx 326 | 2: < 1.3.10 327 | --- config 328 | location /cb { 329 | set $couchbase_cmd $arg_cmd; 330 | set $couchbase_key $arg_key; 331 | set $couchbase_val $arg_val; 332 | couchbase_pass $TEST_NGINX_COUCHBASE_HOST; 333 | add_header X-CAS $couchbase_cas; 334 | } 335 | --- request eval 336 | my $key = "test14_" . time(); 337 | "GET /cb?cmd=set&key=$key&val=blah" 338 | --- error_code: 201 339 | --- response_headers_like 340 | X-CAS: \d+ 341 | 342 | 343 | === TEST 15: it can be used to cache stuff inside nginx 344 | --- config 345 | location /cb { 346 | internal; 347 | set $couchbase_cmd $arg_cmd; 348 | set $couchbase_key $arg_key; 349 | set $couchbase_val $arg_val; 350 | couchbase_pass $TEST_NGINX_COUCHBASE_HOST; 351 | } 352 | location /cached { 353 | set $key $uri$args; 354 | srcache_fetch GET /cb key=$key; 355 | srcache_store PUT /cb key=$key; 356 | srcache_store_statuses 200 301 302; 357 | echo "Hello, World!\n"; 358 | } 359 | --- request eval 360 | my $key = "test15_" . time(); 361 | "GET /cached/$key" 362 | --- error_code: 200 363 | 364 | 365 | === TEST 16: it sets flags 366 | --- config 367 | location /cb { 368 | set $couchbase_cmd $arg_cmd; 369 | set $couchbase_key $arg_key; 370 | set $couchbase_val $arg_val; 371 | set $couchbase_flags $arg_flags; 372 | couchbase_pass $TEST_NGINX_COUCHBASE_HOST; 373 | add_header X-Flags $couchbase_flags; 374 | } 375 | --- request eval 376 | my $key = "test16_" . time(); 377 | [ 378 | "GET /cb?cmd=set&key=$key&val=blah&flags=3735928559", 379 | "GET /cb?cmd=get&key=$key" 380 | ] 381 | --- error_code eval 382 | [ 383 | 201, 384 | 200 385 | ] 386 | --- response_body eval 387 | [ 388 | "", 389 | "blah" 390 | ] 391 | --- response_headers_like eval 392 | [ 393 | "", 394 | "X-Flags: 3735928559" 395 | ] 396 | --------------------------------------------------------------------------------