Skip to content

Commit

Permalink
new package for scheduling
Browse files Browse the repository at this point in the history
  • Loading branch information
kneerunjun committed Dec 10, 2020
0 parents commit 31aff0a
Show file tree
Hide file tree
Showing 8 changed files with 474 additions and 0 deletions.
21 changes: 21 additions & 0 deletions compslice.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package scheduling

// ComparableSlice : would be used to keep relay ids
// we would want to know if there are common ids and some context we would need if all are matching
type ComparableSlice []string

// Intersection : tries to get the intersecting items from 2 slices
// items that are common to both
// items that are unique to cmpsl
// items that are unique to other
func (cmpsl ComparableSlice) Intersection(other ComparableSlice) (int, int, int) {
comm := 0
for _, item := range cmpsl {
for _, oitem := range other {
if item == oitem {
comm++
}
}
}
return comm, len(cmpsl) - comm, len(other) - comm
}
8 changes: 8 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module github.com/eensymachines-in/scheduling

go 1.15

require (
github.com/sirupsen/logrus v1.7.0
github.com/stretchr/testify v1.2.2
)
10 changes: 10 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
69 changes: 69 additions & 0 deletions rlystate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package scheduling

import "fmt"

// RelayState : this is just to hold the state of relay with the identification of the relay
// relay should be identified with the same name as required by srvrelay
// Storing this as just a byte is also possible, but that is when we want the relay module to work as a block, not when we want to operate on individual relays
type RelayState struct {
state byte
id string
}

// Status : Gets the state of the relay with ID
func (rs *RelayState) Status() map[string]byte {
return map[string]byte{rs.id: rs.state}
}

// Flip : flips the state of the relay
func (rs *RelayState) Flip() {
rs.state = byte(1) - rs.state
}

// State : sets the state of the relay
func (rs *RelayState) State(new byte) *RelayState {
if new > 0 {
rs.state = byte(1)
}
rs.state = byte(0)
return rs
}

// ID : spits out the id of the relay state
// this is generally the relay ID on the actual relay, IN1, IN2, IN3..
func (rs *RelayState) ID() string {
return rs.id
}

// NewRelayState : quick way to make a new relay state
func NewRelayState(id string) *RelayState {
return &RelayState{byte(0), id}
}

// ================================== Json Relay state is for file reads ============================
// Making a relay state from a json file

// JSONRelayState : relaystate but in json format
type JSONRelayState struct {
ON int `json:"on"`
OFF int `json:"off"`
IDs []string `json:"ids"`
Primary bool `json:"primary"`
}

// ToSchedule : reads from json and pumps up a schedule
// this saves you the trouble of making a schedule via code,
// from a json file it can read up a relaystate and convert that to schedule
func (jrs *JSONRelayState) ToSchedule() (Schedule, error) {
offs := []*RelayState{}
ons := []*RelayState{}
for _, id := range jrs.IDs {
offs = append(offs, &RelayState{byte(0), id})
ons = append(ons, &RelayState{byte(1), id})
}
trg1, trg2 := NewTrg(jrs.OFF, offs...), NewTrg(jrs.ON, ons...)
if jrs.Primary {
return NewPrimarySchedule(trg1, trg2)
}
return nil, fmt.Errorf("Schedule type unknown")
}
24 changes: 24 additions & 0 deletions sch_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package scheduling

