initial commit
This commit is contained in:
43
.gitignore
vendored
Normal file
43
.gitignore
vendored
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
.idea/**/workspace.xml
|
||||||
|
.idea/**/tasks.xml
|
||||||
|
.idea/**/usage.statistics.xml
|
||||||
|
.idea/**/dictionaries
|
||||||
|
.idea/**/shelf
|
||||||
|
.idea/**/aws.xml
|
||||||
|
.idea/**/contentModel.xml
|
||||||
|
.idea/**/dataSources/
|
||||||
|
.idea/**/dataSources.ids
|
||||||
|
.idea/**/dataSources.local.xml
|
||||||
|
.idea/**/sqlDataSources.xml
|
||||||
|
.idea/**/dynamic.xml
|
||||||
|
.idea/**/uiDesigner.xml
|
||||||
|
.idea/**/dbnavigator.xml
|
||||||
|
.idea/**/gradle.xml
|
||||||
|
.idea/**/libraries
|
||||||
|
cmake-build-*/
|
||||||
|
.idea/**/mongoSettings.xml
|
||||||
|
*.iws
|
||||||
|
out/
|
||||||
|
.idea_modules/
|
||||||
|
atlassian-ide-plugin.xml
|
||||||
|
.idea/replstate.xml
|
||||||
|
.idea/sonarlint/
|
||||||
|
com_crashlytics_export_strings.xml
|
||||||
|
crashlytics.properties
|
||||||
|
crashlytics-build.properties
|
||||||
|
fabric.properties
|
||||||
|
.idea/httpRequests
|
||||||
|
.idea/caches/build_file_checksums.ser
|
||||||
|
*~
|
||||||
|
.fuse_hidden*
|
||||||
|
.directory
|
||||||
|
.Trash-*
|
||||||
|
.nfs*
|
||||||
|
*.exe
|
||||||
|
*.exe~
|
||||||
|
*.dll
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
*.test
|
||||||
|
*.out
|
||||||
|
go.work
|
||||||
8
.idea/.gitignore
generated
vendored
Normal file
8
.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# Editor-based HTTP Client requests
|
||||||
|
/httpRequests/
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
||||||
9
.idea/commander.iml
generated
Normal file
9
.idea/commander.iml
generated
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="WEB_MODULE" version="4">
|
||||||
|
<component name="Go" enabled="true" />
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$" />
|
||||||
|
<orderEntry type="inheritedJdk" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
6
.idea/git_toolbox_blame.xml
generated
Normal file
6
.idea/git_toolbox_blame.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="GitToolBoxBlameSettings">
|
||||||
|
<option name="version" value="2" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
15
.idea/git_toolbox_prj.xml
generated
Normal file
15
.idea/git_toolbox_prj.xml
generated
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="GitToolBoxProjectSettings">
|
||||||
|
<option name="commitMessageIssueKeyValidationOverride">
|
||||||
|
<BoolValueOverride>
|
||||||
|
<option name="enabled" value="true" />
|
||||||
|
</BoolValueOverride>
|
||||||
|
</option>
|
||||||
|
<option name="commitMessageValidationEnabledOverride">
|
||||||
|
<BoolValueOverride>
|
||||||
|
<option name="enabled" value="true" />
|
||||||
|
</BoolValueOverride>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/commander.iml" filepath="$PROJECT_DIR$/.idea/commander.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
117
cache.go
Normal file
117
cache.go
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
package commander
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/vmihailenco/msgpack/v5"
|
||||||
|
"go.opentelemetry.io/otel"
|
||||||
|
"go.opentelemetry.io/otel/codes"
|
||||||
|
"go.opentelemetry.io/otel/trace"
|
||||||
|
)
|
||||||
|
|
||||||
|
const cacheTTL = 5 * time.Second
|
||||||
|
|
||||||
|
type Cache interface {
|
||||||
|
Set(key string, value []byte, ttl time.Duration) error
|
||||||
|
Get(key string) ([]byte, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type commandCache struct {
|
||||||
|
tracer trace.Tracer
|
||||||
|
cache Cache
|
||||||
|
ttl time.Duration
|
||||||
|
|
||||||
|
locks sync.Map
|
||||||
|
}
|
||||||
|
|
||||||
|
func newCommandCache(cache Cache, ttl time.Duration) *commandCache {
|
||||||
|
tracer := otel.Tracer(tracerName)
|
||||||
|
|
||||||
|
if ttl <= 0 {
|
||||||
|
ttl = cacheTTL
|
||||||
|
}
|
||||||
|
|
||||||
|
return &commandCache{
|
||||||
|
tracer: tracer,
|
||||||
|
cache: cache,
|
||||||
|
ttl: ttl,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *commandCache) CommandCacheGet(ctx context.Context, key string, commands ...Command) bool {
|
||||||
|
ctx, span := c.tracer.Start(ctx, "command_cache.get")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
|
defer c.locker(ctx, key)()
|
||||||
|
|
||||||
|
res, err := c.cache.Get(key)
|
||||||
|
if err != nil {
|
||||||
|
span.RecordError(fmt.Errorf("cache get: %w", err))
|
||||||
|
span.SetStatus(codes.Error, err.Error())
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(res) == 0 {
|
||||||
|
span.AddEvent("cache miss")
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
span.AddEvent("cache hit")
|
||||||
|
|
||||||
|
if err := c.unmarshal(res, commands); err != nil {
|
||||||
|
span.RecordError(fmt.Errorf("unmarshal commands: %w", err))
|
||||||
|
span.SetStatus(codes.Error, err.Error())
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *commandCache) CommandCacheStore(ctx context.Context, key string, commands ...Command) {
|
||||||
|
ctx, span := c.tracer.Start(ctx, "command_cache.store")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
|
defer c.locker(ctx, key)()
|
||||||
|
|
||||||
|
marshaled, err := c.marshal(commands)
|
||||||
|
if err != nil {
|
||||||
|
span.RecordError(fmt.Errorf("marshal commands: %w", err))
|
||||||
|
span.SetStatus(codes.Error, err.Error())
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.cache.Set(key, marshaled, c.ttl); err != nil {
|
||||||
|
span.RecordError(fmt.Errorf("set cache: %w", err))
|
||||||
|
span.SetStatus(codes.Error, err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *commandCache) locker(ctx context.Context, key string) (unlock func()) {
|
||||||
|
ctx, span := c.tracer.Start(ctx, "locker")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
|
for {
|
||||||
|
if _, loaded := c.locks.LoadOrStore(key, true); !loaded {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return func() {
|
||||||
|
c.locks.Delete(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *commandCache) marshal(commands []Command) ([]byte, error) {
|
||||||
|
return msgpack.Marshal(commands)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *commandCache) unmarshal(res []byte, commands []Command) error {
|
||||||
|
return msgpack.Unmarshal(res, &commands)
|
||||||
|
}
|
||||||
66
examples/basic/commands.go
Normal file
66
examples/basic/commands.go
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Entity struct {
|
||||||
|
Name string
|
||||||
|
Address string
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetName struct {
|
||||||
|
UID string
|
||||||
|
Sleep time.Duration
|
||||||
|
|
||||||
|
Result Entity
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *GetName) CorrelationID() string {
|
||||||
|
return b.UID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *GetName) Execute(context.Context) error {
|
||||||
|
b.Result = Entity{Name: "Bob"}
|
||||||
|
|
||||||
|
if b.Sleep > 0 {
|
||||||
|
time.Sleep(b.Sleep)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *GetName) Rollback(context.Context) error {
|
||||||
|
b.Result = Entity{}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetAddress struct {
|
||||||
|
Input *Entity
|
||||||
|
|
||||||
|
Result Entity
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *GetAddress) CorrelationID() string {
|
||||||
|
return b.Input.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *GetAddress) Execute(context.Context) error {
|
||||||
|
b.Result = *b.Input
|
||||||
|
if b.Result.Address != "" {
|
||||||
|
return errors.New("already set")
|
||||||
|
}
|
||||||
|
|
||||||
|
b.Result.Address = "London"
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *GetAddress) Rollback(context.Context) error {
|
||||||
|
b.Result = Entity{}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
25
examples/basic/go.mod
Normal file
25
examples/basic/go.mod
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
module git.derfenix.pro/fenix/commander/examples/basic
|
||||||
|
|
||||||
|
go 1.22
|
||||||
|
|
||||||
|
replace (
|
||||||
|
git.derfenix.pro/fenix/commander => ../../
|
||||||
|
git.derfenix.pro/fenix/commander/inmemorycache => ../../inmemorycache
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
git.derfenix.pro/fenix/commander v0.0.0-00010101000000-000000000000
|
||||||
|
git.derfenix.pro/fenix/commander/inmemorycache v0.0.0-00010101000000-000000000000
|
||||||
|
github.com/google/uuid v1.6.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/go-logr/logr v1.4.2 // indirect
|
||||||
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
|
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
|
||||||
|
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
|
||||||
|
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
||||||
|
go.opentelemetry.io/otel v1.28.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/metric v1.28.0 // indirect
|
||||||
|
go.opentelemetry.io/otel/trace v1.28.0 // indirect
|
||||||
|
)
|
||||||
29
examples/basic/go.sum
Normal file
29
examples/basic/go.sum
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
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/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||||
|
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-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
|
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||||
|
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/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
|
||||||
|
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
|
||||||
|
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/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/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
|
||||||
|
github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
|
||||||
|
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
|
||||||
|
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
|
||||||
|
go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo=
|
||||||
|
go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4=
|
||||||
|
go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q=
|
||||||
|
go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s=
|
||||||
|
go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g=
|
||||||
|
go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
25
examples/basic/service.go
Normal file
25
examples/basic/service.go
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"git.derfenix.pro/fenix/commander"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
cmd := commander.New(10)
|
||||||
|
|
||||||
|
c1 := GetName{UID: uuid.NewString()}
|
||||||
|
c2 := GetAddress{Input: &c1.Result}
|
||||||
|
commands := []commander.Command{&c1, &c2}
|
||||||
|
|
||||||
|
if err := cmd.Execute(ctx, "", commands...); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(c2.Result)
|
||||||
|
}
|
||||||
134
examples/basic/service_test.go
Normal file
134
examples/basic/service_test.go
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
|
||||||
|
"git.derfenix.pro/fenix/commander"
|
||||||
|
"git.derfenix.pro/fenix/commander/inmemorycache"
|
||||||
|
)
|
||||||
|
|
||||||
|
func BenchmarkServiceNoop(b *testing.B) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
cmd := commander.New(10)
|
||||||
|
|
||||||
|
c1 := GetName{UID: uuid.NewString()}
|
||||||
|
c2 := GetAddress{Input: &c1.Result}
|
||||||
|
commands := []commander.Command{&c1, &c2}
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
b.ReportAllocs()
|
||||||
|
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
if err := cmd.Execute(ctx, "", commands...); err != nil {
|
||||||
|
_ = err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkServiceWithDelay(b *testing.B) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
cmd := commander.New(10)
|
||||||
|
|
||||||
|
c1 := GetName{UID: uuid.NewString(), Sleep: time.Microsecond * 100}
|
||||||
|
c2 := GetAddress{Input: &c1.Result}
|
||||||
|
commands := []commander.Command{&c1, &c2}
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
b.ReportAllocs()
|
||||||
|
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
if err := cmd.Execute(ctx, "", commands...); err != nil {
|
||||||
|
_ = err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkServiceWithRollbackNoop(b *testing.B) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
cmd := commander.New(10)
|
||||||
|
|
||||||
|
c1 := GetName{UID: uuid.NewString()}
|
||||||
|
c2 := GetAddress{Input: &c1.Result}
|
||||||
|
commands := []commander.Command{&c1, &c2}
|
||||||
|
|
||||||
|
commands = append(commands, &GetAddress{Input: &c2.Result})
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
b.ReportAllocs()
|
||||||
|
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
if err := cmd.Execute(ctx, "", commands...); err != nil {
|
||||||
|
_ = err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkServiceWithRollbackWithDelay(b *testing.B) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
cmd := commander.New(10)
|
||||||
|
|
||||||
|
c1 := GetName{UID: uuid.NewString(), Sleep: time.Microsecond * 100}
|
||||||
|
c2 := GetAddress{Input: &c1.Result}
|
||||||
|
commands := []commander.Command{&c1, &c2}
|
||||||
|
|
||||||
|
commands = append(commands, &GetAddress{Input: &c2.Result})
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
b.ReportAllocs()
|
||||||
|
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
if err := cmd.Execute(ctx, "", commands...); err != nil {
|
||||||
|
_ = err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkServiceWithCacheNoop(b *testing.B) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
cmd := commander.New(10)
|
||||||
|
|
||||||
|
cmd = cmd.WithCache(inmemorycache.NewInMemoryCache())
|
||||||
|
|
||||||
|
c1 := GetName{UID: uuid.NewString()}
|
||||||
|
c2 := GetAddress{Input: &c1.Result}
|
||||||
|
commands := []commander.Command{&c1, &c2}
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
b.ReportAllocs()
|
||||||
|
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
if err := cmd.Execute(ctx, "", commands...); err != nil {
|
||||||
|
_ = err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkServiceWithCacheWithDelay(b *testing.B) {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
cmd := commander.New(10)
|
||||||
|
|
||||||
|
cmd = cmd.WithCache(inmemorycache.NewInMemoryCache())
|
||||||
|
|
||||||
|
c1 := GetName{UID: uuid.NewString(), Sleep: time.Microsecond * 100}
|
||||||
|
c2 := GetAddress{Input: &c1.Result}
|
||||||
|
commands := []commander.Command{&c1, &c2}
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
b.ReportAllocs()
|
||||||
|
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
if err := cmd.Execute(ctx, "", commands...); err != nil {
|
||||||
|
_ = err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
289
executor.go
Normal file
289
executor.go
Normal file
@@ -0,0 +1,289 @@
|
|||||||
|
package commander
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"go.opentelemetry.io/otel"
|
||||||
|
"go.opentelemetry.io/otel/attribute"
|
||||||
|
"go.opentelemetry.io/otel/codes"
|
||||||
|
"go.opentelemetry.io/otel/metric"
|
||||||
|
"go.opentelemetry.io/otel/trace"
|
||||||
|
)
|
||||||
|
|
||||||
|
const tracerName = "git.derfenix.pro/fenix/commander"
|
||||||
|
|
||||||
|
var ErrNoErrorChannel = errors.New("no error channel provided")
|
||||||
|
|
||||||
|
type Command interface {
|
||||||
|
Execute(ctx context.Context) error
|
||||||
|
Rollback(ctx context.Context) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type CorrelatedCommand interface {
|
||||||
|
Command
|
||||||
|
CorrelationID() string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Eventer interface {
|
||||||
|
EmitEvent(ctx context.Context) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type CommandCache interface {
|
||||||
|
CommandCacheGet(context.Context, string, ...Command) bool
|
||||||
|
CommandCacheStore(context.Context, string, ...Command)
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustNewMetrics() metrics {
|
||||||
|
m, err := newMetrics()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMetrics() (metrics, error) {
|
||||||
|
meter := otel.GetMeterProvider().Meter("executor")
|
||||||
|
|
||||||
|
commandsCount, err := meter.Int64Histogram("commands", metric.WithDescription("Count of executed commands (can be rolled back)"))
|
||||||
|
if err != nil {
|
||||||
|
return metrics{}, fmt.Errorf("commands count histogram: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
commandsRollbackCount, err := meter.Int64Histogram("commands.rollback", metric.WithDescription("Count of commands rolled back"))
|
||||||
|
if err != nil {
|
||||||
|
return metrics{}, fmt.Errorf("commands count histogram: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
commandsFailedCount, err := meter.Int64Histogram("commands.failed", metric.WithDescription("Count of failed commands"))
|
||||||
|
if err != nil {
|
||||||
|
return metrics{}, fmt.Errorf("commands failed count histogram: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
commandsRollbackFailedCount, err := meter.Int64Histogram("commands.rollback.failed", metric.WithDescription("Count of commands fail to roll back"))
|
||||||
|
if err != nil {
|
||||||
|
return metrics{}, fmt.Errorf("commands count histogram: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
monitorCommands, err := meter.Int64UpDownCounter("commands.running", metric.WithDescription("Command set in progress"))
|
||||||
|
if err != nil {
|
||||||
|
return metrics{}, fmt.Errorf("monitor commands counter: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cacheHit, err := meter.Int64Counter("cache.hit", metric.WithDescription("Cache hit"))
|
||||||
|
if err != nil {
|
||||||
|
return metrics{}, fmt.Errorf("cache hit: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cacheMiss, err := meter.Int64Counter("cache.miss", metric.WithDescription("Cache miss"))
|
||||||
|
if err != nil {
|
||||||
|
return metrics{}, fmt.Errorf("cache miss: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return metrics{
|
||||||
|
commandsCount: commandsCount,
|
||||||
|
commandsRollbackCount: commandsRollbackCount,
|
||||||
|
commandsFailedCount: commandsFailedCount,
|
||||||
|
commandsRollbackFailedCount: commandsRollbackFailedCount,
|
||||||
|
monitorCommands: monitorCommands,
|
||||||
|
cacheHit: cacheHit,
|
||||||
|
cacheMiss: cacheMiss,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type metrics struct {
|
||||||
|
commandsCount metric.Int64Histogram
|
||||||
|
commandsRollbackCount metric.Int64Histogram
|
||||||
|
commandsFailedCount metric.Int64Histogram
|
||||||
|
commandsRollbackFailedCount metric.Int64Histogram
|
||||||
|
|
||||||
|
monitorCommands metric.Int64UpDownCounter
|
||||||
|
|
||||||
|
cacheHit metric.Int64Counter
|
||||||
|
cacheMiss metric.Int64Counter
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(commandsLimit int) *Commander {
|
||||||
|
tracer := otel.Tracer(tracerName)
|
||||||
|
|
||||||
|
return &Commander{
|
||||||
|
metrics: mustNewMetrics(),
|
||||||
|
tracer: tracer,
|
||||||
|
|
||||||
|
correlationIDBuffer: sync.Pool{New: func() any { return bytes.NewBuffer(nil) }},
|
||||||
|
|
||||||
|
commandsSemaphore: NewSemaphore(commandsLimit),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Commander struct {
|
||||||
|
metrics
|
||||||
|
tracer trace.Tracer
|
||||||
|
cache CommandCache
|
||||||
|
|
||||||
|
correlationIDBuffer sync.Pool
|
||||||
|
|
||||||
|
commandsSemaphore Semaphore
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Commander) WithCache(cache Cache) *Commander {
|
||||||
|
c.cache = newCommandCache(cache, cacheTTL)
|
||||||
|
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Commander) WithCacheTTL(cache Cache, ttl time.Duration) *Commander {
|
||||||
|
c.cache = newCommandCache(cache, ttl)
|
||||||
|
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Commander) ExecuteAsync(ctx context.Context, errCh chan<- error, correlationID string, commands ...Command) {
|
||||||
|
ctx, span := c.tracer.Start(ctx, "executor.execute_async")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
|
if errCh == nil {
|
||||||
|
span.RecordError(ErrNoErrorChannel)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if errCh != nil {
|
||||||
|
close(errCh)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err := c.Execute(ctx, correlationID, commands...); err != nil {
|
||||||
|
if errCh != nil {
|
||||||
|
errCh <- err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Commander) Execute(ctx context.Context, correlationID string, commands ...Command) error {
|
||||||
|
ctx, span := c.tracer.Start(
|
||||||
|
ctx,
|
||||||
|
"executor.execute",
|
||||||
|
trace.WithAttributes(
|
||||||
|
attribute.Int("commands.count", len(commands)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
|
if c.cache != nil && correlationID == "" {
|
||||||
|
correlationID = c.tryGetCorrelationID(commands)
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldCache := c.cache != nil && correlationID != ""
|
||||||
|
|
||||||
|
c.commandsSemaphore.Acquire()
|
||||||
|
c.monitorCommands.Add(ctx, 1)
|
||||||
|
defer func() {
|
||||||
|
c.commandsSemaphore.Release()
|
||||||
|
c.monitorCommands.Add(ctx, -1)
|
||||||
|
}()
|
||||||
|
|
||||||
|
if shouldCache {
|
||||||
|
if c.cache.CommandCacheGet(ctx, correlationID, commands...) {
|
||||||
|
c.cacheHit.Add(ctx, 1)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
c.cacheMiss.Add(ctx, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var actionsCount int64
|
||||||
|
defer func() {
|
||||||
|
if actionsCount > 0 {
|
||||||
|
c.commandsCount.Record(ctx, actionsCount)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
eventEmitters := make([]Eventer, 0, len(commands))
|
||||||
|
|
||||||
|
for idx, command := range commands {
|
||||||
|
if err := command.Execute(ctx); err != nil {
|
||||||
|
c.commandsFailedCount.Record(ctx, 1)
|
||||||
|
|
||||||
|
span.RecordError(
|
||||||
|
err,
|
||||||
|
trace.WithAttributes(attribute.String("step", "execute")),
|
||||||
|
trace.WithAttributes(attribute.String("command", fmt.Sprintf("%T", command))),
|
||||||
|
)
|
||||||
|
span.SetStatus(codes.Error, "failed to execute command")
|
||||||
|
|
||||||
|
if idx > 0 {
|
||||||
|
c.Rollback(ctx, commands[:idx]...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("execute command %v: %w", command, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if eventer, ok := command.(Eventer); ok {
|
||||||
|
eventEmitters = append(eventEmitters, eventer)
|
||||||
|
}
|
||||||
|
|
||||||
|
actionsCount++
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, emitter := range eventEmitters {
|
||||||
|
if err := emitter.EmitEvent(ctx); err != nil {
|
||||||
|
span.RecordError(
|
||||||
|
err,
|
||||||
|
trace.WithAttributes(attribute.String("step", "send events")),
|
||||||
|
trace.WithAttributes(attribute.String("command", fmt.Sprintf("%T", emitter))),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if shouldCache {
|
||||||
|
c.cache.CommandCacheStore(ctx, correlationID, commands...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Commander) tryGetCorrelationID(commands []Command) string {
|
||||||
|
newCorrelationID := c.correlationIDBuffer.Get().(*bytes.Buffer)
|
||||||
|
defer func() {
|
||||||
|
newCorrelationID.Reset()
|
||||||
|
c.correlationIDBuffer.Put(newCorrelationID)
|
||||||
|
}()
|
||||||
|
|
||||||
|
for _, command := range commands {
|
||||||
|
correlationIDer, ok := command.(CorrelatedCommand)
|
||||||
|
if !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
newCorrelationID.WriteString(correlationIDer.CorrelationID())
|
||||||
|
}
|
||||||
|
|
||||||
|
return newCorrelationID.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Commander) Rollback(ctx context.Context, commands ...Command) {
|
||||||
|
ctx, span := c.tracer.Start(ctx, "executor.rollback")
|
||||||
|
defer span.End()
|
||||||
|
|
||||||
|
for _, command := range commands {
|
||||||
|
if err := command.Rollback(ctx); err != nil {
|
||||||
|
c.commandsRollbackFailedCount.Record(ctx, 1)
|
||||||
|
|
||||||
|
span.RecordError(
|
||||||
|
err,
|
||||||
|
trace.WithAttributes(attribute.String("step", "rollback")),
|
||||||
|
trace.WithAttributes(attribute.String("command", fmt.Sprintf("%T", command))),
|
||||||
|
)
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
c.commandsRollbackCount.Record(ctx, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
20
go.mod
Normal file
20
go.mod
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
module git.derfenix.pro/fenix/commander
|
||||||
|
|
||||||
|
go 1.22
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/stretchr/testify v1.9.0
|
||||||
|
github.com/vmihailenco/msgpack/v5 v5.4.1
|
||||||
|
go.opentelemetry.io/otel v1.28.0
|
||||||
|
go.opentelemetry.io/otel/metric v1.28.0
|
||||||
|
go.opentelemetry.io/otel/trace v1.28.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/go-logr/logr v1.4.2 // indirect
|
||||||
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
)
|
||||||
27
go.sum
Normal file
27
go.sum
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
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/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||||
|
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-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
|
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||||
|
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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
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/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
|
||||||
|
github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
|
||||||
|
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
|
||||||
|
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
|
||||||
|
go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo=
|
||||||
|
go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4=
|
||||||
|
go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q=
|
||||||
|
go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s=
|
||||||
|
go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g=
|
||||||
|
go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI=
|
||||||
|
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/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
5
inmemorycache/go.mod
Normal file
5
inmemorycache/go.mod
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
module git.derfenix.pro/fenix/commander/inmemorycache
|
||||||
|
|
||||||
|
go 1.22
|
||||||
|
|
||||||
|
require github.com/patrickmn/go-cache v2.1.0+incompatible
|
||||||
2
inmemorycache/go.sum
Normal file
2
inmemorycache/go.sum
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
|
||||||
|
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
|
||||||
30
inmemorycache/inmemorycache.go
Normal file
30
inmemorycache/inmemorycache.go
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
package inmemorycache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/patrickmn/go-cache"
|
||||||
|
)
|
||||||
|
|
||||||
|
type InMemoryCache struct {
|
||||||
|
cache *cache.Cache
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewInMemoryCache() *InMemoryCache {
|
||||||
|
return &InMemoryCache{cache: cache.New(time.Minute, 5*time.Minute)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *InMemoryCache) Set(key string, value []byte, ttl time.Duration) error {
|
||||||
|
i.cache.Set(key, value, ttl)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *InMemoryCache) Get(key string) ([]byte, error) {
|
||||||
|
res, ok := i.cache.Get(key)
|
||||||
|
if !ok {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.([]byte), nil
|
||||||
|
}
|
||||||
31
semaphore.go
Normal file
31
semaphore.go
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
package commander
|
||||||
|
|
||||||
|
func NewSemaphore(len int) Semaphore {
|
||||||
|
if len == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return make(Semaphore, len)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Semaphore chan struct{}
|
||||||
|
|
||||||
|
func (s Semaphore) Acquire() {
|
||||||
|
if s == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
s <- struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Semaphore) Release() {
|
||||||
|
if s == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
<-s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Semaphore) Close() {
|
||||||
|
close(s)
|
||||||
|
}
|
||||||
36
semaphore_test.go
Normal file
36
semaphore_test.go
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
package commander
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync/atomic"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewSemaphore(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
sem := NewSemaphore(2)
|
||||||
|
|
||||||
|
var locked atomic.Uint32
|
||||||
|
|
||||||
|
for i := 0; i < 3; i++ {
|
||||||
|
go func() {
|
||||||
|
sem.Acquire()
|
||||||
|
locked.Add(1)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(time.Microsecond * 10)
|
||||||
|
|
||||||
|
require.Equal(t, uint32(2), locked.Load())
|
||||||
|
|
||||||
|
sem.Release()
|
||||||
|
time.Sleep(time.Microsecond)
|
||||||
|
|
||||||
|
require.Equal(t, uint32(3), locked.Load())
|
||||||
|
|
||||||
|
sem.Release()
|
||||||
|
sem.Release()
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user