Skip to content

Commit

Permalink
Use clock_nanosleep(2) for scheduling instead of time.Timer (#7)
Browse files Browse the repository at this point in the history
I suppose clock_nanosleep(2) handles susupend/resume.
  • Loading branch information
dsh2dsh committed Oct 27, 2024
1 parent 738afdc commit e35599f
Show file tree
Hide file tree
Showing 6 changed files with 175 additions and 3 deletions.
9 changes: 8 additions & 1 deletion internal/daemon/cron.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,23 @@ package daemon

import (
"context"
"time"

"github.com/dsh2dsh/cron/v3"

"github.com/dsh2dsh/zrepl/internal/daemon/logging"
"github.com/dsh2dsh/zrepl/internal/daemon/nanosleep"
"github.com/dsh2dsh/zrepl/internal/logger"
)

func newCron(ctx context.Context, verbose bool) *cron.Cron {
log := logging.GetLogger(ctx, logging.SubsysCron)
return cron.New(cron.WithLogger(newCronLogger(log, verbose)))
return cron.New(
cron.WithLogger(newCronLogger(log, verbose)),
cron.WithTimer(func(d time.Duration) cron.Timer {
return nanosleep.NewTimer(d)
}),
)
}

func newCronLogger(log logger.Logger, verbose bool) *cronLogger {
Expand Down
50 changes: 50 additions & 0 deletions internal/daemon/nanosleep/freebsd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
//go:build freebsd

package nanosleep

import (
"syscall"
"unsafe"

"golang.org/x/sys/unix"
)

// Do the interface allocations only once for common
// Errno values.
var (
errEAGAIN error = syscall.EAGAIN
errEINVAL error = syscall.EINVAL
errENOENT error = syscall.ENOENT
)

// A copy of linux implementation, because [golang.org/x/sys/unix] doesn't have
// it for FreeBSD.
func clockNanosleep(clockid int32, flags int, request *unix.Timespec,
remain *unix.Timespec,
) error {
_, _, e1 := unix.Syscall6(unix.SYS_CLOCK_NANOSLEEP,
uintptr(clockid), uintptr(flags),
uintptr(unsafe.Pointer(request)),
uintptr(unsafe.Pointer(remain)),
0, 0)
if e1 != 0 {
return errnoErr(e1)
}
return nil
}

// errnoErr returns common boxed Errno values, to prevent
// allocations at runtime.
func errnoErr(e syscall.Errno) error {
switch e {
case 0:
return nil
case unix.EAGAIN:
return errEAGAIN
case unix.EINVAL:
return errEINVAL
case unix.ENOENT:
return errENOENT
}
return e
}
11 changes: 11 additions & 0 deletions internal/daemon/nanosleep/linux.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
//go:build linux

package nanosleep

import "golang.org/x/sys/unix"

func clockNanosleep(clockid int32, flags int, request *unix.Timespec,
remain *unix.Timespec,
) error {
return unix.ClockNanosleep(clockid, flags, request, remain)
}
57 changes: 57 additions & 0 deletions internal/daemon/nanosleep/timer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package nanosleep

import (
"time"

"golang.org/x/sys/unix"
)

// See https://www.reddit.com/r/golang/comments/jeqmtt/wake_up_at_time/
func SleepUntil(t time.Time) error {
rqtp, err := unix.TimeToTimespec(t)
if err != nil {
return err
}

for {
err := clockNanosleep(unix.CLOCK_REALTIME, unix.TIMER_ABSTIME, &rqtp, nil)
if err != nil {
if err == unix.EINTR {
continue
}
return err
}
return nil
}
}

func NewTimer(d time.Duration) *Timer {
t := new(Timer)
t.Reset(d)
return t
}

type Timer struct {
ch <-chan time.Time
}

func (self *Timer) run(t time.Time, c chan<- time.Time) {
_ = SleepUntil(t)
c <- time.Now()
}

func (self *Timer) C() <-chan time.Time { return self.ch }

func (self *Timer) Reset(d time.Duration) bool {
// It just starts a new goroutine without stopping existsing one, because Stop
// does nothing. Yes, it's a goroutine leak, but I don't know how to stop
// clock_nanosleep.
wasRunning := self.Stop()
c := make(chan time.Time, 1)
self.ch = c
go self.run(time.Now().Add(d), c)
return wasRunning
}

// Stop does nothing, because I don't know how to stop clock_nanosleep.
func (self *Timer) Stop() bool { return true }
46 changes: 46 additions & 0 deletions internal/daemon/nanosleep/timer_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package nanosleep

import (
"testing"
"time"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestSleepUntil(t *testing.T) {
startTime := time.Now()
endTime := startTime.Add(time.Second)
require.NoError(t, SleepUntil(endTime))

d := time.Since(startTime)
assert.GreaterOrEqual(t, d, time.Second)
assert.Less(t, d, 2*time.Second)
}

func TestNewTimer(t *testing.T) {
startTime := time.Now()
timer := NewTimer(time.Second)
endTime := <-timer.C()

d := time.Since(startTime)
assert.GreaterOrEqual(t, d, time.Second)
assert.Less(t, d, 2*time.Second)
assert.GreaterOrEqual(t, endTime.Sub(startTime), time.Second)
assert.Less(t, endTime.Sub(startTime), 2*time.Second)
}

func TestTimer_Reset(t *testing.T) {
startTime := time.Now()
timer := NewTimer(time.Second)

time.Sleep(500 * time.Millisecond)
timer.Reset(time.Second)
endTime := <-timer.C()

d := time.Since(startTime)
assert.GreaterOrEqual(t, d, time.Second)
assert.Less(t, d, 2*time.Second)
assert.GreaterOrEqual(t, endTime.Sub(startTime), time.Second)
assert.Less(t, endTime.Sub(startTime), 2*time.Second)
}
5 changes: 3 additions & 2 deletions internal/daemon/snapper/periodic.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"github.com/dsh2dsh/zrepl/internal/daemon/job/wakeup"
"github.com/dsh2dsh/zrepl/internal/daemon/logging"
"github.com/dsh2dsh/zrepl/internal/daemon/logging/trace"
"github.com/dsh2dsh/zrepl/internal/daemon/nanosleep"
"github.com/dsh2dsh/zrepl/internal/util/envconst"
"github.com/dsh2dsh/zrepl/internal/zfs"
)
Expand Down Expand Up @@ -237,9 +238,9 @@ func periodicStateSyncUp(a periodicArgs, u updater) state {
})

if syncPoint.After(time.Now()) {
t := time.NewTimer(time.Until(syncPoint))
t := nanosleep.NewTimer(time.Until(syncPoint))
select {
case <-t.C:
case <-t.C():
case <-a.wakeSig:
t.Stop()
case <-a.ctx.Done():
Expand Down

0 comments on commit e35599f

Please sign in to comment.