我現在覺得持續 coding 也不錯。
最近公司開了個會,有幾個地方看了有所感觸。
同一物件 另一物件
┌───┬───┐ ┌───┐
│ 訊 │ 訊 │ │ 資 │
│ 息 → 息 │ │ 料 │
│ 產 │ 暫 │─→│ 寫 │
│ 生 → 存 │ │ 入 │
│ 源 │ 區 │ │ 區 │
└───┴───┘ └───┘
例如說我們想 log 玩家的戰鬥資料,那麼當玩家「進入戰鬥」時,
訊息產生源就送出資料給訊息暫存區,請它「暫存一條玩家已進入
戰鬥的訊息」,例如進入戰鬥的時間啦、戰鬥發生的地點啦、戰鬥
的目標啦、....。
那麼可以想像在某一段時間區間內,暫存區多半會儲存兩條以上的
訊息。
然後當玩家「結束戰鬥」時,訊息產生源一樣送出資料給訊息暫存
區,例如結束戰鬥的時間、結束戰鬥的地點、玩家獲得的戰利品資
訊等,然後暫存區就將這個玩家從戰鬥開始到結束的一些該寫入的
訊息,匯集後送往資料寫入區去寫入。
但是,當玩家的戰鬥有不正常中止(非結束)的情況時,因為訊息產
生源一直沒有觸發到該玩家已結束戰鬥的資訊,它就不會通知暫存
區要將暫存的資料送往寫入區,當這種情況很頻繁地發生又沒有被
察覺時,就會造成暫存區的資料量增加,直到某一天「滿了」造成
明顯的問題狀況時才被發現,而這問題開始發生、到問題爆出來,
中間被認為有問題的戰鬥歷程就全部沒有被完整紀錄。
一、傳統上其實上述三個方塊多半被寫在同一個物件裡頭,那分開
的好處其實顯而易見,就是「分散工作量」,因為暫存訊息需
要經過處理後才暫存,寫入訊息同樣也需要經過處理後才寫入
,當暫存很頻繁時,由另一物件負責資料寫入(及回存)似乎是
比較適當的做法。
mapping buff_data=([]);
┌────────────────┐
│ mapping save_data=([]); ├──┐
└────────────────┘ │
void create() │
{ │
::create(); │
seteuid(getuid(this_object())); │
┌────────────────┐ │
│if(file_exists(SAVE_FILES+".o"))│ │ 也就是說這三個區塊可挪到
│ restore_object(SAVE_FILES); ├──┼─另一個物件
└────────────────┘ │
. │
. │
} │
│
┌────────────────┐ │
│int save_room() │ │
│{ │ │
│ save_object(SAVE_FILES); ├──┘
│ return 1; │
│} │
└────────────────┘
二、針對暫存的資料,應有一額外檢核的機制,比方說可以預估
玩家「不可能有n秒以上的戰鬥」(例如 n = 3600),那就可
設計讓物件每n秒對所有的暫存訊息做一次檢查,檢查項目
可包括
1.該玩家物件是否仍存在(比方有可能斷線或不正常離線)
2.該玩家所戰鬥的目標是否仍存在(比方可能物件error消失)
3.該玩家物件是否正在戰鬥中
4.該玩家物件戰鬥的對象是否就是暫存訊息所儲存的對象
5.該玩家是否仍在暫存訊息所紀錄的地方進行戰鬥
.
.
當有其中一項不符合時馬上就可判定該暫存訊息已經不正常
,此時就應該做適度的資料補正後送往寫入區,或是剔除該
暫存資料,並以適當的方式通知管理者處理。
更簡單的判斷方式還包括當暫存區是以玩家的 name 當做主
key 去暫存資料時,則當該玩家前一筆暫存資料仍存在於暫
存區、該玩家卻又觸發訊息產生源去產生一筆新的暫存資料
時,就可以做如下判斷
if(buff_data[ppl_name])
save_error("ERROR! "+ppl_name+": "+identify(buff_data[ppl_name])+"\n");
// 然後才做新的儲存
buff_data[ppl_name] = .... ; // 舊的 buff_data 會被新的覆蓋
這樣管理者事後至少有跡可尋,而且資料量也可預期不會肥
胖。
三、暫存資料方式的設定也很重要,以上面為例當暫存資料是以
ppl_name 為主 key 時,相較於以其它方式為主 key 的情
況、或是不使用 mapping 資料而改採陣列資料的串流儲存
方式時,是比較保險的做法。
當然判斷以 ppl_name 為主 key 的方式為可行的依據,就
是一個玩家不可能在同一時間觸發兩場戰鬥,但實際上卻是
可能的,比方戰鬥中「突然又出現新的戰鬥目標」,則考量
到這種情況,就得做適度的修改,例如..
buff_data[ppl_name] = ({ ({暫存一,}),
({暫存二,}),
.
. });
這樣有新的戰鬥訊息就累加上去,有新的戰鬥結束訊息時
就從陣列裡面去找出符合的。
總之就是資料的儲存方式必須要做事前的妥善分析規劃。
四、暫存資料本身的檢核。假設我們有個函數叫做 buff_size
,可用來檢查暫存資料本身的大小時,那就可以在每一次
的暫存資料輸入、或是每一次的暫存資料寫入時,就做底
下的判斷:
switch(buff_size(buff_data))
{
case 80:
sent_alarm_1("buff_data 儲存量已達到 80%。");
break;
case 90:
sent_alarm_2("buff_data 儲存量已達到 90%。");
break;
case 95:
sent_alarm_3("buff_data 儲存量已達到 95%。");
break;
}
但是這畢竟不如主動式檢核來得好,因為通常等到 80%
的警告訊息出現時通常情況就已經很嚴重了,而告警容量
訂得太低則會經常收到告警「但其實系統正常運作」。
但它還是可以寫,只是當做一個備用的告警就好,平常就
要注意不要讓它 init 到告警值。
五、最後就是該系統運作狀態的觀看介面,不論是寫成指令式
或是選單式,重點都在於要能即時知道系統目前的運作狀
態甚至紀錄的訊息詳情、特定資料的搜尋、比對等等。
以上亦會做為個人在 sanc 的 coding 參考,當然實際上依照
需儲存的資料內容來說,大部份的資料其實都不太有資料滿溢
的情況,這是因為在系統建置之初就已考量好資料儲存的格式
及內容範圍,也就是先做好事前的預防,自可避免事後的問題
,但即便思慮再周詳,也不能完全保證系統運作一段時間後不
會產生其它問題,因此最好還是在初期就把相關的檢核機制也
一併加進去做把關,這樣至少可更確保系統的穩定運作。
但是有個兩難的問題就是,有時候也可能因為檢核寫的太優良
,導致「系統即便遇到問題還是能持續運作,但是該問題也同
時持續存在」的情況發生,而可能在某一個時間點導致更大的
問題狀況出現。在漫畫《無敵怪醫》裡面就有這樣的情節,人
工肝臟一般都有產生血栓的問題,導致剛被植入到生物體內沒
多久就出現血栓,但某人發明的人工肝臟「卻因太過優秀」,
導致植物入生物體內後經過快半年都沒出問題,某人就打算進
行人體實驗,然後不幸的就是在人體實驗前幾天,被植入人工
肝臟的生物此時才出現血栓症狀,而導致人體實驗必須中止..
so,總之這也需要經驗的判斷。
Laechan@Sanc