Commit 0dd910f6 authored by Poul-Henning Kamp's avatar Poul-Henning Kamp

Having thought long and hard about this, commit what I think is the
new and simpler flow for version2.

Pass is now handled like a miss where the object will not be cached.

The main result of this is that we drag the entire object, header
and body, from the backend before transmitting it to the client,
thus isolating the backend from slow clients.

From a software engineering point of view it is a big improvement,
because it eliminates the need for all of cache_pass.c and we therefore
end up with less HTTP protocol implementations.

A side effect of this is that ticket #56 should be fixed now.

If the object is pass'ed before vcl_fetch{} that is, in vcl_recv{},
vcl_hit{} or vcl_miss{}, no "pass this" object is inserted in the
cache.  The confusion between "pass", "insert" and "insert_pass"
has been cleaned up, by the removal of the latter.

Pipe and Pass calls vcl_pipe{} and vcl_pass{} respectively, before
contacting the backend.  I havn't quite decided if they should
operate on the request header from the client or the one to the
backend, or both.

One possible use is to inject a "Connection: close" header to limit
pipe to one transaction.

A new vcl_hash{} has been added, it will allow customization of
which fields we hash on, instead of the default "url + Host:" but
this is not yet implemented.

vcl_fetch{} is now called after both the headers and body have been
picked up from the backend.  This will allow us to do more comprehensive
handling of backend errors later on.

A disadvantage to this is that if the object ends up as a "pass
this" object in the cache, we could possibly have released any
queued requests already after the headers were received.  If this
is transpires as a real-world problem, we can add a vcl_fetchhdr{}
which can do an early release (ie: "pass").



