Permalink
554 lines (444 sloc) 13.9 KB
/*
+----------------------------------------------------------------------+
| Copyright (c) 2009-2010 The PHP Group |
+----------------------------------------------------------------------+
| This source file is subject to version 3.01 of the PHP license, |
| that is bundled with this package in the file LICENSE, and is |
| available through the world-wide-web at the following url: |
| http://www.php.net/license/3_01.txt. |
| If you did not receive a copy of the PHP license and are unable to |
| obtain it through the world-wide-web, please send a note to |
| license@php.net so we can mail you a copy immediately. |
+----------------------------------------------------------------------+
| Authors: Andrei Zmievski <andrei@php.net> |
+----------------------------------------------------------------------+
*/
#include "php_memcached.h"
#include "php_memcached_private.h"
#include "php_memcached_session.h"
#include "Zend/zend_smart_str_public.h"
extern ZEND_DECLARE_MODULE_GLOBALS(php_memcached)
#define MEMC_SESS_DEFAULT_LOCK_WAIT 150000
#define MEMC_SESS_LOCK_EXPIRATION 30
#define REALTIME_MAXDELTA 60*60*24*30
ps_module ps_mod_memcached = {
PS_MOD_UPDATE_TIMESTAMP(memcached)
};
typedef struct {
zend_bool is_persistent;
zend_bool has_sasl_data;
zend_bool is_locked;
zend_string *lock_key;
} php_memcached_user_data;
#ifndef MIN
# define MIN(a,b) (((a)<(b))?(a):(b))
#endif
#ifndef MAX
# define MAX(a,b) (((a)>(b))?(a):(b))
#endif
#ifdef ZTS
#define MEMC_SESS_INI(v) TSRMG(php_memcached_globals_id, zend_php_memcached_globals *, session.v)
#else
#define MEMC_SESS_INI(v) (php_memcached_globals.session.v)
#endif
#define MEMC_SESS_STR_INI(vv) ((MEMC_SESS_INI(vv) && *MEMC_SESS_INI(vv)) ? MEMC_SESS_INI(vv) : NULL)
static
int le_memc_sess;
static
int s_memc_sess_list_entry(void)
{
return le_memc_sess;
}
static
void s_destroy_mod_data(memcached_st *memc)
{
php_memcached_user_data *user_data = memcached_get_user_data(memc);
#ifdef HAVE_MEMCACHED_SASL
if (user_data->has_sasl_data) {
memcached_destroy_sasl_auth_data(memc);
}
#endif
memcached_free(memc);
pefree(memc, user_data->is_persistent);
pefree(user_data, user_data->is_persistent);
}
ZEND_RSRC_DTOR_FUNC(php_memc_sess_dtor)
{
if (res->ptr) {
s_destroy_mod_data((memcached_st *) res->ptr);
res->ptr = NULL;
}
}
int php_memc_session_minit(int module_number)
{
le_memc_sess =
zend_register_list_destructors_ex(NULL, php_memc_sess_dtor, "Memcached Sessions persistent connection", module_number);
php_session_register_module(ps_memcached_ptr);
return SUCCESS;
}
static
time_t s_adjust_expiration(zend_long expiration)
{
if (expiration <= REALTIME_MAXDELTA) {
return expiration;
} else {
return time(NULL) + expiration;
}
}
static
time_t s_lock_expiration()
{
if (MEMC_SESS_INI(lock_expiration) > 0) {
return s_adjust_expiration(MEMC_SESS_INI(lock_expiration));
}
else {
zend_long max_execution_time = zend_ini_long(ZEND_STRL("max_execution_time"), 0);
if (max_execution_time > 0) {
return s_adjust_expiration(max_execution_time);
}
}
return 0;
}
static
time_t s_session_expiration(zend_long maxlifetime)
{
if (maxlifetime > 0) {
return s_adjust_expiration(maxlifetime);
}
return 0;
}
static
zend_bool s_lock_session(memcached_st *memc, zend_string *sid)
{
memcached_return rc;
char *lock_key;
size_t lock_key_len;
time_t expiration;
zend_long wait_time, retries;
php_memcached_user_data *user_data = memcached_get_user_data(memc);
lock_key_len = spprintf(&lock_key, 0, "lock.%s", sid->val);
expiration = s_lock_expiration();
wait_time = MEMC_SESS_INI(lock_wait_min);
retries = MEMC_SESS_INI(lock_retries);
do {
rc = memcached_add(memc, lock_key, lock_key_len, "1", sizeof ("1") - 1, expiration, 0);
switch (rc) {
case MEMCACHED_SUCCESS:
user_data->lock_key = zend_string_init(lock_key, lock_key_len, user_data->is_persistent);
user_data->is_locked = 1;
break;
case MEMCACHED_NOTSTORED:
case MEMCACHED_DATA_EXISTS:
if (retries > 0) {
usleep(wait_time * 1000);
wait_time = MIN(MEMC_SESS_INI(lock_wait_max), wait_time * 2);
}
break;
default:
php_error_docref(NULL, E_WARNING, "Failed to write session lock: %s", memcached_strerror (memc, rc));
break;
}
} while (!user_data->is_locked && retries-- > 0);
efree(lock_key);
return user_data->is_locked;
}
static
void s_unlock_session(memcached_st *memc)
{
php_memcached_user_data *user_data = memcached_get_user_data(memc);
if (user_data->is_locked) {
memcached_delete(memc, user_data->lock_key->val, user_data->lock_key->len, 0);
user_data->is_locked = 0;
zend_string_release (user_data->lock_key);
}
}
static
zend_bool s_configure_from_ini_values(memcached_st *memc, zend_bool silent)
{
memcached_return rc;
#define check_set_behavior(behavior, value) \
if ((rc = memcached_behavior_set(memc, (behavior), (value))) != MEMCACHED_SUCCESS) { \
if (!silent) { php_error_docref(NULL, E_WARNING, "failed to initialise session memcached configuration: %s", memcached_strerror(memc, rc)); } \
return 0; \
}
if (MEMC_SESS_INI(binary_protocol_enabled)) {
check_set_behavior(MEMCACHED_BEHAVIOR_BINARY_PROTOCOL, 1);
}
if (MEMC_SESS_INI(consistent_hash_enabled)) {
check_set_behavior(MEMCACHED_BEHAVIOR_KETAMA, 1);
}
if (MEMC_SESS_INI(server_failure_limit)) {
check_set_behavior(MEMCACHED_BEHAVIOR_SERVER_FAILURE_LIMIT, MEMC_SESS_INI(server_failure_limit));
}
if (MEMC_SESS_INI(number_of_replicas)) {
check_set_behavior(MEMCACHED_BEHAVIOR_NUMBER_OF_REPLICAS, MEMC_SESS_INI(number_of_replicas));
}
if (MEMC_SESS_INI(randomize_replica_read_enabled)) {
check_set_behavior(MEMCACHED_BEHAVIOR_RANDOMIZE_REPLICA_READ, 1);
}
if (MEMC_SESS_INI(remove_failed_servers_enabled)) {
check_set_behavior(MEMCACHED_BEHAVIOR_REMOVE_FAILED_SERVERS, 1);
}
if (MEMC_SESS_INI(connect_timeout)) {
check_set_behavior(MEMCACHED_BEHAVIOR_CONNECT_TIMEOUT, MEMC_SESS_INI(connect_timeout));
}
if (MEMC_SESS_STR_INI(prefix)) {
memcached_callback_set(memc, MEMCACHED_CALLBACK_NAMESPACE, MEMC_SESS_STR_INI(prefix));
}
if (MEMC_SESS_STR_INI(sasl_username) && MEMC_SESS_STR_INI(sasl_password)) {
php_memcached_user_data *user_data;
if (!php_memc_init_sasl_if_needed()) {
return 0;
}
check_set_behavior(MEMCACHED_BEHAVIOR_BINARY_PROTOCOL, 1);
if (memcached_set_sasl_auth_data(memc, MEMC_SESS_STR_INI(sasl_username), MEMC_SESS_STR_INI(sasl_password)) == MEMCACHED_FAILURE) {
php_error_docref(NULL, E_WARNING, "failed to set memcached session sasl credentials");
return 0;
}
user_data = memcached_get_user_data(memc);
user_data->has_sasl_data = 1;
}
#undef safe_set_behavior
return 1;
}
static
void *s_pemalloc_fn(const memcached_st *memc, size_t size, void *context)
{
zend_bool *is_persistent = memcached_get_user_data(memc);
return
pemalloc(size, *is_persistent);
}
static
void s_pefree_fn(const memcached_st *memc, void *mem, void *context)
{
zend_bool *is_persistent = memcached_get_user_data(memc);
return
pefree(mem, *is_persistent);
}
static
void *s_perealloc_fn(const memcached_st *memc, void *mem, const size_t size, void *context)
{
zend_bool *is_persistent = memcached_get_user_data(memc);
return
perealloc(mem, size, *is_persistent);
}
static
void *s_pecalloc_fn(const memcached_st *memc, size_t nelem, const size_t elsize, void *context)
{
zend_bool *is_persistent = memcached_get_user_data(memc);
return
pecalloc(nelem, elsize, *is_persistent);
}
static
memcached_st *s_init_mod_data (const memcached_server_list_st servers, zend_bool is_persistent)
{
void *buffer;
php_memcached_user_data *user_data;
memcached_st *memc;
buffer = pecalloc(1, sizeof(memcached_st), is_persistent);
memc = memcached_create (buffer);
if (!memc) {
php_error_docref(NULL, E_ERROR, "failed to allocate memcached structure");
/* not reached */
}
memcached_set_memory_allocators(memc, s_pemalloc_fn, s_pefree_fn, s_perealloc_fn, s_pecalloc_fn, NULL);
user_data = pecalloc(1, sizeof(php_memcached_user_data), is_persistent);
user_data->is_persistent = is_persistent;
user_data->has_sasl_data = 0;
user_data->lock_key = NULL;
user_data->is_locked = 0;
memcached_set_user_data(memc, user_data);
memcached_server_push (memc, servers);
memcached_behavior_set(memc, MEMCACHED_BEHAVIOR_VERIFY_KEY, 1);
return memc;
}
PS_OPEN_FUNC(memcached)
{
memcached_st *memc = NULL;
char *plist_key = NULL;
size_t plist_key_len = 0;
memcached_server_list_st servers;
// First parse servers
servers = memcached_servers_parse(save_path);
if (!servers) {
php_error_docref(NULL, E_WARNING, "failed to parse session.save_path");
PS_SET_MOD_DATA(NULL);
return FAILURE;
}
if (MEMC_SESS_INI(persistent_enabled)) {
zend_resource *le_p;
plist_key_len = spprintf(&plist_key, 0, "memc-session:%s", save_path);
if ((le_p = zend_hash_str_find_ptr(&EG(persistent_list), plist_key, plist_key_len)) != NULL) {
if (le_p->type == s_memc_sess_list_entry()) {
memc = (memcached_st *) le_p->ptr;
if (!s_configure_from_ini_values(memc, 1)) {
// Remove existing plist entry
zend_hash_str_del(&EG(persistent_list), plist_key, plist_key_len);
memc = NULL;
}
else {
efree(plist_key);
PS_SET_MOD_DATA(memc);
memcached_server_list_free(servers);
return SUCCESS;
}
}
}
}
memc = s_init_mod_data(servers, MEMC_SESS_INI(persistent_enabled));
memcached_server_list_free(servers);
if (!s_configure_from_ini_values(memc, 0)) {
if (plist_key) {
efree(plist_key);
}
s_destroy_mod_data(memc);
PS_SET_MOD_DATA(NULL);
return FAILURE;
}
if (plist_key) {
zend_resource le;
le.type = s_memc_sess_list_entry();
le.ptr = memc;
GC_REFCOUNT(&le) = 1;
/* plist_key is not a persistent allocated key, thus we use str_update here */
if (zend_hash_str_update_mem(&EG(persistent_list), plist_key, plist_key_len, &le, sizeof(le)) == NULL) {
php_error_docref(NULL, E_ERROR, "Could not register persistent entry for the memcached session");
/* not reached */
}
efree(plist_key);
}
PS_SET_MOD_DATA(memc);
return SUCCESS;
}
PS_CLOSE_FUNC(memcached)
{
php_memcached_user_data *user_data;
memcached_st *memc = PS_GET_MOD_DATA();
if (!memc) {
php_error_docref(NULL, E_WARNING, "Session is not allocated, check session.save_path value");
return FAILURE;
}
user_data = memcached_get_user_data(memc);
if (user_data->is_locked) {
s_unlock_session(memc);
}
if (!user_data->is_persistent) {
s_destroy_mod_data(memc);
}
PS_SET_MOD_DATA(NULL);
return SUCCESS;
}
PS_READ_FUNC(memcached)
{
char *payload = NULL;
size_t payload_len = 0;
uint32_t flags = 0;
memcached_return status;
memcached_st *memc = PS_GET_MOD_DATA();
if (!memc) {
php_error_docref(NULL, E_WARNING, "Session is not allocated, check session.save_path value");
return FAILURE;
}
if (MEMC_SESS_INI(lock_enabled)) {
if (!s_lock_session(memc, key)) {
php_error_docref(NULL, E_WARNING, "Unable to clear session lock record");
return FAILURE;
}
}
payload = memcached_get(memc, key->val, key->len, &payload_len, &flags, &status);
if (status == MEMCACHED_SUCCESS) {
zend_bool *is_persistent = memcached_get_user_data(memc);
*val = zend_string_init(payload, payload_len, 0);
pefree(payload, *is_persistent);
return SUCCESS;
} else if (status == MEMCACHED_NOTFOUND) {
*val = ZSTR_EMPTY_ALLOC();
return SUCCESS;
} else {
php_error_docref(NULL TSRMLS_CC, E_WARNING, "error getting session from memcached: %s", memcached_last_error_message(memc));
return FAILURE;
}
}
PS_WRITE_FUNC(memcached)
{
zend_long retries = 1;
memcached_st *memc = PS_GET_MOD_DATA();
time_t expiration = s_session_expiration(maxlifetime);
if (!memc) {
php_error_docref(NULL, E_WARNING, "Session is not allocated, check session.save_path value");
return FAILURE;
}
/* Set the number of write retry attempts to the number of replicas times the number of attempts to remove a server plus the initial write */
if (MEMC_SESS_INI(remove_failed_servers_enabled)) {
zend_long replicas, failure_limit;
replicas = memcached_behavior_get(memc, MEMCACHED_BEHAVIOR_NUMBER_OF_REPLICAS);
failure_limit = memcached_behavior_get(memc, MEMCACHED_BEHAVIOR_SERVER_FAILURE_LIMIT);
retries = 1 + replicas * (failure_limit + 1);
}
do {
if (memcached_set(memc, key->val, key->len, val->val, val->len, expiration, 0) == MEMCACHED_SUCCESS) {
return SUCCESS;
} else {
php_error_docref(NULL TSRMLS_CC, E_WARNING, "error saving session to memcached: %s", memcached_last_error_message(memc));
}
} while (--retries > 0);
return FAILURE;
}
PS_DESTROY_FUNC(memcached)
{
php_memcached_user_data *user_data;
memcached_st *memc = PS_GET_MOD_DATA();
if (!memc) {
php_error_docref(NULL, E_WARNING, "Session is not allocated, check session.save_path value");
return FAILURE;
}
memcached_delete(memc, key->val, key->len, 0);
user_data = memcached_get_user_data(memc);
if (user_data->is_locked) {
s_unlock_session(memc);
}
return SUCCESS;
}
PS_GC_FUNC(memcached)
{
return SUCCESS;
}
PS_CREATE_SID_FUNC(memcached)
{
zend_string *sid;
memcached_st *memc = PS_GET_MOD_DATA();
if (!memc) {
sid = php_session_create_id(NULL);
}
else {
int retries = 3;
while (retries-- > 0) {
sid = php_session_create_id((void **) &memc);
if (memcached_add (memc, sid->val, sid->len, NULL, 0, s_lock_expiration(), 0) == MEMCACHED_SUCCESS) {
break;
}
zend_string_release(sid);
sid = NULL;
}
}
return sid;
}
PS_VALIDATE_SID_FUNC(memcached)
{
memcached_st *memc = PS_GET_MOD_DATA();
if (php_memcached_exist(memc, key) == MEMCACHED_SUCCESS) {
return SUCCESS;
} else {
return FAILURE;
}
}
PS_UPDATE_TIMESTAMP_FUNC(memcached)
{
memcached_st *memc = PS_GET_MOD_DATA();
time_t expiration = s_session_expiration(maxlifetime);
if (php_memcached_touch(memc, key->val, key->len, expiration) == MEMCACHED_FAILURE) {
return FAILURE;
}
return SUCCESS;
}
/* }}} */