Handle keep_open_until: cancel, periodic poll, thread safety

- cancelKeepOpenDoor() closes the door when /allowed returns null
- background goroutine polls /allowed every 15s (KDOORPI_ALLOWED_POLL_INTERVAL)
- keepDoorOpenLock mutex guards shared hold state
- skip timer rebuild when the hold is unchanged (anti-thrash)
This commit is contained in:
Mykhailo Yermolenko
2026-06-17 16:42:54 +03:00
parent bd94037e29
commit 728939cef3
2 changed files with 156 additions and 12 deletions

View File

@@ -3,10 +3,28 @@ package main
import (
"fmt"
"log"
"sync"
"time"
)
var keepDoorOpenLock sync.Mutex
// keepDoorOpenGen identifies the currently armed hold timer. It is bumped (under
// keepDoorOpenLock) every time a new timer is installed so that a superseded
// timer's callback can recognise it is stale and do nothing.
var keepDoorOpenGen uint64
func updateKeepOpenDoor(newKeepOpenTime time.Time) {
keepDoorOpenLock.Lock()
defer keepDoorOpenLock.Unlock()
// Hold unchanged since the last poll: keep the existing timer rather than
// rebuilding it every poll, which would float the close time forward by up
// to one poll interval and re-pulse OpenDoor needlessly.
if keepDoorOpen.timer != nil && newKeepOpenTime.Equal(keepDoorOpen.until) {
return
}
// is there one active?
if keepDoorOpen.timer != nil {
keepDoorOpen.timer.Stop()
@@ -15,19 +33,65 @@ func updateKeepOpenDoor(newKeepOpenTime time.Time) {
if newKeepOpenTime.After(time.Now()) {
log.Printf("Keeping door open until %v", newKeepOpenTime)
OpenDoor(wiegand)
timer := time.AfterFunc(time.Until(newKeepOpenTime), handleKeepDoorOpenCloseCleanup)
if err := OpenDoor(wiegand); err != nil {
// Don't commit the hold if the relay didn't actually open: leaving
// keepDoorOpen empty (timer nil) means the next poll retries instead
// of latching the Equal early-return on a door that never opened.
log.Printf("ERROR opening door for hold: %v", err)
return
}
keepDoorOpenGen++
gen := keepDoorOpenGen
timer := time.AfterFunc(time.Until(newKeepOpenTime), func() {
handleKeepDoorOpenCloseCleanup(gen)
})
keepDoorOpen = KeepDoorOpen{
timer: timer,
until: newKeepOpenTime,
}
} else {
CloseDoor(wiegand)
if err := CloseDoor(wiegand); err != nil {
log.Printf("ERROR closing door: %v", err)
}
}
}
func handleKeepDoorOpenCloseCleanup() {
fmt.Println("Keep door open time is reached!")
CloseDoor(wiegand)
func cancelKeepOpenDoor() {
keepDoorOpenLock.Lock()
defer keepDoorOpenLock.Unlock()
if keepDoorOpen.timer == nil {
return
}
keepDoorOpen.timer.Stop()
if err := CloseDoor(wiegand); err != nil {
// Keep keepDoorOpen non-nil so the next poll's cancel retries the close;
// clearing it now would strand the door OPEN with no retry path.
log.Printf("ERROR closing door on hold cancel: %v", err)
return
}
keepDoorOpen = KeepDoorOpen{}
}
func handleKeepDoorOpenCloseCleanup(gen uint64) {
keepDoorOpenLock.Lock()
defer keepDoorOpenLock.Unlock()
// Timer.Stop() can return after this callback has already started and is
// blocked here on the lock; by the time we acquire it, updateKeepOpenDoor
// may have installed a newer hold. Only act if we are still the current
// generation, otherwise we would close the door and clear a live hold.
if gen != keepDoorOpenGen {
return
}
fmt.Println("Keep door open time is reached!")
if err := CloseDoor(wiegand); err != nil {
// Leave keepDoorOpen intact so the next poll (which will see
// keep_open_until=null and call cancelKeepOpenDoor) retries the close
// instead of stranding the door OPEN.
log.Printf("ERROR closing door at hold expiry: %v", err)
return
}
keepDoorOpen = KeepDoorOpen{}
}