玩家資料一致性 SAVE_D

回覆文章
jrealm
系統管理員
文章: 1084
註冊時間: 2014-03-31, 23:10

玩家資料一致性 SAVE_D

文章 jrealm » 2017-07-07, 11:49

mudos 一般來說穩定性都是足夠的, 但仍然無法完全杜絕當機的發生
一但發生當機, 或多或少會造成部分玩家資料未儲存的狀況

即使有定時全玩家自動儲存的機制, 也無法保證全玩家資料的一致性
最常發生的情形就是, 部分玩家(當機前未存檔)資料回朔, 但其他的(當機前已存檔)卻正常

因為上述原因 (慘痛經驗 QQ), CW 開發了一個儲存機制 SAVE_D
原理相當簡單, 所有跟玩家有關的資料, 一律委託 SAVE_D 存檔

結論:
回朔難以杜絕, 退求其次, 至少要保持資料的一致性
將原本的存檔機制 (物件 >> 檔案) 改為三段式存檔 (物件 >> 記憶體 >> 日誌 >> 檔案)


新增 /adm/daemons/saved.c (SAVE_D)

代碼: 選擇全部

private string query_data(string);
private void save_files(string *, int, int);
private int valid_object(object);

#define CALL_OUT_INTERVAL 1
#define FILE_COUNT        10
#define JOURNALS_FILE     DATA_DIR"saved_journals"
#define PENDING_FILE      DATA_DIR"pending"

private nosave int handle;

private nosave mapping journals, pendings = ([]);

// 允許使用 SAVE_D 的其他 daemons, 主要是倉庫相關的
private nosave string *store_daemons = ({
    CLAN_STORE_D,
    STORE_D,
    VIP_STORE_D,
});

// 允許使用 SAVE_D 的物件
private nosave string *valid_objects = ({
    USER_OB,
    LOGIN_OB,
    COMMERCE_D,
    CLAN_D,
    "/d/special_room/bet/lottery",
    "/d/special_room/game/npc/game_dealer",
});

// 允許使用 SAVE_D 的指令
private nosave string *valid_purge_cmds = ({
    COMMAND_DIR"adm/purge",
    COMMAND_DIR"std/suicide",
});

void create() {
    mixed prev;

    if (file_size(JOURNALS_FILE) >= 0) {
        prev = restore_variable(read_file(JOURNALS_FILE));

        if (mapp(prev)) {
            // 如果日誌檔還在, 且內容格式正確, 表示上一次的存檔未正常結束
            // 需要重新再存一次
            journals = prev;

            save_files(keys(journals), sizeof(journals), 0);
        } else {
            rm(JOURNALS_FILE);
        }
    }
}

// 判斷檔案是否存在:
//     新建的檔案可能還在記憶體中, 尚未寫入至磁碟
//     所以需要使用這個 function 來判斷, 不能使用 file_size()
int file_exists(string path) {
    return stringp(query_data(path)) || file_size(path) >= 0;
}

// 將記憶體中的資料寫入磁碟
// 寫入日誌檔後
//     flag=0, 再分批寫入各個檔案, 避免影響效能
//     flag=1, 一次寫入全部檔案, 只適合於 shutdown 時使用
varargs void flush(int flag) {
    if (sizeof(pendings)) {
        if (journals) {
            journals = journals + pendings;
        } else {
            journals = pendings;
        }

        pendings = ([]);

        write_file(JOURNALS_FILE, save_variable(journals), 1);
    } else if (!flag || !journals) {
        return;
    }

    if (handle) {
        // 若上次 flush 還沒結束, 則取消重來
        remove_call_out(handle);
        handle = 0;
    }

    if (flag) {
        save_files(keys(journals), sizeof(journals), 0);
    } else {
        handle = call_out((: save_files :), CALL_OUT_INTERVAL,
                                            keys(journals),
                                            sizeof(journals),
                                            CALL_OUT_INTERVAL);
    }
}

void purge_user(string id) {
    int count, modify;
    object body, fake, *fakes, link, mbox, who;
    string file, *files;

    who = previous_object();

    if (!who || member_array(file_name(who), valid_purge_cmds) == -1) {
        return;
    }

    // 相關物件

    body = find_player(id);
    fakes = ({});  // 臨時載入的物件

    if (body) {
        link = body->query_temp("link_ob");
        mbox = body->query_temp("mbox_ob");
    }

    if (!link) {
        link = new(LOGIN_OB);
        link->set("id", id);
        link->set("body", USER_OB);

        fakes += ({ link });

        if (!body) {
            body = LOGIN_D->make_body(link);

            if (body->restore()) {
                mbox = body->query_temp("mbox_ob");
            }

            fakes += ({ body });
        }
    }

    if (!mbox) {
        mbox = new(MAILBOX_OB);
        mbox->set_owner(id);

        fakes += ({ mbox });
    }

    // 檔案清單

    files = ({
        body->query_save_file() + __SAVE_EXTENSION__,
        link->query_save_file() + __SAVE_EXTENSION__,
        mbox->query_save_file() + __SAVE_EXTENSION__,
        STORE_D->query_store_file(body),
    });

    for (count = body->query("bonus/vip_store_count"); count > 0; count--) {
        files += ({ VIP_STORE_D->query_store_file(body, count) });
    }

    // 清除(因收集檔案清單而創建的)臨時性物件

    foreach (fake in fakes) {
        destruct(fake);
    }

    // 刪除檔案

    foreach (file in files) {
        rm(file);

        map_delete(pendings, file);

        if (journals && stringp(journals[file])) {
            journals[file] = 0;
            modify = 1;
        }
    }

    if (modify) {
        // 不急著 flush, 先將日誌存起來就好
        write_file(JOURNALS_FILE, save_variable(journals), 1);
    }
}

// 查詢存檔檔名
//     由於資料可能會暫存於記憶體, 實體檔案的內容可能是過時的
//     不可使用 obj->query_save_file() 查詢檔名
string query_save_file() {
    object who = previous_object();
    string data, path;

    if (valid_object(who)) {
        path = who->query_save_file();

        if (stringp(path)) {
            // 從記憶體中取出資料
            data = query_data(path + __SAVE_EXTENSION__);

            if (stringp(data)) {
                // 如果有尚未存檔的資料, 那磁碟中的檔案內容就是舊的, 不可使用
                // 將記憶體中的新資料寫入暫存檔, 供物件讀取
                path = PENDING_FILE + __SAVE_EXTENSION__;

                write_file(path, data, 1);

                // 用完就丟
                call_out((: rm :), 0, path);

                return PENDING_FILE;
            }
        }
    }

    return path;
}

string query_store_data(string path) {
    object who = previous_object();
    string data;

    if (who && member_array(file_name(who), store_daemons) != -1) {
        // 記憶體中有資料, 使用之
        data = query_data(path);

        if (!stringp(data) && file_size(path) >= 0) {
            // 否則, 載入之
            data = read_file(path);
        }
    }

    return data;
}

int save_object_data(string data) {
    object who = previous_object();
    string path;

    if (valid_object(who)) {
        path = who->query_save_file();

        if (stringp(path)) {
            // 將資料暫存於記憶體
            pendings[path + __SAVE_EXTENSION__] = data;
            return 1;
        }
    }

    return 0;
}

int save_store_data(string path, string data) {
    object who = previous_object();

    if (who && member_array(file_name(who), store_daemons) != -1) {
        pendings[path] = data;
        return 1;
    }

    return 0;
}

void save_users() {
    foreach (object body in children(USER_OB)) {
        if (userp(body)) {
            body->save();
        }
    }

    flush();
}

int supports() {
    return valid_object(previous_object());
}

// 從記憶體中讀取資料
private string query_data(string path) {
    string data = pendings[path];

    return journals && !stringp(data) ? journals[path] : data;
}

private void save_files(string *paths, int index, int interval) {
    int count;
    string data, path;

    for (index = index - 1; index >= 0; index--) {
        path = paths[index];
        data = journals[path];

        if (!stringp(data)) {
            continue;
        }

        assure_file(path);
        write_file(path, data, 1);

        count++;

        if (count == FILE_COUNT) {
            if (interval) {
                handle = call_out((: save_files :), interval,
                                                    paths,
                                                    index,
                                                    interval);
                return;
            } else {
                count = 0;
                reset_eval_cost();
            }
        }
    }

    handle = 0;
    journals = 0;

    // 內容處理完, 日誌檔就可以刪了
    rm(JOURNALS_FILE);
}

private int valid_object(object who) {
    return who && member_array(base_name(who), valid_objects) != -1;
}

修改 /feature/save.c (F_SAVE)

代碼: 選擇全部

// 不知道從那一版的 mudos 開始的
// save_object() 未指定參數時, 會直接回傳內容, 而不會將內容寫入檔案
// mudos 若不支援此功能, 就需要另加一個 function 來替代
// (先將物件寫入至暫存的檔案, 然後讀取並回傳)

int restore() {
    string file = SAVE_D->query_save_file(); // 若資料於記憶體中, SAVE_D 會回傳 PENDING_FILE

    if (!stringp(file)) {
        file = this_object()->query_save_file(); // 資料尚未載入, 沿用老方法

        if (!stringp(file)) {
            return 0;
        }
    }

    return restore_object(file);
}

int save() {
    string file;

    if (SAVE_D->supports()) {
        // 如果 SAVE_D 支援此物件, 則使用 SAVE_D 存檔
        return SAVE_D->save_object_data(save_object());
    }

    // 否則, 沿用老方法
    file = this_object()->query_save_file();

    if (stringp(file)) {
        assure_file(file + __SAVE_EXTENSION__);

        return save_object(file);
    }

    return 0;
}
其他讀寫檔案的修改

代碼: 選擇全部

// 讀取

if (file_size(file) == -1) {
    items = ({});
} else {
    items = restore_variable(read_file(file));
}

// 改成

items = SAVE_D->query_store_data(file);
items = stringp(items) ? restore_variable(items) : ({});


// 寫入

write_file(file, save_variable(items), 1);

// 改成

SAVE_D->save_store_data(file, save_variable(items));

最後, 記得要排程定時呼叫 SAVE_D->flush()
shutdown 前也需要呼叫 SAVE_D->flush(1)

回覆文章