Skip to content

Commit

Permalink
init commit
Browse files Browse the repository at this point in the history
  • Loading branch information
bstrdlord authored Aug 10, 2024
1 parent a4467e0 commit c0c9637
Show file tree
Hide file tree
Showing 7 changed files with 563 additions and 0 deletions.
147 changes: 147 additions & 0 deletions failover.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
package failover

import (
"errors"
"fmt"
"net/url"
"sync"
"sync/atomic"
"time"
)

type Failover interface {
AddUrl(url *url.URL) error // adds one url
AddUrls(urls ...*url.URL) error // adds multiple urls

Request(requestFunc func(url *url.URL) error, localOpts ...func(*Options)) error // sends a request to the url using your requestFunc

MustParseURL(url string) *url.URL // panics if url is invalid
ParseURL(url string) (*url.URL, error) // returns error if url is invalid
}

type onErrType uint8

const (
ReqOnErrRemoveAndReconnect onErrType = iota // remove url and reconnect (default attempts - 3)
ReqOnErrIgnore // ignore error (returns nil)
ReqOnErrReturnErr // return error
ReqOnErrReconnectNext // reconnect (to next url)
ReqOnErrReconnectCurrent // reconnect (to current url)
)

type Options struct {
CheckConn func(url *url.URL) error // function for checking url
CheckUrlBeforeAdding bool // check url before adding (using CheckConn)
CheckUrlDelay time.Duration // delay before checking url (defualt 30s)
ReqOnErr onErrType // what to do on request error
MaxAttempts uint16 // max attempts (default 3)

}
type storage struct {
activeUrls *atomic.Pointer[[]*url.URL]
badUrls *atomic.Pointer[[]*url.URL]
roundRobin roundRobin
options *Options
mu *sync.Mutex
startCronChan chan bool
}

func New(checkConnection func(url *url.URL) error, options ...func(*Options)) Failover {
activeUrls := &atomic.Pointer[[]*url.URL]{}
badUrls := &atomic.Pointer[[]*url.URL]{}

opts := &Options{ // default options
CheckUrlBeforeAdding: true,
CheckUrlDelay: time.Second * 30,
CheckConn: checkConnection,
}

for _, opt := range options {
opt(opts)
}

s := &storage{roundRobin: newRoundRobin(activeUrls), badUrls: badUrls, activeUrls: activeUrls, options: opts, mu: &sync.Mutex{}, startCronChan: make(chan bool)}

// go printUrls(s.activeUrls, s.badUrls)
go s.runCronUrlCheck()

return s

}

func (s *storage) AddUrls(urls ...*url.URL) error {
for _, url := range urls {
err := s.AddUrl(url)
if err != nil {
return err
}
}
return nil
}

func (s *storage) AddUrl(url__ *url.URL) error {
s.mu.Lock()
defer s.mu.Unlock()

if url__ == nil {
return errors.New("url is nil")
}

if s.options.CheckUrlBeforeAdding {
err := s.options.CheckConn(url__)
if err != nil {
return fmt.Errorf("check connection: %w", err)
}
}

urlsPtr := s.activeUrls.Load()
if urlsPtr == nil {
s.activeUrls.Store(&[]*url.URL{url__})
return nil
}
urls := *urlsPtr

urls = removeUrl(urls, url__) // remove url if already exists
urls = append(urls, url__) // append unique url

s.activeUrls.Store(&urls)

return nil
}

func (s *storage) Request(requestFunc func(url *url.URL) error, localOpts ...func(*Options)) error {
s.mu.Lock()
defer s.mu.Unlock()

urlsPtr := s.activeUrls.Load()
if urlsPtr == nil {
return errors.New("no urls found")
}

urls := *urlsPtr

url, found := s.roundRobin.Next()
if !found {
return errors.New("round robin: no urls found")
}

optsCopy := *s.options // we need to copy opts because we need to change it (locally)

for _, opt := range localOpts {
opt(&optsCopy)
}

return s.request(requestFunc, &optsCopy, urls, url, 1)
}

func (s *storage) MustParseURL(url__ string) *url.URL {
u, err := url.Parse(url__)
if err != nil {
panic(err)
}
return u
}

func (s *storage) ParseURL(url__ string) (*url.URL, error) {
return url.Parse(url__)
}
78 changes: 78 additions & 0 deletions failover_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package failover

import (
"fmt"
"net/http"
"net/url"
"testing"
"time"
)

func TestFailover(t *testing.T) {
f := New(CheckConnection,
OptCheckUrlBeforeAdding(true),
OptCheckUrlDelay(10*time.Second),
OptMaxAttempts(10),
)

tests := []*url.URL{
// {Host: "google.com", Scheme: "https"},
{Host: "0.0.0.0:8080", Scheme: "http"},
{Host: "0.0.0.0:8081", Scheme: "http"},
{Host: "0.0.0.0:8081", Scheme: "http"},
{Host: "0.0.0.0:8081", Scheme: "http"},
{Host: "0.0.0.0:8082", Scheme: "http"},
{Host: "0.0.0.0:8082", Scheme: "http"},
{Host: "0.0.0.0:8083", Scheme: "http"},
{Host: "0.0.0.0:8084", Scheme: "http"},
{Host: "0.0.0.0:8085", Scheme: "http"},
{Host: "0.0.0.0:8086", Scheme: "http"},
{Host: "0.0.0.0:8087", Scheme: "http"},
}

err := f.AddUrl(tests[0])
if err != nil {
t.Fatal(err)
}

err = f.AddUrl(tests[0]) // duplicate
if err != nil {
t.Fatal(err)
}

err = f.AddUrl(tests[1])
if err != nil {
t.Fatal(err)
}

err = f.AddUrls(tests...)
if err != nil {
t.Fatal(err)
}

time.Sleep(5 * time.Second)
// fmt.Println("STATUS", f.Request(Request, OptReqOnErr(ReqOnErrReconnectNext), OptMaxAttempts(4)))
fmt.Println("STATUS", f.Request(Request, OptReqOnErr(ReqOnErrReconnectNext), OptMaxAttempts(2)))
// f.Request(Request)
// f.Request(Request)
// f.Request(Request)

}

func CheckConnection(url *url.URL) error {
_, err := http.Get(url.String())
if err != nil {
fmt.Println(err)
return err
}
return nil
}

func Request(url *url.URL) error {
_, err := http.Get("http://" + url.Host)
if err != nil {
return err
}

return nil
}
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module failover

go 1.22.5
Loading

0 comments on commit c0c9637

Please sign in to comment.