Skip to content

Commit

Permalink
Add option to locally enforce payload size limit (#515)
Browse files Browse the repository at this point in the history
Add a configuration option to enforce an item size limit on the client side. This avoids sending large items
over the wire and getting rejected by the server which can cause delays. The default is 0 for no limit.
The same error code RES_E2BIG is used for the client side limit as for the server side limit.
  • Loading branch information
rposky authored Apr 27, 2023
1 parent 7348cc1 commit e39a2e6
Show file tree
Hide file tree
Showing 8 changed files with 151 additions and 3 deletions.
6 changes: 6 additions & 0 deletions memcached.ini
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,12 @@
; the default is 0
;memcached.store_retry_count = 0

; The maximum payload size in bytes that can be written.
; Writing a payload larger than the limit will result in RES_E2BIG error.
; Specifying 0 means no limit is enforced, though the server may still reject with RES_E2BIG.
; Default is 0.
;memcached.item_size_limit = 1000000

; Sets the default for consistent hashing for new connections.
; (To configure consistent hashing for session connections,
; use memcached.sess_consistent_hash instead)
Expand Down
45 changes: 45 additions & 0 deletions php_memcached.c
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ static int php_memc_list_entry(void) {
#define MEMC_OPT_STORE_RETRY_COUNT -1005
#define MEMC_OPT_USER_FLAGS -1006
#define MEMC_OPT_COMPRESSION_LEVEL -1007
#define MEMC_OPT_ITEM_SIZE_LIMIT -1008

/****************************************
Custom result codes
Expand Down Expand Up @@ -162,6 +163,7 @@ typedef struct {

zend_long store_retry_count;
zend_long set_udf_flags;
zend_long item_size_limit;

#ifdef HAVE_MEMCACHED_SASL
zend_bool has_sasl_data;
Expand Down Expand Up @@ -423,6 +425,7 @@ PHP_INI_BEGIN()
MEMC_INI_ENTRY("compression_threshold", "2000", OnUpdateLong, compression_threshold)
MEMC_INI_ENTRY("serializer", SERIALIZER_DEFAULT_NAME, OnUpdateSerializer, serializer_name)
MEMC_INI_ENTRY("store_retry_count", "0", OnUpdateLong, store_retry_count)
MEMC_INI_ENTRY("item_size_limit", "0", OnUpdateLongGEZero, item_size_limit)

MEMC_INI_BOOL ("default_consistent_hash", "0", OnUpdateBool, default_behavior.consistent_hash_enabled)
MEMC_INI_BOOL ("default_binary_protocol", "0", OnUpdateBool, default_behavior.binary_protocol_enabled)
Expand Down Expand Up @@ -1127,6 +1130,21 @@ zend_string *s_zval_to_payload(php_memc_object_t *intern, zval *value, uint32_t
return payload;
}

static
zend_bool s_is_payload_too_big(php_memc_object_t *intern, zend_string *payload)
{
php_memc_user_data_t *memc_user_data = memcached_get_user_data(intern->memc);

/* An item size limit of 0 implies no limit enforced */
if (memc_user_data->item_size_limit == 0) {
return 0;
}
if (ZSTR_LEN(payload) > memc_user_data->item_size_limit) {
return 1;
}
return 0;
}

static
zend_bool s_should_retry_write (php_memc_object_t *intern, memcached_return status)
{
Expand All @@ -1153,6 +1171,12 @@ zend_bool s_memc_write_zval (php_memc_object_t *intern, php_memc_write_op op, ze
s_memc_set_status(intern, MEMC_RES_PAYLOAD_FAILURE, 0);
return 0;
}

if (s_is_payload_too_big(intern, payload)) {
s_memc_set_status(intern, MEMCACHED_E2BIG, 0);
zend_string_release(payload);
return 0;
}
}

#define memc_write_using_fn(fn_name) payload ? fn_name(intern->memc, ZSTR_VAL(key), ZSTR_LEN(key), ZSTR_VAL(payload), ZSTR_LEN(payload), expiration, flags) : MEMC_RES_PAYLOAD_FAILURE;
Expand Down Expand Up @@ -1305,6 +1329,7 @@ static PHP_METHOD(Memcached, __construct)
memc_user_data->encoding_enabled = 0;
memc_user_data->store_retry_count = MEMC_G(store_retry_count);
memc_user_data->set_udf_flags = -1;
memc_user_data->item_size_limit = MEMC_G(item_size_limit);
memc_user_data->is_persistent = is_persistent;

memcached_set_user_data(intern->memc, memc_user_data);
Expand Down Expand Up @@ -2145,6 +2170,12 @@ static void php_memc_cas_impl(INTERNAL_FUNCTION_PARAMETERS, zend_bool by_key)
RETURN_FALSE;
}

