-
Notifications
You must be signed in to change notification settings - Fork 863
I110/redis session resumption #1087
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry for the delay.
The design has become clear, and I like the approach. Thank you for all the efforts.
I have left some comments regarding how the binding is implemented. Please let me know what you think.
lib/common/redis/socket.c.h
Outdated
|
||
ac->ev.data = p; | ||
#if H2O_USE_LIBUV | ||
p->socket = h2o_uv_poll_create(loop, c->fd, (uv_close_cb)free); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You should create a function that calls free instead of trying to pass the address of free, since free might be a macro.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I understood what you intend, and can change it.
BTW, how do you think about other sources which includes this style of passing free callback?
For example, lib/common/multithread.c
, lib/common/socket/uv-binding.c.h
, and others.
Would it be better to modify these too (in another PR) ?
lib/common/socket/uv-binding.c.h
Outdated
uv_poll_start((uv_poll_t *)sock->handle, sock->poll.events, on_poll); | ||
break; | ||
default: | ||
assert(!"FIXME"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please use h2o_fatal
, since we would not want to see this check dropped when built with -DNDEBUG
.
Same applies for other places.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't know that macro. I'll use it.
lib/common/socket/uv-binding.c.h
Outdated
uv_close((uv_handle_t *)poll, (uv_close_cb)free); | ||
free(sock); | ||
return NULL; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please do not copy-paste the initialization tasks from h2o_uv_socket_create
. Instead, you should extract the logics in common to a separate function and call it from the two create functions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fmm, I'll try it.
lib/common/redis/socket.c.h
Outdated
p->context = ac; | ||
|
||
return REDIS_OK; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe you should move the contents of this file to lib/common/redis.c? I believe the functions are only used from the file.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, I'll move it.
lib/common/redis.c
Outdated
static void on_redis_disconnect(const redisAsyncContext *redis, int status) | ||
{ | ||
h2o_redis_conn_t *conn = (h2o_redis_conn_t *)redis->data; | ||
if (conn->cb.on_disconnect) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please use != NULL
considering the fact that on_disconnect
is a pointer.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
matada. suimasen
lib/common/socket/uv-binding.c.h
Outdated
h2o_socket_close(&sock->super); | ||
return NULL; | ||
} | ||
|
||
return &sock->super; | ||
} | ||
|
||
h2o_socket_t *h2o_uv_poll_create(h2o_loop_t *loop, int fd, uv_close_cb close_cb) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please rename the function to h2o_uv__poll_create
considering the fact that the socket create by this function cannot be used with certain API, e.g. h2o_socket_write
, h2o_socket_getpeername
, etc.
Note: we generally use __
to designate a private function.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK
include/h2o/redis.h
Outdated
#define h2o__redis_h | ||
|
||
#include "hiredis.h" | ||
#include "socket.h" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the intention to include "h2o/socket.h"
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
soukamo. I'll check it
if redis is added it might make sense to look into being able to use it especially for things like dynamically choosing the upstream route it would be great. that being said, i think etcd could be considered as an alternative, it seems to be the de-facto standard now in distributed system for sharing data. |
@yannick Thank you for sharing your idea.
Sure, I'm planning to make it able to use redis from mruby handler, in the near future.
As you said, I also believe that etcd is the best for such use case.
Sounds great, but could you elaborate more details of your idea? In my first impression, it might be enough to write the routing logic in mruby, using redis as the routing configuration store. |
it was just an example, and as you mentioned it would be ok for most scenarios to simply lookup the possible upstream hosts in redis. for some scenarios you might want to implement a distribution algorithm directly without the mruby overhead i guess. |
2686947
to
e305caf
Compare
@yannick Sorry for the delay, and thank you for your suggestion. I believe what you said is worth considering, so I'll revisit it when more specific scenarios appear. |
TODO: write e2e tests |
@i110 no worries, i'm only making very high level comments here, and i am not sure in how far they are really relevant here. from a libh2o's user perspective it would be actually nice if there would be a more clear interface such that adding a different session resumption mechanism would be easier. (e.g. work towards removing hiredis include in ssl.c ). one thing that made nginx very successfull is that its all very modular. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for all your efforts!
I have left mostly high-level comments regarding the design. Please let me know what you think.
#ifndef h2o__redis_h | ||
#define h2o__redis_h | ||
|
||
#include "hiredis.h" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be preferable to not introduce dependency at header-file level.
If we keep the dependency at source-code level, it would be easier to build libh2o (or a standalone server) without dependency to hiredis.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the reason why this is included here is, h2o_redis_command_cb
uses redisReply
as a callback argument (which includes various type of response, like string, array, nil, etc)
Do you mean that we should..
- define original struct which represent the response from redis, and copy values to it from redisReply?
typedef redisReply h2o_redis_reply
and other constants (like REDIS_REPLY_ERROR) ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
memo: as we talked, we still have to include this here
lib/common/redis.c
Outdated
} | ||
} | ||
|
||
h2o_redis_conn_t *h2o_redis_connect(h2o_loop_t *loop, const char *host, uint16_t port, h2o_redis_callbacks_t cb, void *cb_data) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see several issues regarding the design of the function.
I. The function returns failure using two methods, either by returning NULL or by calling a callback. Such approach complicates things. I'd suggest always using a callback for notifying errors.
Note that memory allocation in H2O must be done by h2o_mem_alloc
(or similar functions defined in h2o/memory.h) that never returns a failure.
II. The function is related to three callbacks: on_connect, on_connection_failure, on_disconnect. I'd suggest merging them into one or two functions (e.g. on_connect and on_close; in which case the latter function would be called for both connection failure and disconnection, with its argument specifying the reason).
III. The interface is designed to use void *cb_data
to associate the user data to the redis connection. I'd suggest using structure-extensions instead, much like we do for other structures (e.g. h2o_conn_t, h2o_handler_t). That way, in some applications you would not need to maintain the bidirectional pointer that connects h2o_redis_conn_t and the user data.
IV. The proposed model creates h2o_redis_conn_t
upon connection, and discards the object upon disconnection. It might be worth considering if we should decouple connection/disconnection and the lifecycle of the object. My understanding is that in most of the use cases we would want to retain a connection to a redis server, and use it from many places. Having a single object that occasionally connects / disconnects to a redis server might help in that respect.
V. You should be careful in calling the callbacks synchronously. For example, the get callback of async resumption is called from a callback within SSL_handshake. And I believe there is a path that leading h2o_socket_ssl_resume_server_handshake before exiting from the get callback. What this means that SSL_handshake (called from h2o_socket_ssl_resume_server_hanshake) would be called within SSL_handshake (that is calling the get callback). However, SSL_handshake is not a reentrant function. I'd suggest to make all callback invocations asynchronous, by using zero_timeout when necessary.
lib/common/redis.c
Outdated
void *data; | ||
}; | ||
|
||
void on_command(redisAsyncContext *redis, void *reply, void *privdata) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is static
missing here?
lib/common/redis.c
Outdated
|
||
/* redis socket adapter */ | ||
|
||
struct st_redis_socket_data { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please add the _t
suffix.
lib/common/redis.c
Outdated
if (p == NULL) { | ||
return -1; | ||
} | ||
memset(p, 0, sizeof(*p)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As a general rule, I recommend using compound assignment for initializing a structure. In this case, *p = (struct ...){NULL}
?
lib/core/util.c
Outdated
struct st_h2o_accept_data_t *accept_data = sock->data; | ||
h2o_redis_conn_t *conn = get_redis_connection(accept_data->ctx->ctx); | ||
h2o_iovec_t key = base64_encode(session_id, async_resumption_context.redis.prefix); | ||
h2o_redis_command(conn, redis_resumption_on_get, accept_data, "GET %s", key.base); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What happens if the get function gets called while hiredis is trying to establish a connection to the server? Doesn't the invocation of on_get callback get postponed until the connection is established (or times out)?
We do not want to delay handshakes. Unless there is a live connection to hiredis, the function should fail immediately.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doesn't the invocation of on_get callback get postponed until the connection is established (or times out)?
As you worried (and me too), the invocation will be postponed. I'll change the behavior like the following:
- when the callback is called, check the connection to the redis (using
h2o_redis_is_connected
method) - If live connection exists, do the resumption
- if not, async resumption fails immediately (and do full-handshake), but try to re-connect to the redis
lib/core/util.c
Outdated
} | ||
|
||
void h2o_accept_setup_async_ssl_resumption(h2o_memcached_context_t *memc, unsigned expiration) | ||
static h2o_iovec_t base64_encode(h2o_iovec_t str, h2o_iovec_t prefix) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please rename this function to a more appropriate name.
The name doesn't reflect what it is doing: concatenate a prefix with a base64-encoded string.
One possible candidate would be: build_key.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
one thing to note is, this function is used to encode the value, not only the key.
I'll try to rename it or split this into two separated functions
ac46d6b
to
fd4d840
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for the changes.
The PR looks mostly OK, well-isolated from the existing code-base.
I've left several comments hoping that we could make the resumption-related interface between the core and the protocol binding (e.g. memcached, redis) even clearer.
lib/core/util.c
Outdated
h2o_memcached_req_t *async_resumption_get_req; | ||
h2o_timeout_entry_t resumption_failure_timeout; | ||
h2o_memcached_req_t *memcached_resumption_get_req; | ||
h2o_redis_command_t *redis_resumption_get_command; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about creating a common structure and inheriting it for memcached and redis? By doing so, I assume you can reduce if
statements a lot, and we would also have room to add support for other protocols without making changes to the memcached and redis bindings.
For example, something like below.
struct st_h2o_accept_data_t {
h2o_accept_ctx_t *ctx;
h2o_socket_t *sock;
h2o_timeout_entry_t timeout;
struct timeval connected_at;
};
struct st_h2o_memcached_accept_data_t {
struct st_h2o_accept_data_t super;
h2o_memcached_req_t *memcached_resumption_get_req;
};
struct st_h2o_redis_accept_data_t {
struct st_h2o_accept_data_t super;
h2o_timeout_entry_t resumption_failure_timeout;
h2o_redis_command_t *redis_resumption_get_command;
};
lib/core/util.c
Outdated
h2o_redis_connect(conn, async_resumption_context.redis.host.base, async_resumption_context.redis.port); | ||
} | ||
// abort resumption | ||
h2o_timeout_link(accept_data->ctx->ctx->loop, &accept_data->ctx->ctx->zero_timeout, &accept_data->resumption_failure_timeout); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it be possible to reuse st_accept_data_t::timeout
instead of introducing resumption_failure_timeout
? I assume this is the only use-case for the variable.
src/ssl.c
Outdated
struct { | ||
char *host; | ||
uint16_t port; | ||
} redis; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we merge conf.memcached
and conf.redis
into a union
, named address
or something?
@kazuho thank you for your comment. I addressed the issues. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for the update. Aside from the naming, the code looks good to me.
Let's schedule the release of the redis binding to 2.2.
lib/core/util.c
Outdated
} | ||
|
||
static struct { | ||
struct st_h2o_accept_data_t *(*create)(h2o_accept_ctx_t *ctx, h2o_socket_t *sock, struct timeval connected_at); | ||
void (*free)(struct st_h2o_accept_data_t *accept_data); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please use destroy
instead of free
. free
is a function defined in ISO C, that might be defined as a macro. In such case, the code will fail to compile.
That is the reason we use destroy
as an antonym to create
throughout H2O (and FWIW we use dispose
as an antonym for init
).
Thank you for the updates. I believe there are no pending issues related to this PR. I'll re-check (and hopefully merge) after we release 2.1. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've gone through the code once more and the only issue I have is the in-line comment.
@i110 Could you please look into it?
lib/common/socket.c
Outdated
return 0; | ||
} | ||
|
||
static void on_async_resumption_remove(SSL_CTX *ssl_ctx, SSL_SESSION *session) | ||
{ | ||
if (resumption_remove == NULL) { | ||
return; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@kazuho If I remember correctly, it is because if we use the session backends which have expiration feature, there're no need to remove explicitly the sessions
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for the clarification. Looking at the man page, it says:
The remove_session_cb() is called, whenever the SSL engine removes a session from the internal cache. This happens
- when the session is removed because it is expired
- or when a connection was not shutdown cleanly.
- It also happens for all sessions in the internal session cache when SSL_CTX_free is called.
(source: https://www.openssl.org/docs/man1.0.1/ssl/SSL_CTX_sess_set_remove_cb.html)
So it would be reasonable for an external cache (i.e. a cache that uses memcached or redis) to do nothing when the event gets fired (note also that we would not want to remove a cache entry when a connection is abruptly shut down, which is often the case with the browsers).
To summarize, I support making this change.
OTOH, I do oppose to making this change within this PR. Changing how H2O responds to the remove callback is a bug fix. Therefore, it should go through a separate PR to master, especially since we might want to backport this to 2.0 branch as well.
I would appreciate it if you create a PR against master that fixes the issue. The PR should contain sufficient information describing why the code need to be changed such way.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
note also that we would not want to remove a cache entry when a connection is abruptly shut down, which is often the case with the browsers
ah yes, this was the second reason. thank you for the complement.
I would appreciate it if you create a PR against master that fixes the issue.
I'll do it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
1005e2c
to
b576fb9
Compare
…3b1648ef6f1f95 () at deps/hiredis
b576fb9
to
de740d0
Compare
@kazuho I resolved conflicts and rebased. |
…929410593f60c1 () at deps/hiredis
see also: #1305 |
make it able to use Redis as session cache backend.