매장 사이니지(signage)를 10시에 '영업 중', 18시에 '오늘은 종료했습니다'로 전환한다고 가정해 봅시다.
단순히 현재 시각만으로 표시 내용을 결정할 경우, 시계가 18시에서 15시로 돌아갔을 때 화면도 '영업 중'으로 되돌아가 버립니다. NTP 보정이나 RTC(Real-Time Clock) 이상을 고려하면 이는 책상 위의 이야기가 아닙니다.
Glypha에서는 다음과 같은 방침을 세웠습니다.
시간은 돌아가도 괜찮다. 다만, 시스템의 이력은 돌아가지 않는다.
Glypha의 스케줄러는 현재 시각뿐만 아니라, 어디까지 전환(切り替え)을 소화했는지 보여주는 커서를 가지고 있습니다.
예를 들어, 일일 스케줄이 다음과 같다고 가정해 봅시다.
10:00 → open
13:00 → afternoon
18:00 → closed
14시에 문의가 왔다면, 10시 화면을 거치지 않고 13시의 afternoon을 직접 선택합니다. 커서도 13시까지 진행됩니다.
그 후 시계가 11시로 돌아가더라도, 커서보다 이전의 전환은 이미 소화된 것입니다. open은 재생하지 않고, afternoon을 계속 표시합니다.
Go 구현에서는 반개 구간 (cursor, now]에 있는 최신 전환만 반환합니다.
func (d *Daily) LatestBetween(cursor, now int64) (Occurrence, bool, error) {
if now <= cursor {
return Occurrence{}, false, nil
...
}
핵심은 now <= cursor일 경우 아무것도 하지 않는 것입니다. 시계가 돌아가도 소화된 이력은 되돌아가지 않습니다.
또한, 현재 시각에 적용되는 장면을 직접 계산하기 때문에 통신이 끊겨 몇 시간이 경과해도 중간 화면을 순서대로 재생할 필요가 없습니다.
커서가 메모리에만 있다면, 재부팅 시 과거의 화면이 부활합니다.
그래서 Glypha는 커서와 현재 표시 대상을 SQLite에 동일한 트랜잭션으로 저장하고, 커밋에 성공한 후에 응답합니다.
저장에 실패한 경우에는 모호한 상태로 처리를 계속하지 않고, 서버를 실패 상태로 만듭니다. 화면 측은 그동안 마지막으로 정상적으로 표시된 내용을 계속 출력합니다.
이 방식에도 약점이 있습니다.
시계가 실수로 다음 해로 건너뛰고, 거기서 커서를 진행한 후 올바른 날짜로 돌아오면, 현재 시각이 커서에 도달할 때까지 전환되지 않습니다. Glypha에서는 새로운 콘텐츠를 공개하고 커서를 초기화하는 운영으로 복구합니다.
'과거를 재생하지 않는 것'과 '큰 시계 이상으로부터 자동 복구하는 것'은 단순하게 양립하기 어렵습니다. Glypha는 사이니지(signage)의 용도를 고려하여 전자를 선택했습니다.
시계에 따라 표시를 전환하는 시스템이라도 판단 자료를 현재 시각에만 의존하면, 시계가 되감기면서 과거를 재생해 버립니다.
Glypha에서는 다음 3가지 점에서 이를 방지하고 있습니다.
- 전환 시각과의 완전 일치가 아닌, 미소화 중 최신 장면을 선택함
- 소화된 범위를 커서로 표현하여 한 방향으로만 진행시킴
- 커서와 표시 대상을 영속화한 후 응답함
시간은 신뢰할 수 없을지라도, 이력을 단조롭게 진행하는 것은 가능합니다.
스케줄러를 설계할 때는 '몇 시에 무엇을 보여줄까'뿐만 아니라, 시계가 돌아갔을 때 과거를 재생할 것인가를 먼저 결정해 두면 구현 방침이 명확해집니다.