Memcachedのflush_allは「すべてのレコードを期限切れに」する訳ではない

# 概要
- memcachedのコマンドに「flush_all」というのがある
- 使い方としては「すべてのレコードを消去したい」ときに使う
- **しかし、これを実行することで本当にすべてのレコードが削除されるわけではない**
- というのは調べれば割とすぐ出てくる話で、「すべてのレコードを期限切れにする」機能であると説明されていることが多い
- **ところが、それも微妙に説明として誤解を招きやすい表現になっている（気がする）**

# flush_allでレコードの「全削除」はされない
memcachedにはflush_allというコマンドが用意されていて、これはこれまでに保存されたレコードをすべて消去したいときに利用するコマンド。
ただし、コマンドを実行してもデータの容量に変化は発生しない。
https://dev.classmethod.jp/cloud/aws/memcached-flushall-behavior/
https://github.com/memcached/memcached/wiki/ProgrammingFAQ#why-isnt-curr_items-decreasing-when-items-expire
https://github.com/memcached/memcached/blob/master/doc/protocol.txt

これは適当にググれば出てくるこの辺りの記事を見ればすぐにわかることで、まあ物理削除と論理削除の違いみたいなものということ。（ただし削除されたレコードが残り続けるわけではない）

で、この辺の記述を見ると、つまり「すべてのデータを期限切れにする」というコマンドなのだな、という感じを受けると思う。

# 「すべてのデータを期限切れにする」のか？

しかし、この感じ方も微妙に適切ではない。というのも、これは感覚の問題なのだが、そもそもmemcachedのレコード一件一件は、stats cachedumpなんかをやってやればわかるように、それぞれ有効期限にあたるパラメータを持っている。(↓1234567890s。)

```shell
stats cachedump 1 10
ITEM aaa [1 b; 1234567890 s]
```

こういう情報から考えると、「flush_allは、この有効期限をコマンド実行時の時間などに書き換える機能なのだろう」というイメージにならないだろうか？少なくとも、自分はそう感じた。

しかし、そのイメージに反し、**flush_all実行後も、このstats cachedumpで表示される結果に変化はない。**
レコード全件の有効期限を書き換えるわけではない、ということだ。（ここが今回一番伝えたかった部分）

そういうわけではないのに、実際getでレコードを引こうとすると何も返ってこなくなっている。
何か他の方法で「有効期限が切れた」ことを管理しているものと思われる。

# 実際
では、実際にはどのようにして全件を「invalidate」（上記記事参照）する機能を実現しているのか。これを説明している記事がどうにもなかなか見つからなかったので、ソースコードを追って確認してみることとする。

まず、memcached.c内部のflush_allの実装部分。

```memcached.c
static void process_bin_flush(conn *c) {
    time_t exptime = 0;
    protocol_binary_request_flush* req = binary_get_request(c);
    rel_time_t new_oldest = 0;

    if (!settings.flush_enabled) {
      // flush_all is not allowed but we log it on stats
      write_bin_error(c, PROTOCOL_BINARY_RESPONSE_AUTH_ERROR, NULL, 0);
      return;
    }

    if (c->binary_header.request.extlen == sizeof(req->message.body)) {
        exptime = ntohl(req->message.body.expiration);
    }

    if (exptime > 0) {
        new_oldest = realtime(exptime);
    } else {
        new_oldest = current_time;
    }
    if (settings.use_cas) {
        settings.oldest_live = new_oldest - 1;
        if (settings.oldest_live <= current_time)
            settings.oldest_cas = get_cas_id();
    } else {
        settings.oldest_live = new_oldest;
    }

    pthread_mutex_lock(&c->thread->stats.mutex);
    c->thread->stats.flush_cmds++;
    pthread_mutex_unlock(&c->thread->stats.mutex);

    write_bin_response(c, NULL, 0, 0, 0);
}
```

設定次第で多少挙動に変化はあるものの、要するに「settings.oldest_live」を指定の時間に書き換える、という処理になっている。
で、これでなぜgetから引けなくなるかというと、items.cのgetの実装内にその答えがある。