if (s_is_payload_too_big(intern, payload)) {
intern->rescode = MEMCACHED_E2BIG;
zend_string_release(payload);
RETURN_FALSE;
}

if (by_key) {
status = memcached_cas_by_key(intern->memc, ZSTR_VAL(server_key), ZSTR_LEN(server_key), ZSTR_VAL(key), ZSTR_LEN(key), ZSTR_VAL(payload), ZSTR_LEN(payload), expiration, flags, cas);
} else {
Expand Down Expand Up @@ -2970,6 +3001,9 @@ static PHP_METHOD(Memcached, getOption)
case MEMC_OPT_COMPRESSION:
RETURN_BOOL(memc_user_data->compression_enabled);

case MEMC_OPT_ITEM_SIZE_LIMIT:
RETURN_LONG(memc_user_data->item_size_limit);

case MEMC_OPT_PREFIX_KEY:
{
memcached_return retval;
Expand Down Expand Up @@ -3041,6 +3075,15 @@ int php_memc_set_option(php_memc_object_t *intern, long option, zval *value)
}
break;

case MEMC_OPT_ITEM_SIZE_LIMIT:
lval = zval_get_long(value);
if (lval < 0) {
php_error_docref(NULL, E_WARNING, "ITEM_SIZE_LIMIT must be >= 0");
return 0;
}
memc_user_data->item_size_limit = lval;
break;

case MEMC_OPT_PREFIX_KEY:
{
zend_string *str;
Expand Down Expand Up @@ -4013,6 +4056,7 @@ PHP_GINIT_FUNCTION(php_memcached)
php_memcached_globals->memc.compression_factor = 1.30;
php_memcached_globals->memc.compression_level = 3;
php_memcached_globals->memc.store_retry_count = 2;
php_memcached_globals->memc.item_size_limit = 0;

php_memcached_globals->memc.sasl_initialised = 0;
php_memcached_globals->no_effect = 0;
Expand Down Expand Up @@ -4063,6 +4107,7 @@ static void php_memc_register_constants(INIT_FUNC_ARGS)

REGISTER_MEMC_CLASS_CONST_LONG(OPT_USER_FLAGS, MEMC_OPT_USER_FLAGS);
REGISTER_MEMC_CLASS_CONST_LONG(OPT_STORE_RETRY_COUNT, MEMC_OPT_STORE_RETRY_COUNT);
REGISTER_MEMC_CLASS_CONST_LONG(OPT_ITEM_SIZE_LIMIT, MEMC_OPT_ITEM_SIZE_LIMIT);

/*
* Indicate whether igbinary serializer is available
Expand Down
5 changes: 3 additions & 2 deletions php_memcached_private.h
Original file line number Diff line number Diff line change
Expand Up @@ -186,8 +186,9 @@ ZEND_BEGIN_MODULE_GLOBALS(php_memcached)
char *compression_name;
zend_long compression_threshold;
double compression_factor;
zend_long store_retry_count;
zend_long compression_level;
zend_long store_retry_count;
zend_long compression_level;
zend_long item_size_limit;

/* Converted values*/
php_memc_serializer_type serializer_type;
Expand Down
32 changes: 32 additions & 0 deletions tests/cas_e2big.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
--TEST--
set data exceeding size limit
--SKIPIF--
<?php include "skipif.inc";?>
--FILE--
<?php
include dirname (__FILE__) . '/config.inc';
$m = memc_get_instance (array (
Memcached::OPT_ITEM_SIZE_LIMIT => 100,
));

$m->delete('cas_e2big_test');

$m->set('cas_e2big_test', 'hello');
$result = $m->get('cas_e2big_test', null, Memcached::GET_EXTENDED);
var_dump(is_array($result) && isset($result['cas']) && isset($result['value']) && $result['value'] == 'hello');

$value = str_repeat('a large payload', 1024 * 1024);

var_dump($m->cas($result['cas'], 'cas_e2big_test', $value, 360));
var_dump($m->getResultCode() == Memcached::RES_E2BIG);
var_dump($m->getResultMessage() == 'ITEM TOO BIG');
var_dump($m->get('cas_e2big_test') == 'hello');
var_dump($m->getResultCode() == Memcached::RES_SUCCESS);
?>
--EXPECT--
bool(true)
bool(false)
bool(true)
bool(true)
bool(true)
bool(true)
32 changes: 32 additions & 0 deletions tests/options.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,26 @@ var_dump($m->getOption(Memcached::OPT_COMPRESSION_TYPE) == Memcached::COMPRESSIO

var_dump($m->setOption(Memcached::OPT_COMPRESSION_TYPE, 0));
var_dump($m->getOption(Memcached::OPT_COMPRESSION_TYPE) == Memcached::COMPRESSION_FASTLZ);

echo "item_size_limit setOption\n";
var_dump($m->setOption(Memcached::OPT_ITEM_SIZE_LIMIT, 0));
var_dump($m->getOption(Memcached::OPT_ITEM_SIZE_LIMIT) === 0);
var_dump($m->setOption(Memcached::OPT_ITEM_SIZE_LIMIT, -1));
var_dump($m->setOption(Memcached::OPT_ITEM_SIZE_LIMIT, 1000000));
var_dump($m->getOption(Memcached::OPT_ITEM_SIZE_LIMIT) == 1000000);

echo "item_size_limit ini\n";
ini_set('memcached.item_size_limit', '0');
$m = new Memcached();
var_dump($m->getOption(Memcached::OPT_ITEM_SIZE_LIMIT) === 0);

ini_set('memcached.item_size_limit', '1000000');
$m = new Memcached();
var_dump($m->getOption(Memcached::OPT_ITEM_SIZE_LIMIT) == 1000000);

ini_set('memcached.item_size_limit', null);
$m = new Memcached();
var_dump($m->getOption(Memcached::OPT_ITEM_SIZE_LIMIT) === 0);
?>
--EXPECTF--
bool(true)
Expand All @@ -41,3 +61,15 @@ bool(true)
bool(true)
bool(false)
bool(true)
item_size_limit setOption
bool(true)
bool(true)

Warning: Memcached::setOption(): ITEM_SIZE_LIMIT must be >= 0 in %s on line %d
bool(false)
bool(true)
bool(true)
item_size_limit ini
bool(true)
bool(true)
bool(true)
4 changes: 3 additions & 1 deletion tests/set_large.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ set large data
--FILE--
<?php
include dirname (__FILE__) . '/config.inc';
$m = memc_get_instance ();
$m = memc_get_instance (array (
Memcached::OPT_ITEM_SIZE_LIMIT => 0,
));

$key = 'foobarbazDEADC0DE';
$value = str_repeat("foo bar", 1024 * 1024);
Expand Down
27 changes: 27 additions & 0 deletions tests/set_large_e2big.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
--TEST--
set data exceeding size limit
--SKIPIF--
<?php include "skipif.inc";?>
--FILE--
<?php
include dirname (__FILE__) . '/config.inc';
$m = memc_get_instance (array (
Memcached::OPT_ITEM_SIZE_LIMIT => 100,
));

$m->delete('set_large_e2big_test');

$value = str_repeat('a large payload', 1024 * 1024);

var_dump($m->set('set_large_e2big_test', $value));
var_dump($m->getResultCode() == Memcached::RES_E2BIG);
var_dump($m->getResultMessage() == 'ITEM TOO BIG');
var_dump($m->get('set_large_e2big_test') === false);
var_dump($m->getResultCode() == Memcached::RES_NOTFOUND);
?>
--EXPECT--
bool(false)
bool(true)
bool(true)
bool(true)
bool(true)
3 changes: 3 additions & 0 deletions tests/setoptions.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@ var_dump($m->setOptions(array(
Memcached::OPT_COMPRESSION => 0,
Memcached::OPT_LIBKETAMA_COMPATIBLE => 1,
Memcached::OPT_CONNECT_TIMEOUT => 5000,
Memcached::OPT_ITEM_SIZE_LIMIT => 1000000,
)));

var_dump($m->getOption(Memcached::OPT_PREFIX_KEY) == 'a_prefix');
var_dump($m->getOption(Memcached::OPT_SERIALIZER) == Memcached::SERIALIZER_PHP);
var_dump($m->getOption(Memcached::OPT_COMPRESSION) == 0);
var_dump($m->getOption(Memcached::OPT_LIBKETAMA_COMPATIBLE) == 1);
var_dump($m->getOption(Memcached::OPT_ITEM_SIZE_LIMIT) == 1000000);

echo "test invalid options\n";

Expand All @@ -36,6 +38,7 @@ bool(true)
bool(true)
bool(true)
bool(true)
bool(true)
test invalid options

Warning: Memcached::setOptions(): invalid configuration option in %s on line %d
Expand Down

0 comments on commit e39a2e6

Please sign in to comment.