2023年10月2日月曜日

GAE/J で排他制御したい

 GAEとっても便利なんですが、どうしても排他制御したい処理が出てきてしまいました。特定のファイルの更新とかそんな感じの処理です。syncronized で同じインスタンスなら排他制御できるので大丈夫なんですが、GAEの場合オートスケールするので同じインスタンスからのアクセスとは限らずその技は通用しません。

ググるとこういうときは Memcache を使って実現するようなのが多いようです。

Memcache の使用方法

これの、「同時書き込みの処理」のところを使って、ロックフラグみたいのをいじるようにすると実現できそうです。

というわけで実装してみたわけですが、、、ちゃんと動作しません。なんだろう、ソースそのまま使ってるしなーと思って上記のソースを眺めていると、、、あれ?これってキーがまだ無いときに複数同時に処理されると皆んな0で作っちゃうんじゃね?ということに気づきました。

ここの処理の肝は putIfUntouched を使って、特定の1スレッドだけが処理を完了できるところにあります。が、キーが無いときはただ 0 を put してるだけなのでここが被ると排他にならないわけです。

putIfUntouched で 0→1 は特定の1スレッド限定にできるんですが、 null→0 をやりたいときは putIfUntouched は IllegalArgumentException を投げてくるので使えません。

キーがない → 0でput → putIfUntouched で1にできた人勝ち

とやりたいところなんですが、勝ちが決まった瞬間に0でputの人が出てくると上書きされてしまうのです。

こまった。どうしたらいいんだ。ということで思いついた苦肉の策が、「putした人はしばらくロックバトルに参加できなくする」という作戦です。putした人の動きをしばらく止めておけば、後から来た人はキーが存在する状態なので putIfUntouched で勝者を決めることができます。

    public void LockFile(String filePath) {
        Random rand = new Random();
        MemcacheService syncCache = MemcacheServiceFactory.getMemcacheService();
        
        while (true) {
            byte[] oneValue = BigInteger.valueOf(1).toByteArray();
            byte[] zeroValue = BigInteger.valueOf(0).toByteArray();

            IdentifiableValue lockValue = syncCache.getIdentifiable(filePath);
            if (lockValue == null) {
                syncCache.put(filePath, zeroValue, Expiration.byDeltaSeconds(30));
                try {
                    Thread.sleep(1000);  // ロック情報を作成したスレッドは1秒間ロック取得競争に参加できないようにする
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            } else {
                if (new BigInteger((byte[])lockValue.getValue()).intValue() == 0 && syncCache.putIfUntouched(filePath, lockValue, oneValue, Expiration.byDeltaSeconds(30))) {
                    // ここに来れるのは1スレッドのみ
                    logger.info("Lock File : " + filePath);
                    break;
                }                
            }
            
            try {
                Thread.sleep(rand.nextInt(100));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }        
    }
    
    public void UnlockFile(String filePath) throws Exception {
        MemcacheService syncCache = MemcacheServiceFactory.getMemcacheService();
        syncCache.put(filePath, BigInteger.valueOf(0).toByteArray(), Expiration.byDeltaSeconds(30));        
        logger.info("Unlock File : " + filePath);
    }

いちおうこれで考えてたような処理が無事できました。

競合が居ない(かつキーが無い)場合は最低1秒待たされることになりますがまぁ仕方ないかな。