Skip to content

Conversation

i110
Copy link
Contributor

@i110 i110 commented Sep 26, 2016

make it able to use Redis as session cache backend.

Copy link
Member

@kazuho kazuho left a 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.


ac->ev.data = p;
#if H2O_USE_LIBUV
p->socket = h2o_uv_poll_create(loop, c->fd, (uv_close_cb)free);
Copy link
Member

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.

Copy link
Contributor Author

@i110 i110 Oct 6, 2016

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) ?

uv_poll_start((uv_poll_t *)sock->handle, sock->poll.events, on_poll);
break;
default:
assert(!"FIXME");
Copy link
Member

@kazuho kazuho Oct 6, 2016

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.

Copy link
Contributor Author

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.

uv_close((uv_handle_t *)poll, (uv_close_cb)free);
free(sock);
return NULL;
}
Copy link
Member

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.

Copy link
Contributor Author

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.

p->context = ac;

return REDIS_OK;
}
Copy link
Member

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.

Copy link
Contributor Author

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.

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) {
Copy link
Member

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

matada. suimasen

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)
Copy link
Member

@kazuho kazuho Oct 6, 2016

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK

#define h2o__redis_h

#include "hiredis.h"
#include "socket.h"
Copy link
Member

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"?

Copy link
Contributor Author

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

@yannick
Copy link
Contributor

yannick commented Oct 7, 2016

if redis is added it might make sense to look into being able to use it
a) from mruby
b) to store h2o configuration, certificates etc.

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.

@i110
Copy link
Contributor Author

i110 commented Oct 7, 2016

@yannick Thank you for sharing your idea.

a) from mruby

Sure, I'm planning to make it able to use redis from mruby handler, in the near future.

b) to store h2o configuration, certificates etc.

As you said, I also believe that etcd is the best for such use case.

especially for things like dynamically choosing the upstream route it would be great.

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.

@yannick
Copy link
Contributor

yannick commented Oct 7, 2016

especially for things like dynamically choosing the upstream route it would be great.
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.

@i110 i110 force-pushed the i110/redis-session-resumption branch from 2686947 to e305caf Compare October 17, 2016 08:17
@i110
Copy link
Contributor Author

i110 commented Oct 17, 2016

@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.

@i110 i110 changed the title [WIP] I110/redis session resumption I110/redis session resumption Oct 17, 2016
@i110
Copy link
Contributor Author

i110 commented Oct 17, 2016

TODO: write e2e tests

@yannick
Copy link
Contributor

yannick commented Oct 17, 2016

@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.

Copy link
Member

@kazuho kazuho left a 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"
Copy link
Member

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.

Copy link
Contributor Author

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..

  1. define original struct which represent the response from redis, and copy values to it from redisReply?
  2. typedef redisReply h2o_redis_reply and other constants (like REDIS_REPLY_ERROR) ?

Copy link
Contributor Author

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

}
}

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)
Copy link
Member

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.

void *data;
};

void on_command(redisAsyncContext *redis, void *reply, void *privdata)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is static missing here?


/* redis socket adapter */

struct st_redis_socket_data {
Copy link
Member

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.

if (p == NULL) {
return -1;
}
memset(p, 0, sizeof(*p));
Copy link
Member

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);
Copy link
Member

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.

Copy link
Contributor Author

@i110 i110 Oct 19, 2016

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:

  1. when the callback is called, check the connection to the redis (using h2o_redis_is_connected method)
  2. If live connection exists, do the resumption
  3. 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)
Copy link
Member

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.

Copy link
Contributor Author

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

@i110 i110 force-pushed the i110/redis-session-resumption branch 2 times, most recently from ac46d6b to fd4d840 Compare October 25, 2016 09:20
Copy link
Member

@kazuho kazuho left a 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;
Copy link
Member

@kazuho kazuho Oct 28, 2016

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);
Copy link
Member

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;
Copy link
Member

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?

@i110
Copy link
Contributor Author

i110 commented Oct 28, 2016

@kazuho thank you for your comment. I addressed the issues.

@kazuho kazuho added this to the v2.2 milestone Nov 8, 2016
Copy link
Member

@kazuho kazuho left a 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);
Copy link
Member

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).

@kazuho
Copy link
Member

kazuho commented Nov 30, 2016

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.

@i110 i110 mentioned this pull request Dec 28, 2016
5 tasks
Copy link
Member

@kazuho kazuho left a 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?

return 0;
}

static void on_async_resumption_remove(SSL_CTX *ssl_ctx, SSL_SESSION *session)
{
if (resumption_remove == NULL) {
return;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you recall why the remove callbacks were abolished in edecc08 (for redis) and a5a433b (for memcached)?

If there is some reason to not remove the session tickets, I think it would make more sense for us to make on_async_resumption_remove a no-op rather than relying on resumption_remove == NULL.

Copy link
Contributor Author

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

Copy link
Member

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

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.

Copy link
Contributor Author

@i110 i110 Feb 1, 2017

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

Copy link
Contributor Author

@i110 i110 Feb 1, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kazuho done #1185

@i110 i110 force-pushed the i110/redis-session-resumption branch from 1005e2c to b576fb9 Compare February 5, 2017 01:11
@i110 i110 force-pushed the i110/redis-session-resumption branch from b576fb9 to de740d0 Compare February 6, 2017 02:21
@i110
Copy link
Contributor Author

i110 commented Feb 6, 2017

@kazuho I resolved conflicts and rebased.

@yannick yannick mentioned this pull request Mar 17, 2017
@kazuho kazuho modified the milestones: v2.3, v2.2 Mar 21, 2017
@kazuho
Copy link
Member

kazuho commented Oct 12, 2017

see also: #1305

@kazuho kazuho mentioned this pull request Oct 12, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants