一但發生當機, 或多或少會造成部分玩家資料未儲存的狀況
即使有定時全玩家自動儲存的機制, 也無法保證全玩家資料的一致性
最常發生的情形就是, 部分玩家(當機前未存檔)資料回朔, 但其他的(當機前已存檔)卻正常
因為上述原因 (慘痛經驗 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)