diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..506992f --- /dev/null +++ b/go.mod @@ -0,0 +1,26 @@ +module circuit-breaker + +go 1.22.6 + +require ( + github.com/onsi/ginkgo v1.16.5 + github.com/onsi/gomega v1.34.2 + github.com/redis/go-redis/v9 v9.6.1 + github.com/stretchr/testify v1.9.0 +) + +require ( + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/fsnotify/fsnotify v1.4.9 // indirect + github.com/google/go-cmp v0.6.0 // indirect + github.com/nxadm/tail v1.4.8 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/stretchr/objx v0.5.2 // indirect + golang.org/x/net v0.28.0 // indirect + golang.org/x/sys v0.24.0 // indirect + golang.org/x/text v0.17.0 // indirect + gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..7baae0d --- /dev/null +++ b/go.sum @@ -0,0 +1,115 @@ +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= +github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +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/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5 h1:5iH8iuqE5apketRbSFBy+X1V0o+l+8NF1avt4HWl7cA= +github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= +github.com/onsi/ginkgo/v2 v2.20.1 h1:YlVIbqct+ZmnEph770q9Q7NVAz4wwIiVNahee6JyUzo= +github.com/onsi/ginkgo/v2 v2.20.1/go.mod h1:lG9ey2Z29hR41WMVthyJBGUBcBhGOtoPF2VFMvBXFCI= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.34.2 h1:pNCwDkzrsv7MS9kpaQvVb1aVLahQXyJ/Tv5oAZMI3i8= +github.com/onsi/gomega v1.34.2/go.mod h1:v1xfxRgk0KIsG+QOdm7p8UosrOzPYRo60fd3B/1Dukc= +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/redis/go-redis/v9 v9.6.1 h1:HHDteefn6ZkTtY5fGUE8tj8uy85AHk6zP7CpzIAM0y4= +github.com/redis/go-redis/v9 v9.6.1/go.mod h1:0C0c6ycQsdpVNQpxb1njEQIqkx5UcsM8FJCQLgE9+RA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= +golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= +golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24= +golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= +google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..480a407 --- /dev/null +++ b/readme.md @@ -0,0 +1,163 @@ +# Overview + +The Circuit Breaker package provides a resilient pattern for making HTTP requests to internal and external services, handling failures gracefully to prevent cascading failures. + +# States + +The Circuit Breaker package defines three states for managing the flow of requests to internal or external services. + +## Closed 🟩 + +In this default state, requests are sent directly to the service. Failures increment a failure count, which, if it exceeds a configured threshold, triggers a transition to the Open state. + +**Note** Failures here refer to the scenario when the request fails due to service unavailability, timeouts, or internal server errors (HTTP status codes 500 and above) + +## Open 🛑 + +The circuit breaker stops all attempts to send requests to the service to prevent failure overload, immediately returning an error for all attempts. After a configurable timeout, it transitions to Half-Open. + +## Half-Open ⚠️ + +Limited numbers of test requests are allowed to pass through. If these requests succeed without an error, the circuit breaker transitions back to Closed, indicating the service is again healthy. If failures occur, it returns to Open. + +# Configurations ⚙️ + +- **TimeoutInterval**: Duration before a request times out. +- **MaxFailures**: Failures threshold for opening the circuit. +- **OpenToHalfOpenWait**: Duration before attempting to reset from Open to Half-Open. +- **HalfOpenMaxSuccess**: Successful requests threshold for closing the circuit from Half-Open. +- **HalfOpenMaxFailures**: Failures threshold for reopening the circuit from Half-Open. +- **RetryIntervals**: Slice of durations for retry intervals between requests. + +**Default Configurations** + +- **TimeoutInterval:** 10 seconds +- **MaxFailures:** 5 +- **OpenToHalfOpenWait:** 30 seconds +- **HalfOpenMaxSuccess:** 5 +- **HalfOpenMaxFailures:** 3 +- **RetryIntervals: \[**1 seconds, 2 seconds, 3 seconds, 5 seconds, 8 seconds**\]** + +# Response Types + +The CircuitBreaker package defines three types of response types, each indicating a different outcome of a request processed through the circuit breaker: + +## Success + +This response type indicates that the request was successfully executed without encountering server errors (HTTP status codes below 500). + +E.g + +```json +{ + "http-status": 200, + "response-type": "success", + "data": { + "message": "Data retrieved successfully" + } +} +``` + +## Fallback + +When the circuit is open (preventing further requests to the external service to avoid overloading), and a fallback function is provided, the response type is marked as Fallback. This type signifies that the response comes from the fallback logic. + +E.g + +```json +{ + "http-status": 200, + "response-type": "fallback", + "data": { + "message": "This is a fallback response due to service unavailability." + } +} +``` + +## Error + +This response type is used when the request fails due to service unavailability, timeouts, or internal server errors (HTTP status codes 500 and above). It also covers scenarios where the circuit is open, and no fallback function is provided, indicating that the request cannot be processed. + +E.g + +```json +{ + "response-type": "error", + "error": { + "code": 503, + "message": "Circuit is open" + } +} +``` + +# Usage + +An example is added to the repository to demonstrate the usage of the package. Here is a description of the same + +[Code Link](https://github.com/paper-indonesia/pdk/blob/main/example/circuit-breaker/main.go) +
+ +```golang +package main + +import ( + "fmt" + "log" + "net/http" + "pdk/go/circuitbreaker" + "time" +) + +func fallbackFunc(req *http.Request) *circuitbreaker.CircuitBreakerResponse { + // This is where you define your fallback logic. For example, return a static response or call an alternative service. + // The following is a simple static response for demonstration purposes. + return &circuitbreaker.CircuitBreakerResponse{ + HttpStatus: http.StatusOK, + ResponseType: circuitbreaker.Fallback, + Data: map[string]interface{}{ + "message": "This is a fallback response due to circuit breaker open state.", + }, + } +} + +func main() { + customConfig := circuitbreaker.Config{ + TimeoutInterval: 5 * time.Second, // Request timeout interval + MaxFailures: 3, // Number of failures to open the circuit + OpenToHalfOpenWait: 1 * time.Minute, // Time to wait before transitioning from OPEN to HALF-OPEN + HalfOpenMaxSuccess: 2, // Number of successes to close the circuit from HALF-OPEN + HalfOpenMaxFailures: 1, // Number of failures to reopen the circuit from HALF-OPEN + RetryIntervals: []time.Duration{ // Retry intervals after a failure + 500 * time.Millisecond, + 1 * time.Second, + 2 * time.Second, + }, + } + + cb := circuitbreaker.NewCircuitBreaker(customConfig, "example") + cb.SetFallbackFunc(fallbackFunc) + + requestURL := "http://example.com" + req, err := http.NewRequest("GET", requestURL, nil) + if err != nil { + log.Fatalf("Failed to create request: %v", err) + } + + response := cb.DoRequest(req) + if response.Error != nil { + log.Printf("Request failed with error: %v", response.Error) + return + } + + if response.ResponseType == circuitbreaker.Fallback { + fmt.Println("Fallback response received.") + } else { + fmt.Printf("Received response with status code: %d\n", response.HttpStatus) + } +} + +``` + +🎉 + +# \ No newline at end of file diff --git a/v1/circuitBreaker.go b/v1/circuitBreaker.go new file mode 100644 index 0000000..5665b6d --- /dev/null +++ b/v1/circuitBreaker.go @@ -0,0 +1,189 @@ +package circuitbreaker + +import ( + "context" + "encoding/json" + "io" + "log" + "net/http" + "time" + + "github.com/redis/go-redis/v9" +) + +var ctx = context.Background() + +func (cb *CircuitBreaker) DoRequest(req *http.Request) *CircuitBreakerResponse { + cb.mutex.Lock() + state := cb.getState() + cb.mutex.Unlock() + + if state == Open && cb.fallbackFunc == nil { + return &CircuitBreakerResponse{ + ResponseType: Error, + Error: &ErrorDetail{ + Code: http.StatusServiceUnavailable, + Message: "Circuit is open", + }, + } + } else if state == Open && cb.fallbackFunc != nil { + return cb.fallbackFunc(req) + } + + var lastErr error + for _, interval := range cb.config.RetryIntervals { + resp, err := http.DefaultClient.Do(req) + if err == nil && resp.StatusCode < 500 { + cb.recordSuccess() + + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err != nil { + return &CircuitBreakerResponse{ + ResponseType: Error, + Error: &ErrorDetail{ + Code: http.StatusInternalServerError, + Message: "Failed to read response body", + Raw: err, + }, + } + } + + var data interface{} + if err := json.Unmarshal(body, &data); err != nil { + return &CircuitBreakerResponse{ + ResponseType: Error, + Error: &ErrorDetail{ + Code: http.StatusInternalServerError, + Message: "Failed to unmarshal response body", + Raw: err, + }, + } + } + + return &CircuitBreakerResponse{ + HttpStatus: resp.StatusCode, + ResponseType: Success, + Data: data, + Raw: string(body), + } + } + lastErr = err + time.Sleep(interval) + cb.recordFailure() + } + + if lastErr != nil && cb.fallbackFunc != nil { + return cb.fallbackFunc(req) + } + + return &CircuitBreakerResponse{ + ResponseType: Error, + Error: &ErrorDetail{ + Code: http.StatusInternalServerError, + Message: "Failed to execute request", + Raw: lastErr, + }, + } +} + +func (cb *CircuitBreaker) SetFallbackFunc(f func(*http.Request) *CircuitBreakerResponse) { + cb.fallbackFunc = f +} + +func (cb *CircuitBreaker) syncStateWithRedis() { + stateVal, err := cb.redisClient.Get(ctx, cb.name).Result() + if err == redis.Nil { + cb.setState(Closed) + } else if err == nil { + cb.setState(State(stateVal)) + } else { + log.Printf("Error fetching state for %s from Redis: %v", cb.name, err) + } +} + +func (cb *CircuitBreaker) setState(state State) { + cb.mutex.Lock() + defer cb.mutex.Unlock() + + strState := string(state) + err := cb.redisClient.Set(ctx, cb.name, strState, 0).Err() + if err != nil { + log.Printf("Error updating state for %s in Redis: %v", cb.name, err) + return + } +} + +func (cb *CircuitBreaker) getState() State { + stateVal, err := cb.redisClient.Get(ctx, cb.name).Result() + if err != nil { + log.Printf("Error fetching state for %s from Redis: %v", cb.name, err) + return Closed + } + return State(stateVal) +} + +func (cb *CircuitBreaker) startTimer() { + state := cb.getState() + if state == Open { + cb.timer = time.AfterFunc(cb.config.OpenToHalfOpenWait, func() { + cb.setState(HalfOpen) + cb.failures = 0 + cb.success = 0 + log.Println("Circuit breaker transitioned to HALF-OPEN") + }) + } +} + +func (cb *CircuitBreaker) stopTimer() { + cb.mutex.Lock() + defer cb.mutex.Unlock() + + if cb.timer != nil { + cb.timer.Stop() + cb.timer = nil + } +} + +func (cb *CircuitBreaker) recordFailure() { + state := cb.getState() + now := time.Now() + if state == Closed { + if cb.failures == 0 || now.Sub(cb.lastFail) <= time.Minute { + cb.failures++ + } else { + cb.failures = 1 + } + cb.lastFail = now + + if cb.failures >= cb.config.MaxFailures { + cb.failures = 0 + cb.setState(Open) + cb.startTimer() + log.Println("Circuit breaker transitioned to OPEN") + } + } else if state == HalfOpen { + cb.failures++ + if cb.failures >= cb.config.HalfOpenMaxFailures { + cb.failures = 0 + cb.success = 0 + cb.setState(Open) + cb.startTimer() + log.Println("Circuit breaker transitioned to OPEN from HALF-OPEN due to failures") + } + } +} + +func (cb *CircuitBreaker) recordSuccess() { + state := cb.getState() + if state == HalfOpen { + cb.success++ + if cb.success >= cb.config.HalfOpenMaxSuccess { + cb.failures = 0 + cb.success = 0 + cb.setState(Closed) + cb.stopTimer() + log.Println("Circuit breaker transitioned to CLOSE from HALF-OPEN due to successes") + } + } +} diff --git a/v1/circuitBreaker_test.go b/v1/circuitBreaker_test.go new file mode 100644 index 0000000..e99566b --- /dev/null +++ b/v1/circuitBreaker_test.go @@ -0,0 +1,159 @@ +package circuitbreaker + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "sync" + "time" + + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/redis/go-redis/v9" + "github.com/stretchr/testify/mock" +) + +type MockRedisClient struct { + mock.Mock +} + +type RedisClientMock interface { + Get(ctx context.Context, key string) *redis.StringCmd + Set(ctx context.Context, key string, value interface{}, expiration time.Duration) *redis.StatusCmd +} + +func (m *MockRedisClient) Get(ctx context.Context, key string) *redis.StringCmd { + args := m.Called(ctx, key) + return args.Get(0).(*redis.StringCmd) +} + +func (m *MockRedisClient) Set(ctx context.Context, key string, value interface{}, expiration time.Duration) *redis.StatusCmd { + args := m.Called(ctx, key, value, expiration) + return args.Get(0).(*redis.StatusCmd) +} + +var _ = Describe("CircuitBreaker", func() { + var ( + cb *CircuitBreaker + server *httptest.Server + mux *http.ServeMux + mockedRedisClient *MockRedisClient + ) + + BeforeEach(func() { + mux = http.NewServeMux() + server = httptest.NewServer(mux) + + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + data := map[string]interface{}{ + "key": "value", + } + + w.Header().Set("Content-Type", "application/json") + + err := json.NewEncoder(w).Encode(data) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) + }) + }) + + AfterEach(func() { + server.Close() + }) + + Describe("DoRequest", func() { + Context("when circuit is CLOSED", func() { + It("should successfully execute the request", func() { + mockedRedisClient = new(MockRedisClient) + var cbState redis.StringCmd + cbState.SetVal("CLOSED") + mockedRedisClient.On("Get", mock.Anything, "test").Return(&cbState) + mockedRedisClient.On("Set", mock.Anything, "test", "CLOSED", mock.Anything).Return(&redis.StatusCmd{}) + cb = NewCircuitBreaker(DefaultConfig, "test", mockedRedisClient) + req, _ := http.NewRequest("GET", server.URL, nil) + resp := cb.DoRequest(req) + Expect(resp.ResponseType).To(Equal(Success)) + Expect(resp.Error).To(BeNil()) + }) + }) + + Context("when circuit is OPEN", func() { + It("should not execute further requests", func() { + mockedRedisClient = new(MockRedisClient) + var cbState redis.StringCmd + cbState.SetVal("OPEN") + mockedRedisClient.On("Get", mock.Anything, "test").Return(&cbState) + mockedRedisClient.On("Set", mock.Anything, "test", "OPEN", mock.Anything).Return(&redis.StatusCmd{}) + cb = NewCircuitBreaker(DefaultConfig, "test", mockedRedisClient) + req, _ := http.NewRequest("GET", server.URL+"/fail", nil) + resp := cb.DoRequest(req) + Expect(resp.ResponseType).To(Equal(Error)) + Expect(resp.Error).ToNot(BeNil()) + }) + }) + + Context("when request fails and fallback is provided", func() { + It("should execute the fallback function", func() { + mockedRedisClient = new(MockRedisClient) + var cbState redis.StringCmd + cbState.SetVal("OPEN") + mockedRedisClient.On("Get", mock.Anything, "test").Return(&cbState) + mockedRedisClient.On("Set", mock.Anything, "test", "OPEN", mock.Anything).Return(&redis.StatusCmd{}) + cb = NewCircuitBreaker(DefaultConfig, "test", mockedRedisClient) + cb.SetFallbackFunc(func(req *http.Request) *CircuitBreakerResponse { + return &CircuitBreakerResponse{ + ResponseType: "fallback", + } + }) + req, _ := http.NewRequest("GET", server.URL+"/fail", nil) + resp := cb.DoRequest(req) + Expect(resp.ResponseType).To(Equal(Fallback)) + }) + }) + + Context("with concurrent requests", func() { + It("should handle concurrent requests correctly", func() { + var wg sync.WaitGroup + var mu sync.Mutex + successCount := 0 + failureCount := 0 + + mockedRedisClient = new(MockRedisClient) + var cbState redis.StringCmd + cbState.SetVal("CLOSED") + mockedRedisClient.On("Get", mock.Anything, "test").Return(&cbState) + mockedRedisClient.On("Set", mock.Anything, "test", "CLOSED", mock.Anything).Return(&redis.StatusCmd{}) + cb = NewCircuitBreaker(DefaultConfig, "test", mockedRedisClient) + for i := 0; i < 4; i++ { + wg.Add(1) + go func() { + defer wg.Done() + req, _ := http.NewRequest("GET", server.URL, nil) + resp := cb.DoRequest(req) + mu.Lock() + defer mu.Unlock() + if resp.ResponseType == "success" { + successCount++ + } else { + failureCount++ + } + }() + } + wg.Wait() + Expect(successCount + failureCount).To(Equal(4)) + }) + }) + }) +}) + +func TestCircuitBreaker(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "CircuitBreaker Suite") +} diff --git a/v1/init.go b/v1/init.go new file mode 100644 index 0000000..69de992 --- /dev/null +++ b/v1/init.go @@ -0,0 +1,12 @@ +package circuitbreaker + +func NewCircuitBreaker(config Config, name string, rdb RedisClient) *CircuitBreaker { + cb := &CircuitBreaker{ + name: name, + config: config, + redisClient: rdb, + } + cb.syncStateWithRedis() + cb.startTimer() + return cb +} diff --git a/v1/type.go b/v1/type.go new file mode 100644 index 0000000..eb51aea --- /dev/null +++ b/v1/type.go @@ -0,0 +1,75 @@ +package circuitbreaker + +import ( + "context" + "net/http" + "sync" + "time" + + "github.com/redis/go-redis/v9" +) + +type Config struct { + TimeoutInterval time.Duration + MaxFailures int + OpenToHalfOpenWait time.Duration + HalfOpenMaxSuccess int + HalfOpenMaxFailures int + RetryIntervals []time.Duration +} + +var DefaultConfig = Config{ + TimeoutInterval: 10 * time.Second, + MaxFailures: 5, + OpenToHalfOpenWait: 30 * time.Second, + HalfOpenMaxSuccess: 5, + HalfOpenMaxFailures: 3, + RetryIntervals: []time.Duration{1 * time.Second, 2 * time.Second, 3 * time.Second, 5 * time.Second, 8 * time.Second}, +} + +type State string + +const ( + Closed State = "CLOSED" + Open State = "OPEN" + HalfOpen State = "HALF OPEN" +) + +type ResponseType string + +const ( + Success ResponseType = "success" + Fallback ResponseType = "fallback" + Error ResponseType = "error" +) + +type CircuitBreaker struct { + name string + mutex sync.Mutex + config Config + failures int + success int + lastFail time.Time + timer *time.Timer + fallbackFunc func(*http.Request) *CircuitBreakerResponse + redisClient RedisClient +} + +type ErrorDetail struct { + Code int `json:"code,omitempty"` + Message string `json:"message,omitempty"` + Raw error `json:"-"` +} + +type CircuitBreakerResponse struct { + HttpStatus int `json:"http-status"` + ResponseType ResponseType `json:"response-type"` + Data interface{} `json:"data,omitempty"` + Error *ErrorDetail `json:"error,omitempty"` + Raw interface{} `json:"raw,omitempty"` +} + +type RedisClient interface { + Get(ctx context.Context, key string) *redis.StringCmd + Set(ctx context.Context, key string, value interface{}, expiration time.Duration) *redis.StatusCmd +}