git-svn-id: http://www.varnish-cache.org/svn/trunk/varnish-cache@1277 d4fa192b-c00b-0410-8231-f00ffab90ce4
parent cd96be74
......@@ -21,7 +21,6 @@ varnishd_SOURCES = \
cache_http.c \
cache_main.c \
cache_pool.c \
cache_pass.c \
cache_pipe.c \
cache_response.c \
cache_session.c \
......
......@@ -279,7 +279,6 @@ struct sess {
TAILQ_ENTRY(sess) list;
struct vbe_conn *vbc;
struct backend *backend;
struct object *obj;
struct VCL_conf *vcl;
......@@ -348,10 +347,10 @@ void EXP_Init(void);
void EXP_TTLchange(struct object *o);
/* cache_fetch.c */
int FetchBody(struct sess *sp);
int FetchHeaders(struct sess *sp);
int Fetch(struct sess *sp);
/* cache_hash.c */
void HSH_Prealloc(struct sess *sp);
struct object *HSH_Lookup(struct sess *sp);
void HSH_Unbusy(struct object *o);
void HSH_Ref(struct object *o);
......@@ -390,10 +389,6 @@ void http_DoConnection(struct sess *sp);
#include "http_headers.h"
#undef HTTPH
/* cache_pass.c */
int PassSession(struct sess *sp);
void PassBody(struct sess *sp);
/* cache_pipe.c */
void PipeSession(struct sess *sp);
......
......@@ -44,14 +44,24 @@
/*
DOT digraph vcl_center {
DOT page="8.2,11.7"
DOT size="6.3,9.7"
DOT margin="1.0"
xDOT page="8.2,11.5"
DOT size="7.2,10.5"
DOT margin="0.5"
DOT center="1"
DOT start [
DOT shape=hexagon
DOT label="Start"
DOT label="Request received"
DOT ]
DOT start -> RECV
DOT RECV [shape=plaintext]
DOT PIPE [shape=plaintext]
DOT LOOKUP [shape=plaintext]
DOT HIT [shape=plaintext]
DOT MISS [shape=plaintext]
DOT PASS [shape=plaintext]
DOT FETCH [shape=plaintext]
DOT DELIVER [shape=plaintext]
DOT ERROR [shape=plaintext]
DOT start -> RECV [style=bold,color=green,weight=4]
*/
#include <stdio.h>
......@@ -71,8 +81,13 @@ DOT start -> RECV
static unsigned xids;
/*--------------------------------------------------------------------
* The very first request
* AGAIN
* We come here when we just completed a request and already have
* received (part of) the next one. Instead taking the detour
* around the acceptor and then back to a worker, just stay in this
* worker and do what it takes.
*/
static int
cnt_again(struct sess *sp)
{
......@@ -101,19 +116,19 @@ cnt_again(struct sess *sp)
/*--------------------------------------------------------------------
* We have a refcounted object on the session, now deliver it.
*
DOT subgraph cluster_deliver {
DOT subgraph xcluster_deliver {
DOT deliver [
DOT shape=ellipse
DOT label="Build & send header"
DOT ]
DOT DELIVER -> deliver [style=bold]
DOT DELIVER -> deliver [style=bold,color=green,weight=4]
DOT deliver2 [
DOT shape=ellipse
DOT label="Send object"
DOT ]
DOT deliver -> deliver2 [style=bold]
DOT deliver -> deliver2 [style=bold,color=green,weight=4]
DOT }
DOT deliver2 -> DONE [style=bold]
DOT deliver2 -> DONE [style=bold,color=green,weight=4]
*/
static int
......@@ -154,7 +169,6 @@ cnt_done(struct sess *sp)
double dh, dp, da;
AZ(sp->obj);
AZ(sp->vbc);
sp->backend = NULL;
if (sp->vcl != NULL) {
if (sp->wrk->vcl != NULL)
......@@ -211,7 +225,7 @@ cnt_done(struct sess *sp)
/*--------------------------------------------------------------------
* Emit an error
*
DOT subgraph cluster_error {
DOT subgraph xcluster_error {
DOT error [
DOT shape=ellipse
DOT label="Issue HTTP error"
......@@ -237,99 +251,61 @@ cnt_error(struct sess *sp)
* We have fetched the headers from the backend, ask the VCL code what
* to do next, then head off in that direction.
*
DOT subgraph cluster_fetch {
DOT subgraph xcluster_fetch {
DOT fetch [
DOT shape=ellipse
DOT label="find obj.ttl\nobj.cacheable"
DOT label="fetch from backend\n(find obj.ttl)"
DOT ]
DOT FETCH -> fetch [style=bold]
DOT FETCH -> fetch [style=bold,color=blue,weight=2]
DOT vcl_fetch [
DOT shape=box
DOT label="vcl_fetch()"
DOT ]
DOT fetch -> vcl_fetch [style=bold]
DOT fetch_lookup [
DOT shape=ellipse
DOT label="obj.cacheable=false\nunbusy obj\ndiscard body\n"
DOT ]
DOT vcl_fetch -> fetch_lookup [label="lookup", style=dotted, weight=0]
DOT fetch -> vcl_fetch [style=bold,color=blue,weight=2]
DOT fetch_pass [
DOT shape=ellipse
DOT label="obj.cacheable=false\nunbusy obj"
DOT label="obj.pass=true"
DOT ]
DOT vcl_fetch -> fetch_pass [label="pass"]
DOT fetch_ipass [
DOT shape=ellipse
DOT label="obj.cacheable=true\nobj.pass=true\nunbusy obj"
DOT ]
DOT vcl_fetch -> fetch_ipass [label="insert_pass"]
DOT fetch_insert [
DOT shape=ellipse
DOT label="rcv body\nobj.cacheable=true\nunbusy"
DOT ]
DOT vcl_fetch -> fetch_insert [label="insert", style=bold]
DOT fetch_error [
DOT shape=ellipse
DOT label="disc body\nobj.cacheable=false\nunbusy"
DOT ]
DOT vcl_fetch -> fetch_error [label="error"]
DOT }
DOT fetch_lookup -> LOOKUP [style=dotted, weight=0]
DOT fetch_pass -> PASSBODY
DOT fetch_ipass -> PASSBODY
DOT fetch_insert -> DELIVER [style=bold]
DOT fetch_error -> ERROR
DOT fetch_pass -> DELIVER
DOT vcl_fetch -> DELIVER [label="insert",style=bold,color=blue,weight=2]
DOT vcl_fetch -> errfetch [label="error"]
DOT errfetch [label="ERROR",shape=plaintext]
*/
static int
cnt_fetch(struct sess *sp)
{
CHECK_OBJ_NOTNULL(sp->vbc, VBE_CONN_MAGIC);
RFC2616_cache_policy(sp, sp->vbc->http);
VCL_fetch_method(sp);
if (sp->handling == VCL_RET_LOOKUP)
INCOMPL();
if (sp->handling == VCL_RET_PASS) {
sp->obj->cacheable = 0;
HSH_Unbusy(sp->obj);
HSH_Deref(sp->obj);
sp->obj = NULL;
sp->step = STP_PASSBODY;
return (0);
}
if (sp->handling == VCL_RET_INSERT_PASS) {
sp->obj->pass = 1;
sp->obj->cacheable = 1;
HSH_Unbusy(sp->obj);
/* Don't HSH_Deref(sp->obj); we need the ref for storage */
sp->obj = NULL;
sp->step = STP_PASSBODY;
return (0);
}
if (sp->handling == VCL_RET_INSERT) {
if (FetchBody(sp)) {
if (Fetch(sp)) {
sp->obj->cacheable = 0;
HSH_Unbusy(sp->obj);
HSH_Deref(sp->obj);
sp->obj = NULL;
RES_Error(sp, 503, NULL);
sp->step = STP_DONE;
RES_Error(sp, 503, NULL);
return (0);
}
RFC2616_cache_policy(sp, &sp->obj->http); /* XXX -> VCL */
VCL_fetch_method(sp);
if (sp->handling == VCL_RET_ERROR)
INCOMPL();
if (sp->handling == VCL_RET_PASS)
sp->obj->pass = 1;
sp->obj->cacheable = 1;
AZ(sp->vbc);
HSH_Ref(sp->obj); /* get another, STP_DELIVER will deref */
if (sp->obj->objhead != NULL)
HSH_Unbusy(sp->obj);
sp->wrk->acct.fetch++;
sp->step = STP_DELIVER;
return (0);
}
if (sp->handling == VCL_RET_ERROR)
INCOMPL();
INCOMPL();
}
/*--------------------------------------------------------------------
......@@ -370,120 +346,96 @@ cnt_first(struct sess *sp)
}
/*--------------------------------------------------------------------
* HIT
* We had a cache hit. Ask VCL, then march off as instructed.
*
DOT subgraph cluster_hit {
DOT subgraph xcluster_hit {
DOT hit [
DOT shape=box
DOT label="vcl_hit()"
DOT ]
DOT HIT -> hit [style=bold]
DOT hit2 [
DOT shape=diamond
DOT label="obj.pass ?"
DOT ]
DOT hit -> hit2 [label=deliver, style=bold]
DOT hit_lookup [
DOT shape=ellipse
DOT label="unbusy"
DOT ]
DOT hit -> hit_lookup [label="lookup", style=dotted, weight=0]
DOT hit_error [
DOT shape=ellipse
DOT label="unbusy"
DOT ]
DOT hit -> hit_error [label="error", weight=0]
DOT hit_pass [
DOT shape=ellipse
DOT label="unbusy"
DOT ]
DOT hit -> hit_pass [label=pass]
DOT hit2 -> hit_pass
DOT HIT -> hit [style=bold,color=green,weight=4]
DOT }
DOT hit_error -> ERROR
DOT hit_pass -> PASS
DOT hit_lookup -> LOOKUP [style=dotted, weight=0]
DOT hit2 -> DELIVER [style=bold]
DOT hit -> err_hit [label="error"]
DOT err_hit [label="ERROR",shape=plaintext]
DOT hit -> PASS [label=pass]
DOT hit -> DELIVER [label="deliver",style=bold,color=green,weight=4]
*/
static int
cnt_hit(struct sess *sp)
{
VCL_hit_method(sp);
assert(!sp->obj->pass);
if (sp->handling == VCL_RET_DELIVER && sp->obj->pass)
sp->handling = VCL_RET_PASS;
VCL_hit_method(sp);
if (sp->handling == VCL_RET_DELIVER) {
sp->step = STP_DELIVER;
return (0);
}
if (sp->handling == VCL_RET_PASS) {
/* Drop our object, we won't need it */
HSH_Deref(sp->obj);
sp->obj = NULL;
if (sp->handling == VCL_RET_PASS) {
sp->step = STP_PASS;
return (0);
}
if (sp->handling == VCL_RET_ERROR) {
HSH_Deref(sp->obj);
sp->obj = NULL;
sp->step = STP_ERROR;
return (0);
}
if (sp->handling == VCL_RET_LOOKUP)
INCOMPL();
INCOMPL();
}
/*--------------------------------------------------------------------
* Look up request in hash table
* LOOKUP
* Hash things together and look object up in hash-table.
*
* LOOKUP consists of two substates so that we can reenter if we
* encounter a busy object.
*
DOT subgraph cluster_lookup {
DOT subgraph xcluster_lookup {
DOT hash [
DOT shape=box
DOT label="vcl_hash()"
DOT ]
DOT lookup [
DOT shape=ellipse
DOT label="find obj in cache"
DOT label="obj in cache ?"
DOT ]
DOT LOOKUP -> lookup [style=bold]
DOT lookup3 [
DOT lookup2 [
DOT shape=ellipse
DOT label="Insert new busy object"
DOT label="obj.pass ?"
DOT ]
DOT lookup -> lookup3 [style=bold]
DOT LOOKUP -> hash [style=bold,color=green,weight=4]
DOT hash -> lookup [label="hash",style=bold,color=green,weight=4]
DOT lookup -> lookup2 [label="yes",style=bold,color=green,weight=4]
DOT }
DOT lookup -> HIT [label="hit", style=bold]
DOT lookup3 -> MISS [label="miss", style=bold]
DOT lookup2 -> HIT [label="no", style=bold,color=green,weight=4]
DOT lookup2 -> PASS [label="yes"]
DOT lookup -> MISS [label="no",style=bold,color=blue,weight=2]
*/
static int
cnt_lookup(struct sess *sp)
{
AZ(sp->obj);
sp->step = STP_LOOKUP2;
return (0);
}
static int
cnt_lookup2(struct sess *sp)
{
struct object *o;
/*
* We don't assign to sp->obj directly because it is used
* to cache state when we encounter a busy object.
*/
o = HSH_Lookup(sp);
VCL_hash_method(sp); /* XXX: no-op for now */
/* If we encountered busy-object, disembark worker thread */
o = HSH_Lookup(sp);
if (o == NULL) {
/*
* We hit a busy object, disembark worker thread and expect
* hash code to restart us, still in STP_LOOKUP, later.
*/
WSL(sp->wrk, SLT_Debug, sp->fd,
"on waiting list on obj %u", sp->obj->xid);
SES_Charge(sp);
......@@ -499,14 +451,17 @@ cnt_lookup2(struct sess *sp)
return (0);
}
/* Account separately for pass and cache objects */
if (sp->obj->pass) {
VSL_stats->cache_hitpass++;
WSL(sp->wrk, SLT_HitPass, sp->fd, "%u", sp->obj->xid);
} else {
HSH_Deref(sp->obj);
sp->obj = NULL;
sp->step = STP_PASS;
return (0);
}
VSL_stats->cache_hit++;
WSL(sp->wrk, SLT_Hit, sp->fd, "%u", sp->obj->xid);
}
sp->step = STP_HIT;
return (0);
}
......@@ -515,37 +470,21 @@ cnt_lookup2(struct sess *sp)
/*--------------------------------------------------------------------
* We had a miss, ask VCL, proceed as instructed
*
DOT subgraph cluster_miss {
DOT subgraph xcluster_miss {
DOT miss [
DOT shape=box
DOT label="vcl_miss()"
DOT ]
DOT MISS -> miss [style=bold]
DOT miss_error [
DOT shape=ellipse
DOT label="obj.cacheable=false\nunbusy"
DOT MISS -> miss [style=bold,color=blue,weight=2]
DOT miss_ins [
DOT label="insert new object"
DOT ]
DOT miss -> miss_error [label="error"]
DOT miss_pass [
DOT shape=ellipse
DOT label="obj.cacheable=false\nunbusy"
DOT ]
DOT miss -> miss_pass [label="pass"]
DOT miss_lookup [
DOT shape=ellipse
DOT label="obj.cacheable=false\nunbusy"
DOT ]
DOT miss -> miss_lookup [label="lookup", style=dotted, weight=0]
DOT miss_fetch [
DOT shape=ellipse
DOT label="fetch obj headers\nfrom backend"
DOT ]
DOT miss -> miss_fetch [label="fetch", style=bold]
DOT miss -> miss_ins [label="fetch",style=bold,color=blue,weight=2]
DOT }
DOT miss_error -> ERROR
DOT miss_pass -> PASS
DOT miss_fetch -> FETCH [style=bold]
DOT miss_lookup -> LOOKUP [style=dotted, weight=0]
DOT miss -> err_miss [label="error"]
DOT err_miss [label="ERROR",shape=plaintext]
DOT miss_ins -> FETCH [style=bold,color=blue,weight=2]
DOT miss -> PASS [label="pass"]
DOT
*/
......@@ -570,21 +509,8 @@ cnt_miss(struct sess *sp)
sp->step = STP_PASS;
return (0);
}
if (sp->handling == VCL_RET_LOOKUP)
INCOMPL();
if (sp->handling == VCL_RET_FETCH) {
AZ(sp->vbc);
if (FetchHeaders(sp)) {
sp->obj->cacheable = 0;
HSH_Unbusy(sp->obj);
HSH_Deref(sp->obj);
sp->obj = NULL;
sp->step = STP_DONE;
RES_Error(sp, 503, NULL);
return (0);
}
sp->step = STP_FETCH;
AN(sp->vbc);
return (0);
}
INCOMPL();
......@@ -595,69 +521,61 @@ cnt_miss(struct sess *sp)
* Start pass processing by getting headers from backend, then
* continue in passbody.
*
DOT subgraph cluster_pass {
DOT subgraph xcluster_pass {
DOT pass [
DOT shape=box
DOT label="vcl_pass()"
DOT ]
DOT pass_do [
DOT shape=ellipse
DOT label="send to bke\nrx bkehdr"
DOT label="create new object\n"
DOT ]
DOT PASS -> pass
DOT pass -> pass_do [label="pass"]
DOT }
DOT pass -> PASSBODY
DOT pass_do -> FETCH
DOT pass -> err_pass [label="error"]
DOT err_pass [label="ERROR",shape=plaintext]
*/
static int
cnt_pass(struct sess *sp)
{
AZ(sp->vbc);
if (!PassSession(sp)) {
AN(sp->vbc);
sp->step = STP_PASSBODY;
} else
sp->step = STP_DONE;
return (0);
}
/*--------------------------------------------------------------------
* We get here when we have the backends headers, send them to client
* and pass any body the backend may have on as well.
*
DOT subgraph cluster_passbody {
DOT passbody [
DOT shape=ellipse
DOT label="send hdrs\npass body\n"
DOT ]
DOT PASSBODY -> passbody
DOT }
DOT passbody -> DONE
*/
static int
cnt_passbody(struct sess *sp)
{
AZ(sp->obj);
sp->wrk->acct.pass++;
AN(sp->vbc);
PassBody(sp);
AZ(sp->vbc);
sp->step = STP_DONE;
VCL_pass_method(sp);
if (sp->handling == VCL_RET_ERROR) {
sp->step = STP_ERROR;
return (0);
}
HSH_Prealloc(sp);
sp->obj = sp->wrk->nobj;
sp->wrk->nobj = NULL;
sp->obj->busy = 1;
sp->step = STP_FETCH;
return (0);
}
/*--------------------------------------------------------------------
* Ship the request header to the backend unchanged, then pipe
* until one of the ends close the connection.
*
DOT subgraph cluster_pipe {
DOT subgraph xcluster_pipe {
DOT pipe [
DOT shape=box
DOT label="vcl_pipe()"
DOT ]
DOT pipe_do [
DOT shape=ellipse
DOT label="build&send hdr\npipe until close"
DOT ]
DOT PIPE -> pipe
DOT pipe -> pipe_do [label="pipe"]
DOT }
DOT pipe -> DONE
DOT pipe_do -> DONE
DOT pipe -> err_pipe [label="error"]
DOT err_pipe [label="ERROR",shape=plaintext]
*/
static int
......@@ -672,29 +590,22 @@ cnt_pipe(struct sess *sp)
/*--------------------------------------------------------------------
* Dispatch the request as instructed by VCL
* RECV
* We have a complete request, get a VCL reference and dispatch it
* as instructed by vcl_recv{}
*
DOT subgraph cluster_recv {
DOT subgraph xcluster_recv {
DOT recv [
DOT shape=box
DOT label="vcl_recv()"
DOT ]
DOT RECV -> recv
DOT recv_lookup [
DOT shape=ellipse
DOT label="discard any body"
DOT ]
DOT recv -> recv_lookup [label="lookup"]
DOT recv_error [
DOT shape=ellipse
DOT label="discard any body"
DOT ]
DOT recv -> recv_error [label="error"]
DOT RECV -> recv [style=bold,color=green,weight=4]
DOT }
DOT recv -> PIPE [label="pipe"]
DOT recv -> PASS [label="pass"]
DOT recv_lookup -> LOOKUP
DOT recv_error -> ERROR
DOT recv -> err_recv [label="error"]
DOT err_recv [label="ERROR",shape=plaintext]
DOT recv -> LOOKUP [label="lookup",style=bold,color=green,weight=4]
*/
static int
......@@ -702,43 +613,45 @@ cnt_recv(struct sess *sp)
{
int done;
VSL_stats->client_req++;
AZ(sp->vcl);
AZ(sp->obj);
/* Update stats of various sorts */
VSL_stats->client_req++; /* XXX not locked */
clock_gettime(CLOCK_REALTIME, &sp->t_req);
sp->wrk->idle = sp->t_req.tv_sec;
sp->xid = ++xids;
sp->wrk->acct.req++;
/* Assign XID and log */
sp->xid = ++xids; /* XXX not locked */
WSL(sp->wrk, SLT_ReqStart, sp->fd,
"%s %s %u", sp->addr, sp->port, sp->xid);
AZ(sp->vcl);
/* Borrow VCL reference from worker thread */
VCL_Refresh(&sp->wrk->vcl);
sp->vcl = sp->wrk->vcl;
sp->wrk->vcl = NULL;
AZ(sp->obj);
AZ(sp->vbc);
sp->wrk->acct.req++;
done = http_DissectRequest(sp->wrk, sp->http, sp->fd);
if (done != 0) {
RES_Error(sp, done, NULL);
RES_Error(sp, done, NULL); /* XXX: STP_ERROR ? */
sp->step = STP_DONE;
return (0);
}
http_DoConnection(sp);
/* By default we use the first backend */
sp->backend = sp->vcl->backend[0];
/* XXX: Handle TRACE & OPTIONS of Max-Forwards = 0 */
/* XXX: determine if request comes with body */
VCL_recv_method(sp);
sp->wantbody = !strcmp(sp->http->hd[HTTP_HDR_REQ].b, "GET");
switch(sp->handling) {
case VCL_RET_LOOKUP:
/* XXX: discard req body, if any */
sp->wantbody = !strcmp(sp->http->hd[HTTP_HDR_REQ].b, "GET");
sp->step = STP_LOOKUP;
return (0);
case VCL_RET_PIPE:
......@@ -760,8 +673,7 @@ cnt_recv(struct sess *sp)
/*--------------------------------------------------------------------
* Central state engine dispatcher.
*
* We grab a VCL reference, and keeps kicking the session around until
* it has had enough.
* Kick the session around until it has had enough.
*
*/
......@@ -776,6 +688,10 @@ CNT_Session(struct sess *sp)
CHECK_OBJ_NOTNULL(w, WORKER_MAGIC);
for (done = 0; !done; ) {
/*
* This is a good place to be paranoid about the various
* pointers still pointing to the things we expect.
*/
CHECK_OBJ_NOTNULL(sp, SESS_MAGIC);
if (sp->obj != NULL)
CHECK_OBJ(sp->obj, OBJECT_MAGIC);
......@@ -784,11 +700,9 @@ CNT_Session(struct sess *sp)
CHECK_OBJ(w->nobj, OBJECT_MAGIC);
if (w->nobjhead != NULL)
CHECK_OBJ(w->nobjhead, OBJHEAD_MAGIC);
switch (sp->step) {
#define STEP(l,u) \
case STP_##u: \
done = cnt_##l(sp); \
break;
#define STEP(l,u) case STP_##u: done = cnt_##l(sp); break;
#include "steps.h"
#undef STEP
default: INCOMPL();
......
......@@ -243,11 +243,12 @@ fetch_eof(const struct sess *sp, int fd, struct http *hp)
/*--------------------------------------------------------------------*/
int
FetchBody(struct sess *sp)
Fetch(struct sess *sp)
{
int cls;
struct vbe_conn *vc;
struct worker *w;
char *b;
int cls;
int body = 1; /* XXX */
struct http *hp;
struct storage *st;
......@@ -256,9 +257,53 @@ FetchBody(struct sess *sp)
CHECK_OBJ_NOTNULL(sp->wrk, WORKER_MAGIC);
CHECK_OBJ_NOTNULL(sp->obj, OBJECT_MAGIC);
assert(sp->obj->busy != 0);
w = sp->wrk;
sp->obj->xid = sp->xid;
vc = VBE_GetFd(sp);
if (vc == NULL)
return (1);
http_ClrHeader(vc->http);
vc->http->logtag = HTTP_Tx;
http_GetReq(w, vc->fd, vc->http, sp->http);
http_FilterHeader(w, vc->fd, vc->http, sp->http, HTTPH_R_FETCH);
http_PrintfHeader(w, vc->fd, vc->http, "X-Varnish: %u", sp->xid);
http_PrintfHeader(w, vc->fd, vc->http,
"X-Forwarded-for: %s", sp->addr);
if (!http_GetHdr(vc->http, H_Host, &b)) {
http_PrintfHeader(w, vc->fd, vc->http, "Host: %s",
sp->backend->hostname);
}
vc = sp->vbc;
sp->vbc = NULL;
WRK_Reset(w, &vc->fd);
http_Write(w, vc->http, 0);
if (WRK_Flush(w)) {
/* XXX: cleanup */
return (1);
}
CHECK_OBJ_NOTNULL(sp, SESS_MAGIC);
CHECK_OBJ_NOTNULL(sp->wrk, WORKER_MAGIC);
CHECK_OBJ_NOTNULL(sp->obj, OBJECT_MAGIC);
if (http_RecvHead(vc->http, vc->fd)) {
/* XXX: cleanup */
return (1);
}
if (http_DissectResponse(sp->wrk, vc->http, vc->fd)) {
/* XXX: cleanup */
return (1);
}
CHECK_OBJ_NOTNULL(sp, SESS_MAGIC);
CHECK_OBJ_NOTNULL(sp->wrk, WORKER_MAGIC);
CHECK_OBJ_NOTNULL(sp->obj, OBJECT_MAGIC);
sp->obj->entered = time(NULL);
assert(sp->obj->busy != 0);
if (http_GetHdr(vc->http, H_Last_Modified, &b))
sp->obj->last_modified = TIM_parse(b);
......@@ -314,66 +359,3 @@ FetchBody(struct sess *sp)
return (0);
}
/*--------------------------------------------------------------------*/
int
FetchHeaders(struct sess *sp)
{
struct vbe_conn *vc;
struct worker *w;
char *b;
CHECK_OBJ_NOTNULL(sp, SESS_MAGIC);
CHECK_OBJ_NOTNULL(sp->wrk, WORKER_MAGIC);
CHECK_OBJ_NOTNULL(sp->obj, OBJECT_MAGIC);
assert(sp->obj->busy != 0);
w = sp->wrk;
sp->obj->xid = sp->xid;
vc = VBE_GetFd(sp);
if (vc == NULL)
return (1);
http_ClrHeader(vc->http);
vc->http->logtag = HTTP_Tx;
http_GetReq(w, vc->fd, vc->http, sp->http);
http_FilterHeader(w, vc->fd, vc->http, sp->http, HTTPH_R_FETCH);
http_PrintfHeader(w, vc->fd, vc->http, "X-Varnish: %u", sp->xid);
http_PrintfHeader(w, vc->fd, vc->http,
"X-Forwarded-for: %s", sp->addr);
if (!http_GetHdr(vc->http, H_Host, &b)) {
http_PrintfHeader(w, vc->fd, vc->http, "Host: %s",
sp->backend->hostname);
}
WRK_Reset(w, &vc->fd);
http_Write(w, vc->http, 0);
if (WRK_Flush(w)) {
/* XXX: cleanup */
return (1);
}
CHECK_OBJ_NOTNULL(sp, SESS_MAGIC);
CHECK_OBJ_NOTNULL(sp->wrk, WORKER_MAGIC);
CHECK_OBJ_NOTNULL(sp->obj, OBJECT_MAGIC);
if (http_RecvHead(vc->http, vc->fd)) {
/* XXX: cleanup */
return (1);
}
if (http_DissectResponse(sp->wrk, vc->http, vc->fd)) {
/* XXX: cleanup */
return (1);
}
CHECK_OBJ_NOTNULL(sp, SESS_MAGIC);
CHECK_OBJ_NOTNULL(sp->wrk, WORKER_MAGIC);
CHECK_OBJ_NOTNULL(sp->obj, OBJECT_MAGIC);
AZ(sp->vbc);
sp->vbc = vc;
sp->obj->entered = time(NULL);
return (0);
}
......@@ -64,23 +64,15 @@
static struct hash_slinger *hash;
struct object *
HSH_Lookup(struct sess *sp)
/* Precreate an objhead and object for later use */
void
HSH_Prealloc(struct sess *sp)
{
struct worker *w;
struct http *h;
struct objhead *oh;
struct object *o;
char *url, *host;
CHECK_OBJ_NOTNULL(sp, SESS_MAGIC);
CHECK_OBJ_NOTNULL(sp->wrk, WORKER_MAGIC);
CHECK_OBJ_NOTNULL(sp->http, HTTP_MAGIC);
AN(hash);
w = sp->wrk;
h = sp->http;
/* Precreate an objhead and object in case we need them */
if (w->nobjhead == NULL) {
w->nobjhead = calloc(sizeof *w->nobjhead, 1);
XXXAN(w->nobjhead);
......@@ -102,7 +94,25 @@ HSH_Lookup(struct sess *sp)
VSL_stats->n_object++;
} else
CHECK_OBJ_NOTNULL(w->nobj, OBJECT_MAGIC);
}
struct object *
HSH_Lookup(struct sess *sp)
{
struct worker *w;
struct http *h;
struct objhead *oh;
struct object *o;
char *url, *host;
CHECK_OBJ_NOTNULL(sp, SESS_MAGIC);
CHECK_OBJ_NOTNULL(sp->wrk, WORKER_MAGIC);
CHECK_OBJ_NOTNULL(sp->http, HTTP_MAGIC);
AN(hash);
w = sp->wrk;
h = sp->http;
HSH_Prealloc(sp);
url = h->hd[HTTP_HDR_URL].b;
if (!http_GetHdr(h, H_Host, &host))
host = url;
......@@ -189,10 +199,13 @@ HSH_Ref(struct object *o)
CHECK_OBJ_NOTNULL(o, OBJECT_MAGIC);
oh = o->objhead;
CHECK_OBJ_NOTNULL(oh, OBJHEAD_MAGIC);
if (oh != NULL) {
CHECK_OBJ(oh, OBJHEAD_MAGIC);
LOCK(&oh->mtx);
}
assert(o->refcnt > 0);
o->refcnt++;
if (oh != NULL)
UNLOCK(&oh->mtx);
}
......@@ -205,7 +218,13 @@ HSH_Deref(struct object *o)
CHECK_OBJ_NOTNULL(o, OBJECT_MAGIC);
oh = o->objhead;
CHECK_OBJ_NOTNULL(oh, OBJHEAD_MAGIC);
if (oh == NULL) {
/* Pass object, not referenced anywhere */
free(o);
return;
}
CHECK_OBJ(oh, OBJHEAD_MAGIC);
/* drop ref on object */
LOCK(&oh->mtx);
......
/*-
* Copyright (c) 2006 Verdens Gang AS
* Copyright (c) 2006 Linpro AS
* All rights reserved.
*
* Author: Poul-Henning Kamp <phk@phk.freebsd.dk>
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY AUTHOR AND CONTRIBUTORS ``AS IS'' AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL AUTHOR OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
*
* $Id$
*
* XXX: charge bytes to srcaddr
* XXX: buffer to relieve backed ASAP.
* XXX: Check if response has any body
* XXX: Don't pass chunked to HTTP/1.0 client
*/
#include <stdio.h>
#include <inttypes.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#ifndef HAVE_CLOCK_GETTIME
#include "compat/clock_gettime.h"
#endif
#include "shmlog.h"
#include "cache.h"
#define PASS_BUFSIZ 8192
/*--------------------------------------------------------------------*/
static int
pass_straight(struct sess *sp, int fd, struct http *hp, char *bi)
{
int i;
off_t cl;
unsigned c;
char buf[PASS_BUFSIZ];
if (bi != NULL)
cl = strtoumax(bi, NULL, 0);
else
cl = (1 << 30);
i = fcntl(fd, F_GETFL); /* XXX ? */
i &= ~O_NONBLOCK;
i = fcntl(fd, F_SETFL, i);
while (cl != 0) {
c = cl;
if (c > sizeof buf)
c = sizeof buf;
i = http_Read(hp, fd, buf, c);
if (i == 0 && bi == NULL)
return (1);
if (i <= 0) {
vca_close_session(sp, "backend closed");
return (1);
}
sp->wrk->acct.bodybytes += WRK_Write(sp->wrk, buf, i);
if (WRK_Flush(sp->wrk))
vca_close_session(sp, "remote closed");
cl -= i;
}
return (0);
}
/*--------------------------------------------------------------------*/
static int
pass_chunked(struct sess *sp, int fd, struct http *hp)
{
int i, j;
char *p, *q;
unsigned u;
char buf[PASS_BUFSIZ];
char *bp, *be;
i = fcntl(fd, F_GETFL); /* XXX ? */
i &= ~O_NONBLOCK;
i = fcntl(fd, F_SETFL, i);
bp = buf;
be = buf + sizeof buf;
p = buf;
while (1) {
i = http_Read(hp, fd, bp, be - bp);
xxxassert(i >= 0);
if (i == 0 && p == bp)
break;
bp += i;
/* buffer valid from p to bp */
assert(bp >= p);
/* chunk starts with f("%x\r\n", len) */
u = strtoul(p, &q, 16);
while (q && q < bp && *q == ' ')
/* shouldn't happen - but sometimes it does */
q++;
if (q == NULL || q > bp - 2 /* want \r\n in same buffer */) {
/* short - move to start of buffer and extend */
memmove(buf, p, bp - p);
bp -= p - buf;
p = buf;
continue;
}
assert(*q == '\r');
q++;
assert(*q == '\n');
q++;
/* we just received the final zero-length chunk */
if (u == 0) {
sp->wrk->acct.bodybytes += WRK_Write(sp->wrk, p, q - p);
break;
}
/* include chunk header */
u += q - p;
/* include trailing \r\n with chunk */
u += 2;
for (;;) {
j = u;
if (bp - p < j)
j = bp - p;
sp->wrk->acct.bodybytes += WRK_Write(sp->wrk, p, j);
WRK_Flush(sp->wrk);
p += j;
assert(u >= j);
u -= j;
if (u == 0)
break;
p = bp = buf;
j = u;
if (j > be - bp)
j = be - bp;
i = http_Read(hp, fd, bp, j);
xxxassert(i > 0);
bp += i;
}
}
if (WRK_Flush(sp->wrk))
vca_close_session(sp, "remote closed");
return (0);
}
/*--------------------------------------------------------------------*/
void
PassBody(struct sess *sp)
{
struct vbe_conn *vc;
char *b;
int cls;
CHECK_OBJ_NOTNULL(sp, SESS_MAGIC);
CHECK_OBJ_NOTNULL(sp->vbc, VBE_CONN_MAGIC);
vc = sp->vbc;
sp->vbc = NULL;
clock_gettime(CLOCK_REALTIME, &sp->t_resp);
http_ClrHeader(sp->http);
http_CopyResp(sp->wrk, sp->fd, sp->http, vc->http);
http_FilterHeader(sp->wrk, sp->fd, sp->http, vc->http, HTTPH_A_PASS);
http_PrintfHeader(sp->wrk, sp->fd, sp->http, "X-Varnish: %u", sp->xid);
http_PrintfHeader(sp->wrk, sp->fd, sp->http,
"X-Forwarded-for: %s", sp->addr);
/* XXX */
if (http_HdrIs(vc->http, H_Transfer_Encoding, "chunked"))
http_PrintfHeader(sp->wrk, sp->fd, sp->http, "Transfer-Encoding: chunked");
WRK_Reset(sp->wrk, &sp->fd);
sp->wrk->acct.hdrbytes += http_Write(sp->wrk, sp->http, 1);
if (http_GetHdr(vc->http, H_Content_Length, &b))
cls = pass_straight(sp, vc->fd, vc->http, b);
else if (http_HdrIs(vc->http, H_Connection, "close"))
cls = pass_straight(sp, vc->fd, vc->http, NULL);
else if (http_HdrIs(vc->http, H_Transfer_Encoding, "chunked"))
cls = pass_chunked(sp, vc->fd, vc->http);
else if (http_IsBodyless(vc->http))
cls = 0;
else {
cls = pass_straight(sp, vc->fd, vc->http, NULL);
}
if (WRK_Flush(sp->wrk))
vca_close_session(sp, "remote closed");
if (http_GetHdr(vc->http, H_Connection, &b) && !strcasecmp(b, "close"))
cls = 1;
if (cls)
VBE_ClosedFd(sp->wrk, vc, 0);
else
VBE_RecycleFd(sp->wrk, vc);
}
/*--------------------------------------------------------------------*/
int
PassSession(struct sess *sp)
{
int i;
struct vbe_conn *vc;
struct worker *w;
char *b;
CHECK_OBJ_NOTNULL(sp, SESS_MAGIC);
CHECK_OBJ_NOTNULL(sp->wrk, WORKER_MAGIC);
w = sp->wrk;
vc = VBE_GetFd(sp);
if (vc == NULL)
return (1);
http_CopyReq(w, vc->fd, vc->http, sp->http);
http_FilterHeader(w, vc->fd, vc->http, sp->http, HTTPH_R_PASS);
http_PrintfHeader(w, vc->fd, vc->http, "X-Varnish: %u", sp->xid);
if (!http_GetHdr(vc->http, H_Host, &b)) {
http_PrintfHeader(w, vc->fd, vc->http, "Host: %s",
sp->backend->hostname);
}
WRK_Reset(w, &vc->fd);
http_Write(w, vc->http, 0);
i = WRK_Flush(w);
xxxassert(i == 0);
/* XXX: copy any contents */
i = http_RecvHead(vc->http, vc->fd);
xxxassert(i == 0);
http_DissectResponse(w, vc->http, vc->fd);
assert(sp->vbc == NULL);
sp->vbc = vc;
return (0);
}
......@@ -82,7 +82,7 @@ VRT_GetHdr(struct sess *sp, int where, const char *n)
hp = sp->http;
break;
case 2:
hp = sp->vbc->http;
hp = &sp->obj->http;
break;
default:
INCOMPL();
......
......@@ -79,6 +79,18 @@ static const char *default_vcl =
" lookup;\n"
"}\n"
"\n"
"sub default_vcl_pipe {\n"
" pipe;\n"
"}\n"
"\n"
"sub default_vcl_pass {\n"
" pass;\n"
"}\n"
"\n"
"sub default_vcl_hash {\n"
" hash;\n"
"}\n"
"\n"
"sub default_vcl_hit {\n"
" if (!obj.cacheable) {\n"
" pass;\n"
......@@ -95,10 +107,10 @@ static const char *default_vcl =
" error;\n"
" }\n"
" if (!obj.cacheable) {\n"
" insert_pass;\n"
" pass;\n"
" }\n"
" if (resp.http.Set-Cookie) {\n"
" insert_pass;\n"
" pass;\n"
" }\n"
" insert;\n"
"}\n"
......
......@@ -34,9 +34,7 @@ STEP(first, FIRST)
STEP(recv, RECV)
STEP(pipe, PIPE)
STEP(pass, PASS)
STEP(passbody, PASSBODY)
STEP(lookup, LOOKUP)
STEP(lookup2, LOOKUP2)
STEP(miss, MISS)
STEP(hit, HIT)
STEP(fetch, FETCH)
......
......@@ -28,6 +28,9 @@ struct VCL_conf {
vcl_fini_f *fini_func;
vcl_func_f *recv_func;
vcl_func_f *pipe_func;
vcl_func_f *pass_func;
vcl_func_f *hash_func;
vcl_func_f *miss_func;
vcl_func_f *hit_func;
vcl_func_f *fetch_func;
......
......@@ -11,9 +11,9 @@
VCL_RET_MAC_E(error, ERROR, (1 << 0), 0)
#endif
VCL_RET_MAC(lookup, LOOKUP, (1 << 1), 1)
VCL_RET_MAC(pipe, PIPE, (1 << 2), 2)
VCL_RET_MAC(pass, PASS, (1 << 3), 3)
VCL_RET_MAC(insert_pass, INSERT_PASS, (1 << 4), 4)
VCL_RET_MAC(hash, HASH, (1 << 2), 2)
VCL_RET_MAC(pipe, PIPE, (1 << 3), 3)
VCL_RET_MAC(pass, PASS, (1 << 4), 4)
VCL_RET_MAC(fetch, FETCH, (1 << 5), 5)
VCL_RET_MAC(insert, INSERT, (1 << 6), 6)
VCL_RET_MAC(deliver, DELIVER, (1 << 7), 7)
......@@ -21,9 +21,9 @@ VCL_RET_MAC(discard, DISCARD, (1 << 8), 8)
#else
#define VCL_RET_ERROR (1 << 0)
#define VCL_RET_LOOKUP (1 << 1)
#define VCL_RET_PIPE (1 << 2)
#define VCL_RET_PASS (1 << 3)
#define VCL_RET_INSERT_PASS (1 << 4)
#define VCL_RET_HASH (1 << 2)
#define VCL_RET_PIPE (1 << 3)
#define VCL_RET_PASS (1 << 4)
#define VCL_RET_FETCH (1 << 5)
#define VCL_RET_INSERT (1 << 6)
#define VCL_RET_DELIVER (1 << 7)
......@@ -33,8 +33,11 @@ VCL_RET_MAC(discard, DISCARD, (1 << 8), 8)
#ifdef VCL_MET_MAC
VCL_MET_MAC(recv,RECV,(VCL_RET_ERROR|VCL_RET_PASS|VCL_RET_PIPE|VCL_RET_LOOKUP))
VCL_MET_MAC(miss,MISS,(VCL_RET_ERROR|VCL_RET_PASS|VCL_RET_PIPE|VCL_RET_FETCH))
VCL_MET_MAC(hit,HIT,(VCL_RET_ERROR|VCL_RET_PASS|VCL_RET_PIPE|VCL_RET_DELIVER))
VCL_MET_MAC(fetch,FETCH,(VCL_RET_ERROR|VCL_RET_PASS|VCL_RET_PIPE|VCL_RET_INSERT|VCL_RET_INSERT_PASS))
VCL_MET_MAC(pipe,PIPE,(VCL_RET_ERROR|VCL_RET_PIPE))
VCL_MET_MAC(pass,PASS,(VCL_RET_ERROR|VCL_RET_PASS))
VCL_MET_MAC(hash,HASH,(VCL_RET_HASH))
VCL_MET_MAC(miss,MISS,(VCL_RET_ERROR|VCL_RET_PASS|VCL_RET_FETCH))
VCL_MET_MAC(hit,HIT,(VCL_RET_ERROR|VCL_RET_PASS|VCL_RET_DELIVER))
VCL_MET_MAC(fetch,FETCH,(VCL_RET_ERROR|VCL_RET_PASS|VCL_RET_INSERT))
VCL_MET_MAC(timeout,TIMEOUT,(VCL_RET_FETCH|VCL_RET_DISCARD))
#endif
......@@ -228,14 +228,14 @@ vcl_fixed_token(const char *p, const char **q)
return (T_FETCH);
}
return (0);
case 'i':
if (p[0] == 'i' && p[1] == 'n' && p[2] == 's' &&
p[3] == 'e' && p[4] == 'r' && p[5] == 't' &&
p[6] == '_' && p[7] == 'p' && p[8] == 'a' &&
p[9] == 's' && p[10] == 's' && !isvar(p[11])) {
*q = p + 11;
return (T_INSERT_PASS);
case 'h':
if (p[0] == 'h' && p[1] == 'a' && p[2] == 's' &&
p[3] == 'h' && !isvar(p[4])) {
*q = p + 4;
return (T_HASH);
}
return (0);
case 'i':
if (p[0] == 'i' && p[1] == 'n' && p[2] == 's' &&
p[3] == 'e' && p[4] == 'r' && p[5] == 't'
&& !isvar(p[6])) {
......@@ -396,11 +396,11 @@ vcl_init_tnames(void)
vcl_tnames[T_FETCH] = "fetch";
vcl_tnames[T_FUNC] = "func";
vcl_tnames[T_GEQ] = ">=";
vcl_tnames[T_HASH] = "hash";
vcl_tnames[T_IF] = "if";
vcl_tnames[T_INC] = "++";
vcl_tnames[T_INCR] = "+=";
vcl_tnames[T_INSERT] = "insert";
vcl_tnames[T_INSERT_PASS] = "insert_pass";
vcl_tnames[T_LEQ] = "<=";
vcl_tnames[T_LOOKUP] = "lookup";
vcl_tnames[T_MUL] = "*=";
......@@ -424,9 +424,9 @@ vcl_output_lang_h(FILE *f)
{
fputs("#define VCL_RET_ERROR (1 << 0)\n", f);
fputs("#define VCL_RET_LOOKUP (1 << 1)\n", f);
fputs("#define VCL_RET_PIPE (1 << 2)\n", f);
fputs("#define VCL_RET_PASS (1 << 3)\n", f);
fputs("#define VCL_RET_INSERT_PASS (1 << 4)\n", f);
fputs("#define VCL_RET_HASH (1 << 2)\n", f);
fputs("#define VCL_RET_PIPE (1 << 3)\n", f);
fputs("#define VCL_RET_PASS (1 << 4)\n", f);
fputs("#define VCL_RET_FETCH (1 << 5)\n", f);
fputs("#define VCL_RET_INSERT (1 << 6)\n", f);
fputs("#define VCL_RET_DELIVER (1 << 7)\n", f);
......@@ -461,6 +461,9 @@ vcl_output_lang_h(FILE *f)
fputs(" vcl_fini_f *fini_func;\n", f);
fputs("\n", f);
fputs(" vcl_func_f *recv_func;\n", f);
fputs(" vcl_func_f *pipe_func;\n", f);
fputs(" vcl_func_f *pass_func;\n", f);
fputs(" vcl_func_f *hash_func;\n", f);
fputs(" vcl_func_f *miss_func;\n", f);
fputs(" vcl_func_f *hit_func;\n", f);
fputs(" vcl_func_f *fetch_func;\n", f);
......
......@@ -35,9 +35,12 @@
#
set methods {
{recv {error pass pipe lookup}}
{miss {error pass pipe fetch}}
{hit {error pass pipe deliver}}
{fetch {error pass pipe insert insert_pass}}
{pipe {error pipe}}
{pass {error pass}}
{hash {hash}}
{miss {error pass fetch}}
{hit {error pass deliver}}
{fetch {error pass insert}}
{timeout {fetch discard}}
}
......@@ -46,9 +49,9 @@ set methods {
set returns {
error
lookup
hash
pipe
pass
insert_pass
fetch
insert
deliver
......
......@@ -24,9 +24,9 @@
#define T_SWITCH_CONFIG 142
#define T_ERROR 143
#define T_LOOKUP 144
#define T_PIPE 145
#define T_PASS 146
#define T_INSERT_PASS 147
#define T_HASH 145
#define T_PIPE 146
#define T_PASS 147
#define T_FETCH 148
#define T_INSERT 149
#define T_DELIVER 150
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment