Dịch Vụ Lịch Trình - nói dối e blog

Dịch Vụ Lịch Trình

Một trong những câu hỏi thường gặp nhất từ cộng đồng người dùng Skynet chính là: “Tại sao khi tôi thay đổi thời gian hệ thống nhưng Skynet lại không phản hồi?” Khi tìm hiểu kỹ hơn, hóa ra phần lớn người dùng có nhu cầu này đều muốn xây dựng một hệ thống lịch trình tự động kích hoạt nhiệm vụ tại thời điểm cụ thể. Việc điều chỉnh thời gian hệ thống thường xuất phát từ nhu cầu kiểm thử.

Phải thừa nhận rằng thay đổi thời gian hệ thống để kiểm tra là một phương pháp trực quan nhưng cực kỳ thiếu an toàn. Các timer của Skynet hoàn toàn không phụ thuộc vào thời gian hệ thống, do đó việc chỉnh sửa thời gian máy tính là hành động vô ích.

Yêu cầu về dịch vụ lịch trình là nhu cầu phổ biến trong các game online. Nếu không có các nhiệm vụ theo mùa vụ hay副本 (câu hỏi này xuất hiện lỗi, cần sửa lại thành “chế độ thử thách theo tuần”) thì gần như không thể vận hành game. Bài viết này sẽ thảo luận cách triển khai dịch vụ lịch trình hiệu quả trong Skynet.

Phương pháp tối ưu là xây dựng một dịch vụ độc lập chuyên trách quản lý lịch trình. Như vậy, khi cần kiểm thử, bạn có thể thông qua các giao diện dự phòng để dịch vụ này giả lập việc đẩy nhanh thời gian, từ đó kích hoạt nhanh chóng các nhiệm vụ định thời.

Thông thường, các nhiệm vụ lịch trình không đòi hỏi độ chính xác cao đến từng giây, chỉ cần chính xác đến phút là đủ. Chúng ta ít khi thiết lập thời gian kích hoạt chính xác đến từng giây. Các thiết lập lịch trình thường liên quan đến ngày tháng cụ thể hoặc thứ trong tuần. Ví dụ điển hình như: “Mỗi thứ Bảy hàng tuần vào lúc 20:00”, “12:00 trưa thứ Năm tuần thứ hai của mỗi tháng”, “Sự kiện ngày Quốc tế Thiếu nhi 1/6”, v.v.

Để thiết kế dịch vụ lịch trình như vậy, thực tế chỉ cần một giao diện đăng ký sự kiện đơn giản, tương tự như cơ chế định thời một lần của skynet.timer. Giao diện kích hoạt sẽ nhận vào một mốc thời gian mô tả như các ví dụ trên. Trong dịch vụ sự kiện cụ thể, chúng ta sử dụng skynet.call tới dịch vụ lịch trình, và khi skynet.call trả về chính là lúc sự kiện được kích hoạt. Chẳng hạn, để cố định mở một sự kiện vào mỗi thứ Bảy lúc 20:00, ta có thể dùng vòng lặp while như sau:

1
2
3
4
while true do
 skynet.call(schedule_service, "lua", { wday = 7 , hour = 20 } )
 -- Thực hiện hoạt động
end

Dịch vụ lịch trình mỗi khi nhận yêu cầu đăng ký, sẽ tính toán mốc thời gian kích hoạt tiếp theo dựa trên tham số truyền vào, sau đó sử dụng skynet.sleep chờ đến thời điểm đó. Nếu cần điều chỉnh thời gian giả lập để kiểm thử, chỉ cần gửi thông báo tới dịch vụ và hiệu chỉnh độ lệch giữa thời gian giả lập và thời gian thật, đánh thức tất cả các yêu cầu đang chờ và tính toán lại thời gian chờ.

Dịch vụ lịch trình như vậy có độ phức tạp vừa phải. Tôi đã dành chút thời gian viết một bản triển khai mẫu, mời các bạn tham khảo. Bản này còn nhiều điểm có thể cải tiến, nhưng hy vọng sẽ giúp bạn hiểu rõ nguyên lý thiết kế:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
local skynet = require "skynet"
local service = require "skynet.service"
local schedule = {}
local service_addr
-- { month=, day=, wday=, hour= , min= }
function schedule.submit(ti)
  return skynet.call(service_addr, "lua", ti)
end
function schedule.changetime(ti)
  local tmp = {}
  for k,v in pairs(ti) do
    tmp[k] = v
  end
  tmp.changetime = true
  return skynet.call(service_addr, "lua", tmp)
end
skynet.init(function()
  local schedule_service = function()
-- schedule service
local skynet = require "skynet"
local task = { session = 0, difftime = 0 }
local function next_time(now, ti)
  local nt = {
    year = now.year ,
    month = now.month ,
    day = now.day,
    hour = ti.hour or 0,
    min = ti.min or 0,
    sec = ti.sec,
  }
  if ti.wday then
    -- set week
    assert(ti.day == nil and ti.month == nil)
    nt.day = nt.day + ti.wday - now.wday
    local t = os.time(nt)
    if t < now.time then
      nt.day = nt.day + 7
    end
  else
    -- set day, no week day
    if ti.day then
      nt.day = ti.day
    end
    if ti.month then
      nt.month = ti.month
    end
    local t = os.time(nt)
    if t < now.time then
      if ti.month then
        nt.year = nt.year + 1  -- next year
      else
        nt.month = nt.month + 1 -- next month
      end
    end
  end
  return os.time(nt)
end
local function changetime(ti)
  local ct = math.floor(skynet.time())
  local current = os.date("*t", ct)
  current.time = ct
  if not ti.hour then
    ti.hour = current.hour
  end
  if not ti.min then
    ti.min = current.min
  end
  ti.sec = current.sec
  local nt = next_time(current, ti)
  skynet.error(string.format("Đổi thời gian thành %s", os.date(nil, nt)))
  task.difftime = os.difftime(nt,ct)
  for k,v in pairs(task) do
    if type(v) == "table" then
      skynet.wakeup(v.co)
    end
  end
  skynet.ret()
end
local function submit(_, addr, ti)
  if ti.changetime then
    return changetime(ti)
  end
  local session = task.session + 1
  task.session = session
  repeat
    local ct = math.floor(skynet.time()) + task.difftime
    local current = os.date("*t", ct)
    current.time = ct
    local nt = next_time(current, ti)
    task[session] = { time = nt, co = coroutine.running(), address = addr }
    local diff = os.difftime(nt , ct)
    print("sleep", diff)
  until skynet.sleep(diff * 100) ~= "BREAK"
  task[session] = nil
  skynet.ret()
end
skynet.start(function()
  skynet.dispatch("lua", submit)
  skynet.info_func(function()
    local info = {}
    for k, v in pairs(task) do
      if type(v) == "table" then
        table.insert( info, {
          time = os.date(nil, v.time),
          address = skynet.address(v.address),
        })
      end
      return info
    end
  end)
end)
-- end of schedule service
  end
  service_addr = service.new("schedule", schedule_service)
end)
return schedule

Bạn có thể dùng schedule.submit để đăng ký một mốc thời gian, khi đến thời điểm sẽ tự động trả về. Khi sử dụng, chỉ cần khởi động một

0%