import (
"testing"

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

func TestComparableSlice(t *testing.T) {
sl1 := ComparableSlice{"IN1", "IN2", "IN3"}
sl2 := ComparableSlice{"IN2", "IN3", "IN4", "IN1"}
matches, mismatch1, mismatch2 := sl1.Intersection(sl2)
assert.Equal(t, 3, matches, "Was expecting 2 matches in the slices above")
assert.Equal(t, 0, mismatch1, "Incorrect mismatches on the first")
assert.Equal(t, 1, mismatch2, "Incorrect mismatches on the second")

t.Log("--------------------------\n")
sl1 = ComparableSlice{}
sl2 = ComparableSlice{"IN2", "IN3", "IN4", "IN1"}
matches, mismatch1, mismatch2 = sl1.Intersection(sl2)
assert.Equal(t, 0, matches, "Was expecting 2 matches in the slices above")
assert.Equal(t, 0, mismatch1, "Incorrect mismatches on the first")
assert.Equal(t, 4, mismatch2, "Incorrect mismatches on the second")
}
173 changes: 173 additions & 0 deletions sched.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
package scheduling

import (
"encoding/json"
"fmt"
"time"

log "github.com/sirupsen/logrus"
)

// Schedule : is the handle for external packages
type Schedule interface {
Triggers() (Trigger, Trigger)
Duration() int
NearFarTrigger(elapsed int) (Trigger, Trigger, int, int)
ConflictsWith(another Schedule) bool
Midpoint() int
Close()
Apply(ok, cancel chan interface{}, send chan []byte, err chan error)
}

// primarySched : this schedule is the longer schedule and in all the cases there is only one of this
// primarySched is circular and beyond the triggers applies the last valid state of the trigger
type primarySched struct {
lower Trigger
higher Trigger
}

func (ps *primarySched) Triggers() (Trigger, Trigger) {
return ps.lower, ps.higher
}
func (ps *primarySched) Duration() int {
return ps.higher.At() - ps.lower.At()
}
func (ps *primarySched) Midpoint() int {
return (ps.Duration() / 2) + ps.lower.At()
}
func (ps *primarySched) Close() {
// For now all what the schedule does when closing is just ouput a log message
log.Infof("%s Schedule is now closing", ps)
}
func (ps *primarySched) String() string {
return fmt.Sprintf("%s - %s", tmStrFromUnixSecs(ps.lower.At()), tmStrFromUnixSecs(ps.higher.At()))
}

// NearFarTrigger : in context of the current time, this helps to get the triggers that are near or far
// For any schedule when its applied - pre sleep - nr state apply - post sleep - fr state apply
// For a primary schedule its thought to be circular, meaning to say : if beyond the trigger bounds the higher trigger is applied
func (ps *primarySched) NearFarTrigger(elapsed int) (Trigger, Trigger, int, int) {
// for primary schedule nr trigger will be applied then, sleep, then fr state
// for primary schedule there is no pre sleep - since its circular and applies beyond the 2 triggers as well
var nr, fr Trigger
var post int
if elapsed >= ps.lower.At() && elapsed < ps.higher.At() {
nr, fr = ps.lower, ps.higher
post = ps.higher.At() - elapsed
} else {
nr, fr = ps.higher, ps.lower
if elapsed < ps.lower.At() {
post = ps.lower.At() - elapsed
}
if elapsed > ps.higher.At() {
post = 86400 - elapsed + ps.lower.At()
}
}
return nr, fr, 0, post // for primary schedules pre sleep is always 0
}

func (ps *primarySched) overlapsWith(another Schedule) bool {
// Midpoints are distance of the half time since midnight for any schedule
mdpt1, mdpt2 := ps.Midpoint(), another.Midpoint()
// half duration of each schedule
hfdur1, hfdur2 := ps.Duration()/2, another.Duration()/2
// getting the absolute of the midpoint distance
mdptdis := mdpt1 - mdpt2
if mdptdis < 0 {
mdptdis = -mdptdis
}
// Getting the larger of the 2 schedules
var min, max int
if hfdur1 <= hfdur2 {
min, max = hfdur1, hfdur2
} else {
min, max = hfdur2, hfdur1
}
if (mdptdis > (hfdur1 + hfdur2)) || ((mdptdis + min) < max) {
// case when the schedules are clearing and not interferring with one another
// either one schedule is inside the other or on one side
return false
}
// all other cases the schedules are either partially/exactly overlapping
return true
}

// ConflictsWith : checks to see partial overlapping of schedules
func (ps *primarySched) ConflictsWith(another Schedule) bool {
_, ok := another.(*primarySched)
if ok {
// Always conflicts with other primary schedule
// overlaps are checked for circular and non-cicrular schedule
return true
}
return ps.overlapsWith(another)
}

func (ps *primarySched) Apply(ok, cancel chan interface{}, send chan []byte, err chan error) {
nr, fr, pre, post := ps.NearFarTrigger(elapsedSecondsNow())
if pre > 0 {
// this will work as expected even when pre=0, but the problem is it sill still allow the processor to jump to the next task
<-time.After(time.Duration(pre) * time.Duration(1*time.Second))
}
byt, e := json.Marshal(nr)
if e != nil { // state of the trigger is applied
err <- fmt.Errorf("Schedule/Apply: Failed to marshall trigger data - %s", e)
return
}
send <- byt
select {
// sleep duration is always a second extra than the sleep time
// so that incase the processor is fast enough this will still be in the next slot
case <-time.After(time.Duration(post+1) * time.Duration(1*time.Second)):
log.Info("End of schedule.. ")
if byt, e = json.Marshal(fr); e != nil {
err <- fmt.Errorf("Schedule/Apply: Failed to marshall trigger data - %s", e)
return
}
send <- byt
ok <- struct{}{}
case <-cancel:
log.Warn("Schedule/Apply: Interruption")
ps.Close()
}
return
}

// Loop : this shall loop the schedule forever till there is a interruption or the schedule application fails
func (ps *primarySched) Loop(cancel, interrupt chan interface{}, send chan []byte, loopErr chan error) {
// this channnel communicates the ok from apply function
// The loop still does not indicate done unless ofcourse the done <-nil
ok := make(chan interface{}, 1)
defer close(ok)
stop := make(chan interface{}) // this is to stop the currently running schedule
for {
ps.Apply(ok, stop, send, loopErr) // applies the schedul infinitely
select {
case <-cancel:
case <-interrupt:
close(stop)
log.Warn("Running schedule is stopped or interrupted, now closing the loop as well")
return
case <-ok:
// this is when the schedule has done applying for one cycle
// will go back to applying the next schedule for the then current time
}
}
}

// NewPrimarySchedule : makes a new TriggeredSchedul, will take 2 triggers
func NewPrimarySchedule(trg1, trg2 Trigger) (Schedule, error) {
if trg1.At() == trg2.At() {
return nil, fmt.Errorf("ERROR/NewPrimarySchedule: triggers cannot be overlapping")
}
if !trg1.IdenticalRelays(trg2) {
return nil, fmt.Errorf("ERROR/NewPrimarySchedule:triggers are paired with same relay ids")
}
var l, h Trigger
if trg1.At() < trg2.At() {
l, h = trg1, trg2
} else {
l, h = trg2, trg1
}
return &primarySched{l, h}, nil
}
47 changes: 47 additions & 0 deletions time.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package scheduling

import (
"fmt"
"time"
)

const (
// I tried to change PM to AM in the format, and then it fails to read PM. but when PM in format, it reads both
// https://stackoverflow.com/questions/44924628/golang-time-parse-1122-pm-something-that-i-can-do-math-with
format = "03:04 PM"
mdNt = "12:00 AM"
)

// TimeStr : custom definition of time as string, indicated of the format above
type TimeStr string

// ToElapsedTm : just converts the string time to
func (ts TimeStr) ToElapsedTm() (int, error) {
mdntTm, _ := time.Parse(format, mdNt) // getting the midnight time
tm, err := time.Parse(format, string(ts))
if err != nil {
return -1, err
}
elapsed := tm.Sub(mdntTm).Seconds()
return int(elapsed), nil
}

// tmStrFromUnixSecs : for the unix seconds given this can convert that into TimeStr
// this application uses specific format of clock so that its compatible to PArsing of time
func tmStrFromUnixSecs(elapsed int) TimeStr {
hr, rem := elapsed/3600, elapsed%3600
min := rem / 60
// fmt.Printf("%d %d\n", hr, min)
ampm := "AM"
if hr >= 12 { // noon 12 is 12:01, while midnight 12 is 00:01
hr = hr - 12
ampm = "PM"
}
return TimeStr(fmt.Sprintf("%02d:%02d %s", hr, min, ampm))
}

// elapsedSecondsNow : this can for any given day, calculate the seconds that have elapsed since midnight
func elapsedSecondsNow() int {
hr, min, sec := time.Now().Clock()
return (hr * 3600) + (min * 60) + sec
}
Loading

0 comments on commit 31aff0a

Please sign in to comment.