[206] | 1 | /*
|
---|
| 2 | Unix SMB/CIFS implementation.
|
---|
| 3 |
|
---|
| 4 | Generic, persistent and shared between processes cache mechanism for use
|
---|
| 5 | by various parts of the Samba code
|
---|
| 6 |
|
---|
| 7 | Copyright (C) Rafal Szczesniak 2002
|
---|
| 8 |
|
---|
| 9 | This program is free software; you can redistribute it and/or modify
|
---|
| 10 | it under the terms of the GNU General Public License as published by
|
---|
| 11 | the Free Software Foundation; either version 3 of the License, or
|
---|
| 12 | (at your option) any later version.
|
---|
| 13 |
|
---|
| 14 | This program is distributed in the hope that it will be useful,
|
---|
| 15 | but WITHOUT ANY WARRANTY; without even the implied warranty of
|
---|
| 16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
---|
| 17 | GNU General Public License for more details.
|
---|
| 18 |
|
---|
| 19 | You should have received a copy of the GNU General Public License
|
---|
| 20 | along with this program. If not, see <http://www.gnu.org/licenses/>.
|
---|
| 21 | */
|
---|
| 22 |
|
---|
| 23 | #include "includes.h"
|
---|
| 24 |
|
---|
| 25 | #undef DBGC_CLASS
|
---|
| 26 | #define DBGC_CLASS DBGC_TDB
|
---|
| 27 |
|
---|
| 28 | #define TIMEOUT_LEN 12
|
---|
| 29 | #define CACHE_DATA_FMT "%12u/%s"
|
---|
| 30 | #define READ_CACHE_DATA_FMT_TEMPLATE "%%12u/%%%us"
|
---|
| 31 | #define BLOB_TYPE "DATA_BLOB"
|
---|
| 32 | #define BLOB_TYPE_LEN 9
|
---|
| 33 |
|
---|
| 34 | static TDB_CONTEXT *cache;
|
---|
| 35 |
|
---|
| 36 | /**
|
---|
| 37 | * @file gencache.c
|
---|
| 38 | * @brief Generic, persistent and shared between processes cache mechanism
|
---|
| 39 | * for use by various parts of the Samba code
|
---|
| 40 | *
|
---|
| 41 | **/
|
---|
| 42 |
|
---|
| 43 |
|
---|
| 44 | /**
|
---|
| 45 | * Cache initialisation function. Opens cache tdb file or creates
|
---|
| 46 | * it if does not exist.
|
---|
| 47 | *
|
---|
| 48 | * @return true on successful initialisation of the cache or
|
---|
| 49 | * false on failure
|
---|
| 50 | **/
|
---|
| 51 |
|
---|
| 52 | bool gencache_init(void)
|
---|
| 53 | {
|
---|
| 54 | char* cache_fname = NULL;
|
---|
| 55 |
|
---|
| 56 | /* skip file open if it's already opened */
|
---|
| 57 | if (cache) return True;
|
---|
| 58 |
|
---|
| 59 | cache_fname = lock_path("gencache.tdb");
|
---|
| 60 |
|
---|
| 61 | DEBUG(5, ("Opening cache file at %s\n", cache_fname));
|
---|
| 62 |
|
---|
| 63 | cache = tdb_open_log(cache_fname, 0, TDB_DEFAULT,
|
---|
| 64 | O_RDWR|O_CREAT, 0644);
|
---|
| 65 |
|
---|
| 66 | if (!cache && (errno == EACCES)) {
|
---|
| 67 | cache = tdb_open_log(cache_fname, 0, TDB_DEFAULT, O_RDONLY, 0644);
|
---|
| 68 | if (cache) {
|
---|
| 69 | DEBUG(5, ("gencache_init: Opening cache file %s read-only.\n", cache_fname));
|
---|
| 70 | }
|
---|
| 71 | }
|
---|
| 72 |
|
---|
| 73 | if (!cache) {
|
---|
| 74 | DEBUG(5, ("Attempt to open gencache.tdb has failed.\n"));
|
---|
| 75 | return False;
|
---|
| 76 | }
|
---|
| 77 | return True;
|
---|
| 78 | }
|
---|
| 79 |
|
---|
| 80 |
|
---|
| 81 | /**
|
---|
| 82 | * Cache shutdown function. Closes opened cache tdb file.
|
---|
| 83 | *
|
---|
| 84 | * @return true on successful closing the cache or
|
---|
| 85 | * false on failure during cache shutdown
|
---|
| 86 | **/
|
---|
| 87 |
|
---|
| 88 | bool gencache_shutdown(void)
|
---|
| 89 | {
|
---|
| 90 | int ret;
|
---|
| 91 | /* tdb_close routine returns -1 on error */
|
---|
| 92 | if (!cache) return False;
|
---|
| 93 | DEBUG(5, ("Closing cache file\n"));
|
---|
| 94 | ret = tdb_close(cache);
|
---|
| 95 | cache = NULL;
|
---|
| 96 | return ret != -1;
|
---|
| 97 | }
|
---|
| 98 |
|
---|
| 99 |
|
---|
| 100 | /**
|
---|
| 101 | * Set an entry in the cache file. If there's no such
|
---|
| 102 | * one, then add it.
|
---|
| 103 | *
|
---|
| 104 | * @param keystr string that represents a key of this entry
|
---|
| 105 | * @param value text representation value being cached
|
---|
| 106 | * @param timeout time when the value is expired
|
---|
| 107 | *
|
---|
| 108 | * @retval true when entry is successfuly stored
|
---|
| 109 | * @retval false on failure
|
---|
| 110 | **/
|
---|
| 111 |
|
---|
| 112 | bool gencache_set(const char *keystr, const char *value, time_t timeout)
|
---|
| 113 | {
|
---|
| 114 | int ret;
|
---|
| 115 | TDB_DATA databuf;
|
---|
| 116 | char* valstr = NULL;
|
---|
| 117 |
|
---|
| 118 | /* fail completely if get null pointers passed */
|
---|
| 119 | SMB_ASSERT(keystr && value);
|
---|
| 120 |
|
---|
| 121 | if (!gencache_init()) return False;
|
---|
| 122 |
|
---|
| 123 | if (asprintf(&valstr, CACHE_DATA_FMT, (int)timeout, value) == -1) {
|
---|
| 124 | return False;
|
---|
| 125 | }
|
---|
| 126 |
|
---|
| 127 | databuf = string_term_tdb_data(valstr);
|
---|
| 128 | DEBUG(10, ("Adding cache entry with key = %s; value = %s and timeout ="
|
---|
| 129 | " %s (%d seconds %s)\n", keystr, value,ctime(&timeout),
|
---|
| 130 | (int)(timeout - time(NULL)),
|
---|
| 131 | timeout > time(NULL) ? "ahead" : "in the past"));
|
---|
| 132 |
|
---|
| 133 | ret = tdb_store_bystring(cache, keystr, databuf, 0);
|
---|
| 134 | SAFE_FREE(valstr);
|
---|
| 135 |
|
---|
| 136 | return ret == 0;
|
---|
| 137 | }
|
---|
| 138 |
|
---|
| 139 | /**
|
---|
| 140 | * Delete one entry from the cache file.
|
---|
| 141 | *
|
---|
| 142 | * @param keystr string that represents a key of this entry
|
---|
| 143 | *
|
---|
| 144 | * @retval true upon successful deletion
|
---|
| 145 | * @retval false in case of failure
|
---|
| 146 | **/
|
---|
| 147 |
|
---|
| 148 | bool gencache_del(const char *keystr)
|
---|
| 149 | {
|
---|
| 150 | int ret;
|
---|
| 151 |
|
---|
| 152 | /* fail completely if get null pointers passed */
|
---|
| 153 | SMB_ASSERT(keystr);
|
---|
| 154 |
|
---|
| 155 | if (!gencache_init()) return False;
|
---|
| 156 |
|
---|
| 157 | DEBUG(10, ("Deleting cache entry (key = %s)\n", keystr));
|
---|
| 158 | ret = tdb_delete_bystring(cache, keystr);
|
---|
| 159 |
|
---|
| 160 | return ret == 0;
|
---|
| 161 | }
|
---|
| 162 |
|
---|
| 163 |
|
---|
| 164 | /**
|
---|
| 165 | * Get existing entry from the cache file.
|
---|
| 166 | *
|
---|
| 167 | * @param keystr string that represents a key of this entry
|
---|
| 168 | * @param valstr buffer that is allocated and filled with the entry value
|
---|
| 169 | * buffer's disposing must be done outside
|
---|
| 170 | * @param timeout pointer to a time_t that is filled with entry's
|
---|
| 171 | * timeout
|
---|
| 172 | *
|
---|
| 173 | * @retval true when entry is successfuly fetched
|
---|
| 174 | * @retval False for failure
|
---|
| 175 | **/
|
---|
| 176 |
|
---|
| 177 | bool gencache_get(const char *keystr, char **valstr, time_t *timeout)
|
---|
| 178 | {
|
---|
| 179 | TDB_DATA databuf;
|
---|
| 180 | time_t t;
|
---|
| 181 | char *endptr;
|
---|
| 182 |
|
---|
| 183 | /* fail completely if get null pointers passed */
|
---|
| 184 | SMB_ASSERT(keystr);
|
---|
| 185 |
|
---|
| 186 | if (!gencache_init()) {
|
---|
| 187 | return False;
|
---|
| 188 | }
|
---|
| 189 |
|
---|
| 190 | databuf = tdb_fetch_bystring(cache, keystr);
|
---|
| 191 |
|
---|
| 192 | if (databuf.dptr == NULL) {
|
---|
| 193 | DEBUG(10, ("Cache entry with key = %s couldn't be found\n",
|
---|
| 194 | keystr));
|
---|
| 195 | return False;
|
---|
| 196 | }
|
---|
| 197 |
|
---|
| 198 | t = strtol((const char *)databuf.dptr, &endptr, 10);
|
---|
| 199 |
|
---|
| 200 | if ((endptr == NULL) || (*endptr != '/')) {
|
---|
| 201 | DEBUG(2, ("Invalid gencache data format: %s\n", databuf.dptr));
|
---|
| 202 | SAFE_FREE(databuf.dptr);
|
---|
| 203 | return False;
|
---|
| 204 | }
|
---|
| 205 |
|
---|
| 206 | DEBUG(10, ("Returning %s cache entry: key = %s, value = %s, "
|
---|
| 207 | "timeout = %s", t > time(NULL) ? "valid" :
|
---|
| 208 | "expired", keystr, endptr+1, ctime(&t)));
|
---|
| 209 |
|
---|
| 210 | if (t <= time(NULL)) {
|
---|
| 211 |
|
---|
| 212 | /* We're expired, delete the entry */
|
---|
| 213 | tdb_delete_bystring(cache, keystr);
|
---|
| 214 |
|
---|
| 215 | SAFE_FREE(databuf.dptr);
|
---|
| 216 | return False;
|
---|
| 217 | }
|
---|
| 218 |
|
---|
| 219 | if (valstr) {
|
---|
| 220 | *valstr = SMB_STRDUP(endptr+1);
|
---|
| 221 | if (*valstr == NULL) {
|
---|
| 222 | SAFE_FREE(databuf.dptr);
|
---|
| 223 | DEBUG(0, ("strdup failed\n"));
|
---|
| 224 | return False;
|
---|
| 225 | }
|
---|
| 226 | }
|
---|
| 227 |
|
---|
| 228 | SAFE_FREE(databuf.dptr);
|
---|
| 229 |
|
---|
| 230 | if (timeout) {
|
---|
| 231 | *timeout = t;
|
---|
| 232 | }
|
---|
| 233 |
|
---|
| 234 | return True;
|
---|
| 235 | }
|
---|
| 236 |
|
---|
| 237 | /**
|
---|
| 238 | * Get existing entry from the cache file.
|
---|
| 239 | *
|
---|
| 240 | * @param keystr string that represents a key of this entry
|
---|
| 241 | * @param blob DATA_BLOB that is filled with entry's blob
|
---|
| 242 | * @param expired pointer to a bool that indicates whether the entry is expired
|
---|
| 243 | *
|
---|
| 244 | * @retval true when entry is successfuly fetched
|
---|
| 245 | * @retval False for failure
|
---|
| 246 | **/
|
---|
| 247 |
|
---|
| 248 | bool gencache_get_data_blob(const char *keystr, DATA_BLOB *blob, bool *expired)
|
---|
| 249 | {
|
---|
| 250 | TDB_DATA databuf;
|
---|
| 251 | time_t t;
|
---|
| 252 | char *blob_type;
|
---|
| 253 | unsigned char *buf = NULL;
|
---|
| 254 | bool ret = False;
|
---|
| 255 | fstring valstr;
|
---|
| 256 | int buflen = 0, len = 0, blob_len = 0;
|
---|
| 257 | unsigned char *blob_buf = NULL;
|
---|
| 258 |
|
---|
| 259 | /* fail completely if get null pointers passed */
|
---|
| 260 | SMB_ASSERT(keystr);
|
---|
| 261 |
|
---|
| 262 | if (!gencache_init()) {
|
---|
| 263 | return False;
|
---|
| 264 | }
|
---|
| 265 |
|
---|
| 266 | databuf = tdb_fetch_bystring(cache, keystr);
|
---|
| 267 | if (!databuf.dptr) {
|
---|
| 268 | DEBUG(10,("Cache entry with key = %s couldn't be found\n",
|
---|
| 269 | keystr));
|
---|
| 270 | return False;
|
---|
| 271 | }
|
---|
| 272 |
|
---|
| 273 | buf = (unsigned char *)databuf.dptr;
|
---|
| 274 | buflen = databuf.dsize;
|
---|
| 275 |
|
---|
| 276 | len += tdb_unpack(buf+len, buflen-len, "fB",
|
---|
| 277 | &valstr,
|
---|
| 278 | &blob_len, &blob_buf);
|
---|
| 279 | if (len == -1) {
|
---|
| 280 | goto out;
|
---|
| 281 | }
|
---|
| 282 |
|
---|
| 283 | t = strtol(valstr, &blob_type, 10);
|
---|
| 284 |
|
---|
| 285 | if (strcmp(blob_type+1, BLOB_TYPE) != 0) {
|
---|
| 286 | goto out;
|
---|
| 287 | }
|
---|
| 288 |
|
---|
| 289 | DEBUG(10,("Returning %s cache entry: key = %s, "
|
---|
| 290 | "timeout = %s", t > time(NULL) ? "valid" :
|
---|
| 291 | "expired", keystr, ctime(&t)));
|
---|
| 292 |
|
---|
| 293 | if (t <= time(NULL)) {
|
---|
| 294 | /* We're expired */
|
---|
| 295 | if (expired) {
|
---|
| 296 | *expired = True;
|
---|
| 297 | }
|
---|
| 298 | }
|
---|
| 299 |
|
---|
| 300 | if (blob) {
|
---|
| 301 | *blob = data_blob(blob_buf, blob_len);
|
---|
| 302 | if (!blob->data) {
|
---|
| 303 | goto out;
|
---|
| 304 | }
|
---|
| 305 | }
|
---|
| 306 |
|
---|
| 307 | ret = True;
|
---|
| 308 | out:
|
---|
| 309 | SAFE_FREE(blob_buf);
|
---|
| 310 | SAFE_FREE(databuf.dptr);
|
---|
| 311 |
|
---|
| 312 | return ret;
|
---|
| 313 | }
|
---|
| 314 |
|
---|
| 315 | /**
|
---|
| 316 | * Set an entry in the cache file. If there's no such
|
---|
| 317 | * one, then add it.
|
---|
| 318 | *
|
---|
| 319 | * @param keystr string that represents a key of this entry
|
---|
| 320 | * @param blob DATA_BLOB value being cached
|
---|
| 321 | * @param timeout time when the value is expired
|
---|
| 322 | *
|
---|
| 323 | * @retval true when entry is successfuly stored
|
---|
| 324 | * @retval false on failure
|
---|
| 325 | **/
|
---|
| 326 |
|
---|
| 327 | bool gencache_set_data_blob(const char *keystr, const DATA_BLOB *blob, time_t timeout)
|
---|
| 328 | {
|
---|
| 329 | bool ret = False;
|
---|
| 330 | int tdb_ret;
|
---|
| 331 | TDB_DATA databuf;
|
---|
| 332 | char *valstr = NULL;
|
---|
| 333 | unsigned char *buf = NULL;
|
---|
| 334 | int len = 0, buflen = 0;
|
---|
| 335 |
|
---|
| 336 | /* fail completely if get null pointers passed */
|
---|
| 337 | SMB_ASSERT(keystr && blob);
|
---|
| 338 |
|
---|
| 339 | if (!gencache_init()) {
|
---|
| 340 | return False;
|
---|
| 341 | }
|
---|
| 342 |
|
---|
| 343 | if (asprintf(&valstr, "%12u/%s", (int)timeout, BLOB_TYPE) == -1) {
|
---|
| 344 | return False;
|
---|
| 345 | }
|
---|
| 346 |
|
---|
| 347 | again:
|
---|
| 348 | len = 0;
|
---|
| 349 |
|
---|
| 350 | len += tdb_pack(buf+len, buflen-len, "fB",
|
---|
| 351 | valstr,
|
---|
| 352 | blob->length, blob->data);
|
---|
| 353 |
|
---|
| 354 | if (len == -1) {
|
---|
| 355 | goto out;
|
---|
| 356 | }
|
---|
| 357 |
|
---|
| 358 | if (buflen < len) {
|
---|
| 359 | SAFE_FREE(buf);
|
---|
| 360 | buf = SMB_MALLOC_ARRAY(unsigned char, len);
|
---|
| 361 | if (!buf) {
|
---|
| 362 | goto out;
|
---|
| 363 | }
|
---|
| 364 | buflen = len;
|
---|
| 365 | goto again;
|
---|
| 366 | }
|
---|
| 367 |
|
---|
| 368 | databuf = make_tdb_data(buf, len);
|
---|
| 369 |
|
---|
| 370 | DEBUG(10,("Adding cache entry with key = %s; "
|
---|
| 371 | "blob size = %d and timeout = %s"
|
---|
| 372 | "(%d seconds %s)\n", keystr, (int)databuf.dsize,
|
---|
| 373 | ctime(&timeout), (int)(timeout - time(NULL)),
|
---|
| 374 | timeout > time(NULL) ? "ahead" : "in the past"));
|
---|
| 375 |
|
---|
| 376 | tdb_ret = tdb_store_bystring(cache, keystr, databuf, 0);
|
---|
| 377 | if (tdb_ret == 0) {
|
---|
| 378 | ret = True;
|
---|
| 379 | }
|
---|
| 380 |
|
---|
| 381 | out:
|
---|
| 382 | SAFE_FREE(valstr);
|
---|
| 383 | SAFE_FREE(buf);
|
---|
| 384 |
|
---|
| 385 | return ret;
|
---|
| 386 | }
|
---|
| 387 |
|
---|
| 388 | /**
|
---|
| 389 | * Iterate through all entries which key matches to specified pattern
|
---|
| 390 | *
|
---|
| 391 | * @param fn pointer to the function that will be supplied with each single
|
---|
| 392 | * matching cache entry (key, value and timeout) as an arguments
|
---|
| 393 | * @param data void pointer to an arbitrary data that is passed directly to the fn
|
---|
| 394 | * function on each call
|
---|
| 395 | * @param keystr_pattern pattern the existing entries' keys are matched to
|
---|
| 396 | *
|
---|
| 397 | **/
|
---|
| 398 |
|
---|
| 399 | void gencache_iterate(void (*fn)(const char* key, const char *value, time_t timeout, void* dptr),
|
---|
| 400 | void* data, const char* keystr_pattern)
|
---|
| 401 | {
|
---|
| 402 | TDB_LIST_NODE *node, *first_node;
|
---|
| 403 | TDB_DATA databuf;
|
---|
| 404 | char *keystr = NULL, *valstr = NULL, *entry = NULL;
|
---|
| 405 | time_t timeout = 0;
|
---|
| 406 | int status;
|
---|
| 407 | unsigned u;
|
---|
| 408 |
|
---|
| 409 | /* fail completely if get null pointers passed */
|
---|
| 410 | SMB_ASSERT(fn && keystr_pattern);
|
---|
| 411 |
|
---|
| 412 | if (!gencache_init()) return;
|
---|
| 413 |
|
---|
| 414 | DEBUG(5, ("Searching cache keys with pattern %s\n", keystr_pattern));
|
---|
| 415 | node = tdb_search_keys(cache, keystr_pattern);
|
---|
| 416 | first_node = node;
|
---|
| 417 |
|
---|
| 418 | while (node) {
|
---|
| 419 | char *fmt;
|
---|
| 420 |
|
---|
| 421 | /* ensure null termination of the key string */
|
---|
| 422 | keystr = SMB_STRNDUP((const char *)node->node_key.dptr, node->node_key.dsize);
|
---|
| 423 | if (!keystr) {
|
---|
| 424 | break;
|
---|
| 425 | }
|
---|
| 426 |
|
---|
| 427 | /*
|
---|
| 428 | * We don't use gencache_get function, because we need to iterate through
|
---|
| 429 | * all of the entries. Validity verification is up to fn routine.
|
---|
| 430 | */
|
---|
| 431 | databuf = tdb_fetch(cache, node->node_key);
|
---|
| 432 | if (!databuf.dptr || databuf.dsize <= TIMEOUT_LEN) {
|
---|
| 433 | SAFE_FREE(databuf.dptr);
|
---|
| 434 | SAFE_FREE(keystr);
|
---|
| 435 | node = node->next;
|
---|
| 436 | continue;
|
---|
| 437 | }
|
---|
| 438 | entry = SMB_STRNDUP((const char *)databuf.dptr, databuf.dsize);
|
---|
| 439 | if (!entry) {
|
---|
| 440 | SAFE_FREE(databuf.dptr);
|
---|
| 441 | SAFE_FREE(keystr);
|
---|
| 442 | break;
|
---|
| 443 | }
|
---|
| 444 |
|
---|
| 445 | SAFE_FREE(databuf.dptr);
|
---|
| 446 |
|
---|
| 447 | valstr = (char *)SMB_MALLOC(databuf.dsize + 1 - TIMEOUT_LEN);
|
---|
| 448 | if (!valstr) {
|
---|
| 449 | SAFE_FREE(entry);
|
---|
| 450 | SAFE_FREE(keystr);
|
---|
| 451 | break;
|
---|
| 452 | }
|
---|
| 453 |
|
---|
| 454 | if (asprintf(&fmt, READ_CACHE_DATA_FMT_TEMPLATE,
|
---|
| 455 | (unsigned int)databuf.dsize - TIMEOUT_LEN)
|
---|
| 456 | == -1) {
|
---|
| 457 | SAFE_FREE(valstr);
|
---|
| 458 | SAFE_FREE(entry);
|
---|
| 459 | SAFE_FREE(keystr);
|
---|
| 460 | break;
|
---|
| 461 | }
|
---|
| 462 | status = sscanf(entry, fmt, &u, valstr);
|
---|
| 463 | SAFE_FREE(fmt);
|
---|
| 464 |
|
---|
| 465 | if ( status != 2 ) {
|
---|
| 466 | DEBUG(0,("gencache_iterate: invalid return from sscanf %d\n",status));
|
---|
| 467 | }
|
---|
| 468 | timeout = u;
|
---|
| 469 |
|
---|
| 470 | DEBUG(10, ("Calling function with arguments (key = %s, value = %s, timeout = %s)\n",
|
---|
| 471 | keystr, valstr, ctime(&timeout)));
|
---|
| 472 | fn(keystr, valstr, timeout, data);
|
---|
| 473 |
|
---|
| 474 | SAFE_FREE(valstr);
|
---|
| 475 | SAFE_FREE(entry);
|
---|
| 476 | SAFE_FREE(keystr);
|
---|
| 477 | node = node->next;
|
---|
| 478 | }
|
---|
| 479 |
|
---|
| 480 | tdb_search_list_free(first_node);
|
---|
| 481 | }
|
---|
| 482 |
|
---|
| 483 | /********************************************************************
|
---|
| 484 | lock a key
|
---|
| 485 | ********************************************************************/
|
---|
| 486 |
|
---|
| 487 | int gencache_lock_entry( const char *key )
|
---|
| 488 | {
|
---|
| 489 | if (!gencache_init())
|
---|
| 490 | return -1;
|
---|
| 491 |
|
---|
| 492 | return tdb_lock_bystring(cache, key);
|
---|
| 493 | }
|
---|
| 494 |
|
---|
| 495 | /********************************************************************
|
---|
| 496 | unlock a key
|
---|
| 497 | ********************************************************************/
|
---|
| 498 |
|
---|
| 499 | void gencache_unlock_entry( const char *key )
|
---|
| 500 | {
|
---|
| 501 | if (!gencache_init())
|
---|
| 502 | return;
|
---|
| 503 |
|
---|
| 504 | tdb_unlock_bystring(cache, key);
|
---|
| 505 | return;
|
---|
| 506 | }
|
---|