```items.c
item *do_item_get(const char *key, const size_t nkey, const uint32_t hv, conn *c, const bool do_update) {
    item *it = assoc_find(key, nkey, hv);
    if (it != NULL) {
        refcount_incr(it);
        /* Optimization for slab reassignment. prevents popular items from
         * jamming in busy wait. Can only do this here to satisfy lock order
         * of item_lock, slabs_lock. */
        /* This was made unsafe by removal of the cache_lock:
         * slab_rebalance_signal and slab_rebal.* are modified in a separate
         * thread under slabs_lock. If slab_rebalance_signal = 1, slab_start =
         * NULL (0), but slab_end is still equal to some value, this would end
         * up unlinking every item fetched.
         * This is either an acceptable loss, or if slab_rebalance_signal is
         * true, slab_start/slab_end should be put behind the slabs_lock.
         * Which would cause a huge potential slowdown.
         * Could also use a specific lock for slab_rebal.* and
         * slab_rebalance_signal (shorter lock?)
         */
        /*if (slab_rebalance_signal &&
            ((void *)it >= slab_rebal.slab_start && (void *)it < slab_rebal.slab_end)) {
            do_item_unlink(it, hv);
            do_item_remove(it);
            it = NULL;
        }*/
    }
    int was_found = 0;

    if (settings.verbose > 2) {
        int ii;
        if (it == NULL) {
            fprintf(stderr, "> NOT FOUND ");
        } else {
            fprintf(stderr, "> FOUND KEY ");
        }
        for (ii = 0; ii < nkey; ++ii) {
            fprintf(stderr, "%c", key[ii]);
        }
    }

    if (it != NULL) {
        was_found = 1;
        if (item_is_flushed(it)) {
            do_item_unlink(it, hv);
            STORAGE_delete(c->thread->storage, it);
            do_item_remove(it);
            it = NULL;
            pthread_mutex_lock(&c->thread->stats.mutex);
            c->thread->stats.get_flushed++;
            pthread_mutex_unlock(&c->thread->stats.mutex);
            if (settings.verbose > 2) {
                fprintf(stderr, " -nuked by flush");
            }
            was_found = 2;
        } else if (it->exptime != 0 && it->exptime <= current_time) {
            do_item_unlink(it, hv);
            STORAGE_delete(c->thread->storage, it);
            do_item_remove(it);
            it = NULL;
            pthread_mutex_lock(&c->thread->stats.mutex);
            c->thread->stats.get_expired++;
            pthread_mutex_unlock(&c->thread->stats.mutex);
            if (settings.verbose > 2) {
                fprintf(stderr, " -nuked by expire");
            }
            was_found = 3;
        } else {
            if (do_update) {
                do_item_bump(c, it, hv);
            }
            DEBUG_REFCNT(it, '+');
        }
    }

    if (settings.verbose > 2)
        fprintf(stderr, "\n");
    /* For now this is in addition to the above verbose logging. */
    LOGGER_LOG(c->thread->l, LOG_FETCHERS, LOGGER_ITEM_GET, NULL, was_found, key, nkey,
               (it) ? ITEM_clsid(it) : 0, c->sfd);

    return it;
}

```

どうやら、is_flushedという関数で専用の判定が行われている。で、その中身がこれ

```items.c
int item_is_flushed(item *it) {
    rel_time_t oldest_live = settings.oldest_live;
    uint64_t cas = ITEM_get_cas(it);
    uint64_t oldest_cas = settings.oldest_cas;
    if (oldest_live == 0 || oldest_live > current_time)
        return 0;
    if ((it->time <= oldest_live)
            || (oldest_cas != 0 && cas != 0 && cas < oldest_cas)) {
        return 1;
    }
    return 0;
}
```
oldest_liveと現在時刻を比較している。
つまり、「flush_allがどういうコマンドか？」ということを」日本語で説明しようとするならば、**「『この日時よりも前ならば期限切れレコード扱いにする』という値を指定の日時に書き換える」**という表現になると思われる。
どうしても回りくどい説明になってしまうため、公式やphpのドキュメントなどでは「invalidate」という記述が用いられているものと思われる。

https://www.php.net/manual/en/memcached.flush.php

# 付記
ちなみにこの仕様により、「正しくすべてのレコードがflushされている」ことを確認するためには、基本的にgetで引けないことを確認していく以外の方法がない。
幸いmemcached-toolにはdumpというコマンドがあるので、それを利用して何も返ってこなければOKではある。
あとstats settings で出てくる「oldest」の値は実行するたびに増える。（ただ、これでいいのならcmd_flushの実行回数が増えたのを確認すればいいという話ではある）