Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 2612d44

Browse files
Update RedisCluster scan logic for large SCAN cursors.
We also need to update the `RedisCluster` logic to handle very large curosr values, in addition to handling them for the `Redis` and `RedisArray` classes. See #2454, #2458
1 parent e52f0af commit 2612d44

9 files changed

Lines changed: 87 additions & 93 deletions

cluster_library.c

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2279,8 +2279,9 @@ PHP_REDIS_API void cluster_gen_mbulk_resp(INTERNAL_FUNCTION_PARAMETERS,
22792279
}
22802280

22812281
/* HSCAN, SSCAN, ZSCAN */
2282-
PHP_REDIS_API int cluster_scan_resp(INTERNAL_FUNCTION_PARAMETERS, redisCluster *c,
2283-
REDIS_SCAN_TYPE type, long *it)
2282+
PHP_REDIS_API int
2283+
cluster_scan_resp(INTERNAL_FUNCTION_PARAMETERS, redisCluster *c,
2284+
REDIS_SCAN_TYPE type, uint64_t *cursor)
22842285
{
22852286
char *pit;
22862287

@@ -2304,12 +2305,11 @@ PHP_REDIS_API int cluster_scan_resp(INTERNAL_FUNCTION_PARAMETERS, redisCluster *
23042305
}
23052306

23062307
// Push the new iterator value to our caller
2307-
*it = atol(pit);
2308+
*cursor = strtoull(pit, NULL, 10);
23082309
efree(pit);
23092310

23102311
// We'll need another MULTIBULK response for the payload
2311-
if (cluster_check_response(c, &c->reply_type) < 0)
2312-
{
2312+
if (cluster_check_response(c, &c->reply_type) < 0) {
23132313
return FAILURE;
23142314
}
23152315

cluster_library.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -488,7 +488,7 @@ PHP_REDIS_API void cluster_msetnx_resp(INTERNAL_FUNCTION_PARAMETERS,
488488

489489
/* Response handler for ZSCAN, SSCAN, and HSCAN */
490490
PHP_REDIS_API int cluster_scan_resp(INTERNAL_FUNCTION_PARAMETERS,
491-
redisCluster *c, REDIS_SCAN_TYPE type, long *it);
491+
redisCluster *c, REDIS_SCAN_TYPE type, uint64_t *cursor);
492492

493493
/* INFO response handler */
494494
PHP_REDIS_API void cluster_info_resp(INTERNAL_FUNCTION_PARAMETERS,

library.c

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4512,4 +4512,45 @@ void redis_conf_auth(HashTable *ht, const char *key, size_t keylen,
45124512
redis_extract_auth_info(zv, user, pass);
45134513
}
45144514

4515+
/* Update a zval with the current 64 bit scan cursor. This presents a problem
4516+
* because we can only represent up to 63 bits in a PHP integer. So depending
4517+
* on the cursor value, we may need to represent it as a string. */
4518+
void redisSetScanCursor(zval *zv, uint64_t cursor) {
4519+
char tmp[21];
4520+
size_t len;
4521+
4522+
ZEND_ASSERT(zv != NULL && (Z_TYPE_P(zv) == IS_LONG ||
4523+
Z_TYPE_P(zv) == IS_STRING));
4524+
4525+
if (Z_TYPE_P(zv) == IS_STRING)
4526+
zend_string_release(Z_STR_P(zv));
4527+
4528+
if (cursor > ZEND_LONG_MAX) {
4529+
len = snprintf(tmp, sizeof(tmp), "%llu", (unsigned long long)cursor);
4530+
ZVAL_STRINGL(zv, tmp, len);
4531+
} else {
4532+
ZVAL_LONG(zv, cursor);
4533+
}
4534+
}
4535+
4536+
/* Get a Redis SCAN cursor value out of a zval. These are always taken as a
4537+
* reference argument that that must be `null`, `int`, or `string`. */
4538+
uint64_t redisGetScanCursor(zval *zv, zend_bool *was_zero) {
4539+
ZEND_ASSERT(zv != NULL && (Z_TYPE_P(zv) == IS_LONG ||
4540+
Z_TYPE_P(zv) == IS_STRING ||
4541+
Z_TYPE_P(zv) == IS_NULL));
4542+
4543+
if (Z_TYPE_P(zv) == IS_NULL) {
4544+
convert_to_long(zv);
4545+
*was_zero = 0;
4546+
return 0;
4547+
} else if (Z_TYPE_P(zv) == IS_STRING) {
4548+
*was_zero = Z_STRLEN_P(zv) == 1 && Z_STRVAL_P(zv)[0] == '0';
4549+
return strtoull(Z_STRVAL_P(zv), NULL, 10);
4550+
} else {
4551+
*was_zero = Z_LVAL_P(zv) == 0;
4552+
return Z_LVAL_P(zv);
4553+
}
4554+
}
4555+
45154556
/* vim: set tabstop=4 softtabstop=4 expandtab shiftwidth=4: */

library.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,8 +100,10 @@ PHP_REDIS_API int redis_mbulk_reply_zipped_keys_dbl(INTERNAL_FUNCTION_PARAMETERS
100100
PHP_REDIS_API int redis_mbulk_reply_assoc(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock, zval *z_tab, void *ctx);
101101
PHP_REDIS_API int redis_mbulk_reply_double(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock, zval *z_tab, void *ctx);
102102

103-
PHP_REDIS_API int redis_sock_read_scan_reply(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock, REDIS_SCAN_TYPE type, uint64_t *cursor);
103+
void redisSetScanCursor(zval *zv, uint64_t cursor);
104+
uint64_t redisGetScanCursor(zval *zv, zend_bool *was_zero);
104105

106+
PHP_REDIS_API int redis_sock_read_scan_reply(INTERNAL_FUNCTION_PARAMETERS, RedisSock *redis_sock, REDIS_SCAN_TYPE type, uint64_t *cursor);
105107

106108
PHP_REDIS_API int redis_xrange_reply(INTERNAL_FUNCTION_PARAMETERS,
107109
RedisSock *redis_sock, zval *z_tab, void *ctx);

redis.c

Lines changed: 5 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -2744,40 +2744,6 @@ redis_build_scan_cmd(char **cmd, REDIS_SCAN_TYPE type, char *key, int key_len,
27442744
return cmdstr.len;
27452745
}
27462746

2747-
/* Update a zval with the current 64 bit scan cursor. This presents a problem
2748-
* because we can only represent up to 63 bits in a PHP integer. So depending
2749-
* on the cursor value, we may need to represent it as a string. */
2750-
static void updateScanCursorZVal(zval *zv, uint64_t cursor) {
2751-
char tmp[21];
2752-
size_t len;
2753-
2754-
ZEND_ASSERT(zv != NULL && (Z_TYPE_P(zv) == IS_LONG ||
2755-
Z_TYPE_P(zv) == IS_STRING));
2756-
2757-
if (Z_TYPE_P(zv) == IS_STRING)
2758-
zend_string_release(Z_STR_P(zv));
2759-
2760-
if (cursor > ZEND_LONG_MAX) {
2761-
len = snprintf(tmp, sizeof(tmp), "%llu", (unsigned long long)cursor);
2762-
ZVAL_STRINGL(zv, tmp, len);
2763-
} else {
2764-
ZVAL_LONG(zv, cursor);
2765-
}
2766-
}
2767-
2768-
static uint64_t getScanCursorZVal(zval *zv, zend_bool *was_zero) {
2769-
ZEND_ASSERT(zv != NULL && (Z_TYPE_P(zv) == IS_LONG ||
2770-
Z_TYPE_P(zv) == IS_STRING));;
2771-
2772-
if (Z_TYPE_P(zv) == IS_STRING) {
2773-
*was_zero = Z_STRLEN_P(zv) == 1 && Z_STRVAL_P(zv)[0] == '0';
2774-
return strtoull(Z_STRVAL_P(zv), NULL, 10);
2775-
} else {
2776-
*was_zero = Z_LVAL_P(zv) == 0;
2777-
return Z_LVAL_P(zv);
2778-
}
2779-
}
2780-
27812747
/* {{{ proto redis::scan(&$iterator, [pattern, [count, [type]]]) */
27822748
PHP_REDIS_API void
27832749
generic_scan_cmd(INTERNAL_FUNCTION_PARAMETERS, REDIS_SCAN_TYPE type) {
@@ -2825,18 +2791,10 @@ generic_scan_cmd(INTERNAL_FUNCTION_PARAMETERS, REDIS_SCAN_TYPE type) {
28252791
RETURN_FALSE;
28262792
}
28272793

2828-
/* If our cursor is NULL (it can only be null|int|string), convert it to a
2829-
* long and initialize it to zero for oure initial SCAN. Otherwise et the
2830-
* uint64_t value from the zval which can either be in the form of a long or
2831-
* a string (if the cursor is too large to fit in a zend_long). */
2832-
if (Z_TYPE_P(z_cursor) == IS_NULL) {
2833-
convert_to_long(z_cursor);
2834-
cursor = 0;
2835-
} else {
2836-
cursor = getScanCursorZVal(z_cursor, &completed);
2837-
if (completed)
2838-
RETURN_FALSE;
2839-
}
2794+
/* Get our SCAN cursor short circuiting if we're done */
2795+
cursor = redisGetScanCursor(z_cursor, &completed);
2796+
if (completed)
2797+
RETURN_FALSE;
28402798

28412799
/* Prefix our key if we've got one and we have a prefix set */
28422800
if(key_len) {
@@ -2889,7 +2847,7 @@ generic_scan_cmd(INTERNAL_FUNCTION_PARAMETERS, REDIS_SCAN_TYPE type) {
28892847
if(key_free) efree(key);
28902848

28912849
/* Update our iterator reference */
2892-
updateScanCursorZVal(z_cursor, cursor);
2850+
redisSetScanCursor(z_cursor, cursor);
28932851
}
28942852

28952853
PHP_METHOD(Redis, scan) {

redis_cluster.c

Lines changed: 22 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2266,8 +2266,10 @@ static void cluster_kscan_cmd(INTERNAL_FUNCTION_PARAMETERS,
22662266
short slot;
22672267
zval *z_it;
22682268
HashTable *hash;
2269-
long it, num_ele;
2269+
long num_ele;
22702270
zend_long count = 0;
2271+
zend_bool complted;
2272+
uint64_t cursor;
22712273

22722274
// Can't be in MULTI mode
22732275
if (!CLUSTER_IS_ATOMIC(c)) {
@@ -2285,16 +2287,10 @@ static void cluster_kscan_cmd(INTERNAL_FUNCTION_PARAMETERS,
22852287
/* Treat as readonly */
22862288
c->readonly = 1;
22872289

2288-
// Convert iterator to long if it isn't, update our long iterator if it's
2289-
// set and >0, and finish if it's back to zero
2290-
if (Z_TYPE_P(z_it) != IS_LONG || Z_LVAL_P(z_it) < 0) {
2291-
convert_to_long(z_it);
2292-
it = 0;
2293-
} else if (Z_LVAL_P(z_it) != 0) {
2294-
it = Z_LVAL_P(z_it);
2295-
} else {
2290+
/* Get our scan cursor and return early if we're done */
2291+
cursor = redisGetScanCursor(z_it, &complted);
2292+
if (complted)
22962293
RETURN_FALSE;
2297-
}
22982294

22992295
// Apply any key prefix we have, get the slot
23002296
key_free = redis_key_prefix(c->flags, &key, &key_len);
@@ -2314,7 +2310,7 @@ static void cluster_kscan_cmd(INTERNAL_FUNCTION_PARAMETERS,
23142310
}
23152311

23162312
// Create command
2317-
cmd_len = redis_fmt_scan_cmd(&cmd, type, key, key_len, it, pat, pat_len,
2313+
cmd_len = redis_fmt_scan_cmd(&cmd, type, key, key_len, cursor, pat, pat_len,
23182314
count);
23192315

23202316
// Send it off
@@ -2328,7 +2324,7 @@ static void cluster_kscan_cmd(INTERNAL_FUNCTION_PARAMETERS,
23282324

23292325
// Read response
23302326
if (cluster_scan_resp(INTERNAL_FUNCTION_PARAM_PASSTHRU, c, type,
2331-
&it) == FAILURE)
2327+
&cursor) == FAILURE)
23322328
{
23332329
CLUSTER_THROW_EXCEPTION("Couldn't read SCAN response", 0);
23342330
if (key_free) efree(key);
@@ -2342,7 +2338,7 @@ static void cluster_kscan_cmd(INTERNAL_FUNCTION_PARAMETERS,
23422338

23432339
// Free our command
23442340
efree(cmd);
2345-
} while (c->flags->scan & REDIS_SCAN_RETRY && it != 0 && num_ele == 0);
2341+
} while (c->flags->scan & REDIS_SCAN_RETRY && cursor != 0 && num_ele == 0);
23462342

23472343
// Free our pattern
23482344
if (pat_free) efree(pat);
@@ -2351,7 +2347,7 @@ static void cluster_kscan_cmd(INTERNAL_FUNCTION_PARAMETERS,
23512347
if (key_free) efree(key);
23522348

23532349
// Update iterator reference
2354-
Z_LVAL_P(z_it) = it;
2350+
redisSetScanCursor(z_it, cursor);
23552351
}
23562352

23572353
static int redis_acl_op_readonly(zend_string *op) {
@@ -2445,9 +2441,11 @@ PHP_METHOD(RedisCluster, scan) {
24452441
size_t pat_len = 0;
24462442
int cmd_len;
24472443
short slot;
2448-
zval *z_it, *z_node;
2449-
long it, num_ele, pat_free = 0;
2444+
zval *zcursor, *z_node;
2445+
long num_ele, pat_free = 0;
24502446
zend_long count = 0;
2447+
zend_bool completed;
2448+
uint64_t cursor;
24512449

24522450
/* Treat as read-only */
24532451
c->readonly = CLUSTER_IS_ATOMIC(c);
@@ -2459,21 +2457,16 @@ PHP_METHOD(RedisCluster, scan) {
24592457
}
24602458

24612459
/* Parse arguments */
2462-
if (zend_parse_parameters(ZEND_NUM_ARGS(), "z/z|s!l", &z_it,
2460+
if (zend_parse_parameters(ZEND_NUM_ARGS(), "z/z|s!l", &zcursor,
24632461
&z_node, &pat, &pat_len, &count) == FAILURE)
24642462
{
24652463
RETURN_FALSE;
24662464
}
24672465

2468-
/* Convert or update iterator */
2469-
if (Z_TYPE_P(z_it) != IS_LONG || Z_LVAL_P(z_it) < 0) {
2470-
convert_to_long(z_it);
2471-
it = 0;
2472-
} else if (Z_LVAL_P(z_it) != 0) {
2473-
it = Z_LVAL_P(z_it);
2474-
} else {
2466+
/* Get the scan cursor and return early if we're done */
2467+
cursor = redisGetScanCursor(zcursor, &completed);
2468+
if (completed)
24752469
RETURN_FALSE;
2476-
}
24772470

24782471
if (c->flags->scan & REDIS_SCAN_PREFIX) {
24792472
pat_free = redis_key_prefix(c->flags, &pat, &pat_len);
@@ -2489,7 +2482,7 @@ PHP_METHOD(RedisCluster, scan) {
24892482
}
24902483

24912484
/* Construct our command */
2492-
cmd_len = redis_fmt_scan_cmd(&cmd, TYPE_SCAN, NULL, 0, it, pat, pat_len,
2485+
cmd_len = redis_fmt_scan_cmd(&cmd, TYPE_SCAN, NULL, 0, cursor, pat, pat_len,
24932486
count);
24942487

24952488
if ((slot = cluster_cmd_get_slot(c, z_node)) < 0) {
@@ -2505,7 +2498,7 @@ PHP_METHOD(RedisCluster, scan) {
25052498
}
25062499

25072500
if (cluster_scan_resp(INTERNAL_FUNCTION_PARAM_PASSTHRU, c, TYPE_SCAN,
2508-
&it) == FAILURE || Z_TYPE_P(return_value)!=IS_ARRAY)
2501+
&cursor) == FAILURE || Z_TYPE_P(return_value) != IS_ARRAY)
25092502
{
25102503
CLUSTER_THROW_EXCEPTION("Couldn't process SCAN response from node", 0);
25112504
efree(cmd);
@@ -2515,11 +2508,11 @@ PHP_METHOD(RedisCluster, scan) {
25152508
efree(cmd);
25162509

25172510
num_ele = zend_hash_num_elements(Z_ARRVAL_P(return_value));
2518-
} while (c->flags->scan & REDIS_SCAN_RETRY && it != 0 && num_ele == 0);
2511+
} while (c->flags->scan & REDIS_SCAN_RETRY && cursor != 0 && num_ele == 0);
25192512

25202513
if (pat_free) efree(pat);
25212514

2522-
Z_LVAL_P(z_it) = it;
2515+
redisSetScanCursor(zcursor, cursor);
25232516
}
25242517
/* }}} */
25252518

redis_cluster.stub.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -488,7 +488,7 @@ public function hmset(string $key, array $key_values): RedisCluster|bool;
488488
/**
489489
* @see Redis::hscan
490490
*/
491-
public function hscan(string $key, ?int &$iterator, ?string $pattern = null, int $count = 0): array|bool;
491+
public function hscan(string $key, null|int|string &$iterator, ?string $pattern = null, int $count = 0): array|bool;
492492

493493
/**
494494
* @see https://redis.io/commands/hrandfield
@@ -787,7 +787,7 @@ public function save(string|array $key_or_address): RedisCluster|bool;
787787
/**
788788
* @see Redis::scan
789789
*/
790-
public function scan(?int &$iterator, string|array $key_or_address, ?string $pattern = null, int $count = 0): bool|array;
790+
public function scan(null|int|string &$iterator, string|array $key_or_address, ?string $pattern = null, int $count = 0): bool|array;
791791

792792
/**
793793
* @see Redis::scard
@@ -907,7 +907,7 @@ public function srem(string $key, mixed $value, mixed ...$other_values): RedisCl
907907
/**
908908
* @see Redis::sscan
909909
*/
910-
public function sscan(string $key, ?int &$iterator, ?string $pattern = null, int $count = 0): array|false;
910+
public function sscan(string $key, null|int|string &$iterator, ?string $pattern = null, int $count = 0): array|false;
911911

912912
/**
913913
* @see Redis::strlen
@@ -1154,7 +1154,7 @@ public function zrevrank(string $key, mixed $member): RedisCluster|int|false;
11541154
/**
11551155
* @see Redis::zscan
11561156
*/
1157-
public function zscan(string $key, ?int &$iterator, ?string $pattern = null, int $count = 0): RedisCluster|bool|array;
1157+
public function zscan(string $key, null|int|string &$iterator, ?string $pattern = null, int $count = 0): RedisCluster|bool|array;
11581158

11591159
/**
11601160
* @see Redis::zscore

redis_cluster_arginfo.h

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/* This is a generated file, edit the .stub.php file instead.
2-
* Stub hash: 35b71fe87bbd8df3a7495e14be957b18c3241a19 */
2+
* Stub hash: c19108e54b637b6c76a529c1285104a0c38da220 */
33

44
ZEND_BEGIN_ARG_INFO_EX(arginfo_class_RedisCluster___construct, 0, 0, 1)
55
ZEND_ARG_TYPE_INFO(0, name, IS_STRING, 1)
@@ -406,7 +406,7 @@ ZEND_END_ARG_INFO()
406406

407407
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_MASK_EX(arginfo_class_RedisCluster_hscan, 0, 2, MAY_BE_ARRAY|MAY_BE_BOOL)
408408
ZEND_ARG_TYPE_INFO(0, key, IS_STRING, 0)
409-
ZEND_ARG_TYPE_INFO(1, iterator, IS_LONG, 1)
409+
ZEND_ARG_TYPE_MASK(1, iterator, MAY_BE_NULL|MAY_BE_LONG|MAY_BE_STRING, NULL)
410410
ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, pattern, IS_STRING, 1, "null")
411411
ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, count, IS_LONG, 0, "0")
412412
ZEND_END_ARG_INFO()
@@ -660,7 +660,7 @@ ZEND_END_ARG_INFO()
660660
#define arginfo_class_RedisCluster_save arginfo_class_RedisCluster_bgrewriteaof
661661

662662
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_MASK_EX(arginfo_class_RedisCluster_scan, 0, 2, MAY_BE_BOOL|MAY_BE_ARRAY)
663-
ZEND_ARG_TYPE_INFO(1, iterator, IS_LONG, 1)
663+
ZEND_ARG_TYPE_MASK(1, iterator, MAY_BE_NULL|MAY_BE_LONG|MAY_BE_STRING, NULL)
664664
ZEND_ARG_TYPE_MASK(0, key_or_address, MAY_BE_STRING|MAY_BE_ARRAY, NULL)
665665
ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, pattern, IS_STRING, 1, "null")
666666
ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, count, IS_LONG, 0, "0")
@@ -762,7 +762,7 @@ ZEND_END_ARG_INFO()
762762

763763
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_MASK_EX(arginfo_class_RedisCluster_sscan, 0, 2, MAY_BE_ARRAY|MAY_BE_FALSE)
764764
ZEND_ARG_TYPE_INFO(0, key, IS_STRING, 0)
765-
ZEND_ARG_TYPE_INFO(1, iterator, IS_LONG, 1)
765+
ZEND_ARG_TYPE_MASK(1, iterator, MAY_BE_NULL|MAY_BE_LONG|MAY_BE_STRING, NULL)
766766
ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, pattern, IS_STRING, 1, "null")
767767
ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, count, IS_LONG, 0, "0")
768768
ZEND_END_ARG_INFO()
@@ -1004,7 +1004,7 @@ ZEND_END_ARG_INFO()
10041004

10051005
ZEND_BEGIN_ARG_WITH_RETURN_OBJ_TYPE_MASK_EX(arginfo_class_RedisCluster_zscan, 0, 2, RedisCluster, MAY_BE_BOOL|MAY_BE_ARRAY)
10061006
ZEND_ARG_TYPE_INFO(0, key, IS_STRING, 0)
1007-
ZEND_ARG_TYPE_INFO(1, iterator, IS_LONG, 1)
1007+
ZEND_ARG_TYPE_MASK(1, iterator, MAY_BE_NULL|MAY_BE_LONG|MAY_BE_STRING, NULL)
10081008
ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, pattern, IS_STRING, 1, "null")
10091009
ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, count, IS_LONG, 0, "0")
10101010
ZEND_END_ARG_INFO()

redis_cluster_legacy_arginfo.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/* This is a generated file, edit the .stub.php file instead.
2-
* Stub hash: 35b71fe87bbd8df3a7495e14be957b18c3241a19 */
2+
* Stub hash: c19108e54b637b6c76a529c1285104a0c38da220 */
33

44
ZEND_BEGIN_ARG_INFO_EX(arginfo_class_RedisCluster___construct, 0, 0, 1)
55
ZEND_ARG_INFO(0, name)

0 commit comments

Comments
 (0)