├── _config.yml
├── docs
├── images
│ ├── logo.png
│ ├── gopher-pool.png
│ ├── process-pool.png
│ ├── process-monitoring.gif
│ └── worker-monitoring.gif
└── README.md
├── .idea
├── vcs.xml
├── .gitignore
├── modules.xml
└── gowl.iml
├── go.mod
├── .github
├── scripts
│ └── coverage.sh
└── workflows
│ └── build.yml
├── .fossa.yml
├── .gitignore
├── status
├── worker
│ └── status.go
├── pool
│ └── status.go
└── process
│ └── status.go
├── .golangci.yml
├── Makefile
├── LICENSE
├── go.sum
├── map_test.go
├── map.go
├── README.md
├── pool_test.go
└── pool.go
/_config.yml:
--------------------------------------------------------------------------------
1 | theme: jekyll-theme-cayman
--------------------------------------------------------------------------------
/docs/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmdsefi/gowl/HEAD/docs/images/logo.png
--------------------------------------------------------------------------------
/docs/images/gopher-pool.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmdsefi/gowl/HEAD/docs/images/gopher-pool.png
--------------------------------------------------------------------------------
/docs/images/process-pool.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmdsefi/gowl/HEAD/docs/images/process-pool.png
--------------------------------------------------------------------------------
/docs/images/process-monitoring.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmdsefi/gowl/HEAD/docs/images/process-monitoring.gif
--------------------------------------------------------------------------------
/docs/images/worker-monitoring.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hmdsefi/gowl/HEAD/docs/images/worker-monitoring.gif
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 | # Editor-based HTTP Client requests
5 | /httpRequests/
6 | # Datasource local storage ignored files
7 | /dataSources/
8 | /dataSources.local.xml
9 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/hamed-yousefi/gowl
2 |
3 | go 1.17
4 |
5 | require github.com/stretchr/testify v1.7.0
6 |
7 | require (
8 | github.com/davecgh/go-spew v1.1.0 // indirect
9 | github.com/pmezard/go-difflib v1.0.0 // indirect
10 | gopkg.in/yaml.v3 v3.0.0-20220521103104-8f96da9f5d5e // indirect
11 | )
12 |
--------------------------------------------------------------------------------
/.github/scripts/coverage.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | set -e
4 | echo "" > coverage.out
5 |
6 | for d in $(go list ./... | grep -v vendor); do
7 | go test -race -coverprofile=profile.out -covermode=atomic $d
8 | if [ -f profile.out ]; then
9 | cat profile.out >> coverage.out
10 | rm profile.out
11 | fi
12 | done
--------------------------------------------------------------------------------
/.idea/gowl.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.fossa.yml:
--------------------------------------------------------------------------------
1 | # Generated by FOSSA CLI (https://github.com/fossas/fossa-cli)
2 | # Visit https://fossa.com to learn more
3 |
4 | version: 2
5 | cli:
6 | server: https://app.fossa.com
7 | fetcher: custom
8 | project: https://github.com/hamed-yousefi/gowl.git
9 | analyze:
10 | modules:
11 | - name: github.com/hamed-yousefi/gowl
12 | type: go
13 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Binaries for programs and plugins
2 | *.exe
3 | *.exe~
4 | *.dll
5 | *.so
6 | *.dylib
7 |
8 | # Test binary, built with `go test -c`
9 | *.test
10 |
11 | # Output of the go coverage tool, specifically when used with LiteIDE
12 | *.out
13 |
14 | # Dependency directories (remove the comment below to include it)
15 | # vendor/
16 | cmd
17 | app
18 |
--------------------------------------------------------------------------------
/status/worker/status.go:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright © 2019 Hamed Yousefi .
3 | */
4 |
5 | package worker
6 |
7 | const (
8 | // Waiting is a worker state when the worker is waiting to consume a process.
9 | Waiting Status = iota
10 | // Busy is a worker state when the worker consumed a process and running it.
11 | Busy
12 | )
13 |
14 | var (
15 | status2String = map[Status]string{
16 | Waiting: "Waiting",
17 | Busy: "Busy",
18 | }
19 | )
20 |
21 | type (
22 | // Status represents worker current state.
23 | Status int
24 | )
25 |
26 | // String returns string value of worker state.
27 | func (s Status) String() string {
28 | return status2String[s]
29 | }
30 |
--------------------------------------------------------------------------------
/status/pool/status.go:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright © 2019 Hamed Yousefi .
3 | */
4 |
5 | package pool
6 |
7 | const (
8 | // Created is a pool state after pool has been created and before it starts.
9 | Created Status = iota
10 | // Running is a pool state when the pool started by Start() function.
11 | Running
12 | // Closed is a pool state when the pool stopped by Close() function.
13 | Closed
14 | )
15 |
16 | var (
17 | status2string = map[Status]string{
18 | Created: "Created",
19 | Running: "Running",
20 | Closed: "Closed",
21 | }
22 | )
23 |
24 | type (
25 | // Status represents pool current state.
26 | Status int
27 | )
28 |
29 | // String returns string value of pool state.
30 | func (p Status) String() string {
31 | return status2string[p]
32 | }
33 |
--------------------------------------------------------------------------------
/.golangci.yml:
--------------------------------------------------------------------------------
1 | linters-settings:
2 | govet:
3 | check-shadowing: false
4 | gocyclo:
5 | min-complexity: 25
6 | goconst:
7 | min-len: 2
8 | min-occurrences: 2
9 | errcheck:
10 | check-type-assertions: true
11 | gocritic:
12 | disabled-checks:
13 | - ifElseChain
14 | nakedret:
15 | max-func-lines: 15
16 |
17 | run:
18 | skip-dirs:
19 | - mock
20 |
21 | linters:
22 | enable:
23 | - gocritic
24 | - stylecheck
25 | - goimports
26 | - gosec
27 | - unconvert
28 | - unparam
29 | - gochecknoinits
30 | - gosec
31 | - nakedret
32 | - whitespace
33 | - gosimple
34 | - bodyclose
35 | - dogsled
36 | - rowserrcheck
37 | disable:
38 | - maligned
39 | - lll
40 | - dupl
41 | - gochecknoglobals
42 |
--------------------------------------------------------------------------------
/status/process/status.go:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright © 2019 Hamed Yousefi .
3 | */
4 |
5 | package process
6 |
7 | const (
8 | // Waiting is a process state when the process is waiting to consume by a worker.
9 | Waiting Status = iota
10 | // Running is a process state when it consumed by a worker.
11 | Running
12 | // Succeeded is a process state when it has been ended without error.
13 | Succeeded
14 | // Failed is a process state when it has been ended with error.
15 | Failed
16 | // Killed is a process state when the process cancelled before running.
17 | Killed
18 | )
19 |
20 | var (
21 | status2String = map[Status]string{
22 | Waiting: "Waiting",
23 | Running: "Running",
24 | Succeeded: "Succeeded",
25 | Failed: "Failed",
26 | Killed: "Killed",
27 | }
28 | )
29 |
30 | type (
31 | // Status represents process current state.
32 | Status int
33 | )
34 |
35 | // String returns string value of process state.
36 | func (s Status) String() string {
37 | return status2String[s]
38 | }
39 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | GO = go
2 | M = $(shell printf "\033[34;1m>>\033[0m")
3 |
4 | # Check richgo does exist.
5 | ifeq (, $(shell which richgo))
6 | $(warning "could not find richgo in $(PATH), run: go get github.com/kyoh86/richgo")
7 | endif
8 |
9 | .PHONY: test sync codecov test-app
10 |
11 | .PHONY: default
12 | default: all
13 |
14 | .PHONY: all
15 | all: test
16 |
17 | .PHONY: test
18 | test: sync
19 | $(info running tests)
20 | richgo test -v ./...
21 |
22 | .PHONY: codecov
23 | codecov: sync
24 | $(info running tests coverage)
25 | sh build/script/coverage.sh
26 |
27 | .PHONY: sync
28 | sync:
29 | $(info downloading dependencies)
30 | go get -v ./...
31 |
32 | .PHONY: fmt
33 | fmt:
34 | $(info $(M) format code)
35 | @ret=0 && for d in $$($(GO) list -f '{{.Dir}}' ./... | grep -v /vendor/); do \
36 | $(GO) fmt $$d/*.go || ret=$$? ; \
37 | done ; exit $$ret
38 |
39 | .PHONY: lint
40 | lint: ## Run linters
41 | $(info $(M) running golangci linter)
42 | golangci-lint run --timeout 5m0s ./...
43 |
44 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Hamed Yousefi
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
3 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
4 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
5 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
6 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
7 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
8 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
9 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
10 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
11 | gopkg.in/yaml.v3 v3.0.0-20220521103104-8f96da9f5d5e h1:3i3ny04XV6HbZ2N1oIBw1UBYATHAOpo4tfTF83JM3Z0=
12 | gopkg.in/yaml.v3 v3.0.0-20220521103104-8f96da9f5d5e/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
13 |
--------------------------------------------------------------------------------
/map_test.go:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright © 2019 Hamed Yousefi .
3 | */
4 |
5 | package gowl
6 |
7 | import (
8 | "context"
9 | "testing"
10 | "time"
11 |
12 | "github.com/stretchr/testify/assert"
13 |
14 | "github.com/hamed-yousefi/gowl/status/worker"
15 | )
16 |
17 | // Test controlPanelMap put and get functions
18 | func TestControlPanelMap(t *testing.T) {
19 | cp := new(controlPanelMap)
20 | ctx, cancel := context.WithCancel(context.Background())
21 | pc := &processContext{
22 | ctx: ctx,
23 | cancel: cancel,
24 | }
25 | cp.put("p-11", pc)
26 |
27 | a := assert.New(t)
28 | a.Equal(pc, cp.get("p-11"))
29 | }
30 |
31 | // Test workerStatsMap put and get functions
32 | func TestWorkerStatsMap(t *testing.T) {
33 | ws := new(workerStatsMap)
34 | ws.put("w1", worker.Busy)
35 |
36 | a := assert.New(t)
37 | a.Equal(worker.Busy, ws.get("w1"))
38 | }
39 |
40 | // Test processStatusMap put and get functions
41 | func TestProcessStatusMap(t *testing.T) {
42 | ps := new(processStatusMap)
43 | p := ProcessStats{
44 | WorkerName: "w1",
45 | StartedAt: time.Now(),
46 | }
47 | ps.put("p-11", p)
48 |
49 | a := assert.New(t)
50 | a.Equal(p, ps.get("p-11"))
51 | }
52 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: build
2 | on: [push, pull_request]
3 | jobs:
4 | golangci:
5 | name: lint
6 | runs-on: ubuntu-latest
7 | steps:
8 | - uses: actions/setup-go@v3
9 | with:
10 | go-version: 1.19
11 | - uses: actions/checkout@v3
12 | - name: golangci-lint
13 | uses: golangci/golangci-lint-action@v3
14 | with:
15 | # Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version
16 | version: v1.50.1
17 |
18 | build:
19 | name: Go build
20 | runs-on: ubuntu-latest
21 | steps:
22 | - uses: actions/checkout@v3
23 | with:
24 | go-version: 1.19
25 | - name: Build
26 | run: |
27 | git clone --depth=1 https://github.com/${GITHUB_REPOSITORY}
28 | cd $(basename ${GITHUB_REPOSITORY})
29 | go build -v -race
30 |
31 | test:
32 | name: Go test
33 | runs-on: ubuntu-latest
34 | steps:
35 | - uses: actions/checkout@v3
36 | with:
37 | go-version: 1.19
38 | - name: go get & test
39 | run: |
40 | go get -v -t -d ./...
41 | go test -v ./...
42 |
43 | - name: Generate coverage report
44 | run: sh ./.github/scripts/coverage.sh
45 | shell: bash
46 |
47 | - name: Upload coverage to codecov
48 | uses: codecov/codecov-action@v3
49 | with:
50 | token: ${{ secrets.CODECOV_TOKEN }}
51 | files: ./coverage.out
52 | flags: unittests # optional
53 | name: codecov-umbrella # optional
54 | fail_ci_if_error: false # optional (default = false)
55 | verbose: true # optional (default = false)
56 |
--------------------------------------------------------------------------------
/map.go:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright © 2019 Hamed Yousefi .
3 | */
4 |
5 | package gowl
6 |
7 | import (
8 | "context"
9 | "sync"
10 |
11 | "github.com/hamed-yousefi/gowl/status/worker"
12 | )
13 |
14 | type (
15 | // controlPanelMap is a thread safe map for controlling processes. It also
16 | // provides type safety.
17 | // Key: PID
18 | // Value: processContext
19 | controlPanelMap struct {
20 | internal sync.Map
21 | }
22 |
23 | // workerStatsMap is a thread safe map for controlling processes. It also
24 | // provides type safety.
25 | // Key: WorkerName
26 | // Value: worker.Status
27 | workerStatsMap struct {
28 | internal sync.Map
29 | }
30 |
31 | // processStatusMap is a thread safe map for controlling processes. It also
32 | // provides type safety.
33 | // Key: PID
34 | // Value: ProcessStats
35 | processStatusMap struct {
36 | internal sync.Map
37 | }
38 |
39 | // processContext represents a cancellation context by holding a context and
40 | // a cancel function.
41 | processContext struct {
42 | ctx context.Context
43 | cancel context.CancelFunc
44 | }
45 | )
46 |
47 | func (c *controlPanelMap) put(pid PID, pc *processContext) {
48 | c.internal.Store(pid, pc)
49 | }
50 |
51 | func (c *controlPanelMap) get(pid PID) *processContext {
52 | in, _ := c.internal.Load(pid)
53 | cancel, _ := in.(*processContext)
54 | return cancel
55 | }
56 |
57 | func (c *workerStatsMap) put(name WorkerName, status worker.Status) {
58 | c.internal.Store(name, status)
59 | }
60 |
61 | func (c *workerStatsMap) get(name WorkerName) worker.Status {
62 | in, _ := c.internal.Load(name)
63 | status, _ := in.(worker.Status)
64 | return status
65 | }
66 |
67 | func (c *processStatusMap) put(pid PID, stats ProcessStats) {
68 | c.internal.Store(pid, stats)
69 | }
70 |
71 | func (c *processStatusMap) get(pid PID) ProcessStats {
72 | in, _ := c.internal.Load(pid)
73 | stats, _ := in.(ProcessStats)
74 | return stats
75 | }
76 |
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | # Gowl
2 | [](https://travis-ci.com/hamed-yousefi/gowl)
3 | [](https://codecov.io/gh/hamed-yousefi/gowl)
4 | [](https://goreportcard.com/report/github.com/hamed-yousefi/gowl)
5 | [](https://app.fossa.com/projects/custom%2B24403%2Fgithub.com%2Fhamed-yousefi%2Fgowl?ref=badge_shield)
6 | [](https://pkg.go.dev/github.com/hamed-yousefi/gowl)
7 | 
8 | Gowl is a process management and process monitoring tool at once.
9 | An infinite worker pool gives you the ability to control the pool and processes
10 | and monitor their status.
11 |
12 | ## Table of Contents
13 |
14 | * [Install](#Install)
15 | * [How to use](#How-to-use)
16 | * [Pool](#Pooling)
17 | * [Start](#Start)
18 | * [Register process](#Register-process)
19 | * [Kill process](#Kill-process)
20 | * [Close](#Close)
21 | * [Monitor](#Monitor)
22 | * [License](#License)
23 |
24 | ## Install
25 | Using Gowl is easy. First, use `go get` to install the latest version of the library.
26 | This command will install the `gowl` along with library and its dependencies:
27 | ```shell
28 | go get -u github.com/hamed-yousefi/gowl
29 | ```
30 | Next, include Gowl in your application:
31 | ```go
32 | import "github.com/hamed-yousefi/gowl"
33 | ```
34 |
35 | ## How to use
36 | Gowl has three main parts. Process, Pool, and Monitor. The process is the
37 | smallest part of this project. The process is the part of code that the
38 | developer must implement. To do that, Gowl provides an interface to inject
39 | outside code into the pool. The process interface is as follows:
40 |
41 | ```go
42 | Process interface {
43 | Start() error
44 | Name() string
45 | PID() PID
46 | }
47 | ```
48 |
49 | The process interface has three methods. The Start function contains the
50 | user codes, and the pool workers use this function to run the process.
51 | The Name function returns the process name, and the monitor uses this
52 | function to provide reports. The PID function returns process id. The
53 | process id is unique in the entire pool, and it will use by the pool and
54 | monitor.
55 |
56 | Let's take a look at an example:
57 | ```go
58 | Document struct {
59 | content string
60 | hash string
61 | }
62 |
63 | func (d *Document) Start() error {
64 | hasher := sha1.New()
65 | hasher.Write(bv)
66 | h.hash= base64.URLEncoding.EncodeToString(hasher.Sum(nil))
67 | }
68 |
69 | func (d *Document) Name() string {
70 | return "hashing-process"
71 | }
72 |
73 | func (d *Document) PID() PID {
74 | return "p-1"
75 | }
76 |
77 | func (d *Document) Hash() string {
78 | return h.hash
79 | }
80 | ```
81 |
82 | As you can see, in this example, `Document` implements the Process interface.
83 | So now we can register it into the pool.
84 |
85 | ### Pool
86 | Creating Gowl pool is very easy. You must use the `NewPool(size int)`
87 | function and pass the pool size to this function. Pool size indicates
88 | the worker numbers in and the underlying queue size that workers consume
89 | process from it. Look at the following example:
90 |
91 | ```go
92 | pool := gowl.NewPool(4)
93 | ```
94 | In this example, Gowl will create a new instance of a Pool object with four workers
95 | and an underlying queue with the size of four.
96 |
97 | #### Start
98 |
99 | To start the Gowl, you must call the `Start()` method of the pool
100 | object. It will begin to create the workers, and workers start listening
101 | to the queue to consume process.
102 |
103 | #### Register process
104 |
105 | To register processes to the pool, you must use the `Register(args ...process)`
106 | method. Pass the processes to the register method, and it will create a
107 | new publisher to publish the process list to the queue. You can call multiple
108 | times when Gowl pool is running.
109 |
110 | #### Kill process
111 |
112 | One of the most remarkable features of Gowl is the ability to control the
113 | process after registered it into the pool. You can kill a process before
114 | any worker runs it. Killing a process is simple, and you need the process
115 | id to do it.
116 |
117 | ```go
118 | pool.Kill(PID("p-909"))
119 | ```
120 |
121 | #### Close
122 |
123 | Gowl is an infinite worker pool. However, you should have control over
124 | the pool and decide when you want to start it, register a new process on
125 | it, kill a process, and `close` the pool and terminate the workers. Gowl
126 | gives you this option to close the pool by the `Close()` method of the
127 | Pool object.
128 |
129 | ## Monitor
130 |
131 | Every process management tool needs a monitoring system to expose the
132 | internal stats to the outside world. Gowl gives you a monitoring API
133 | to see processes and workers stats.
134 |
135 | You can get the Monitor instance by calling the `Monitor()` method of
136 | the Pool. The monitor object is as follows:
137 |
138 | ```go
139 | Monitor interface {
140 | PoolStatus() pool.Status
141 | Error(PID) error
142 | WorkerList() []WorkerName
143 | WorkerStatus(name WorkerName) worker.Status
144 | ProcessStats(pid PID) ProcessStats
145 | }
146 | ```
147 |
148 | The Monitor gives you this opportunity to get the Pool status, process
149 | error, worker list, worker status, and process stats. Wis Monitor API,
150 | you can create your monitoring app with ease. The following example is
151 | using Monitor API to present the stats in the console in real-time.
152 |
153 | 
154 |
155 | Also, you can use the Monitor API to show worker status in the console:
156 |
157 | 
158 |
159 | ## License
160 |
161 | MIT License, please see [LICENSE](https://github.com/hamed-yousefi/gowl/blob/master/LICENSE) for details.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Gowl
2 |
3 | 
4 | [](https://codecov.io/gh/hmdsefi/gowl)
5 | [](https://goreportcard.com/report/github.com/hamed-yousefi/gowl)
6 | [](https://app.fossa.com/projects/custom%2B24403%2Fgithub.com%2Fhamed-yousefi%2Fgowl?ref=badge_shield)
7 | [](https://github.com/avelino/awesome-go)
8 | [](https://pkg.go.dev/github.com/hamed-yousefi/gowl)
9 | 
10 | Gowl is a process management and process monitoring tool at once.
11 | An infinite worker pool gives you the ability to control the pool and processes
12 | and monitor their status.
13 |
14 | ## Table of Contents
15 |
16 | * [Install](#Install)
17 | * [How to use](#How-to-use)
18 | * [Pool](#Pooling)
19 | * [Start](#Start)
20 | * [Register process](#Register-process)
21 | * [Kill process](#Kill-process)
22 | * [Close](#Close)
23 | * [Monitor](#Monitor)
24 | * [License](#License)
25 |
26 | ## Install
27 |
28 | Using Gowl is easy. First, use `go get` to install the latest version of the library. This command will install
29 | the `gowl` along with library and its dependencies:
30 |
31 | ```shell
32 | go get -u github.com/hamed-yousefi/gowl
33 | ```
34 |
35 | Next, include Gowl in your application:
36 |
37 | ```go
38 | import "github.com/hamed-yousefi/gowl"
39 | ```
40 |
41 | ## How to use
42 |
43 | Gowl has three main parts. Process, Pool, and Monitor. The process is the smallest part of this project. The process is
44 | the part of code that the developer must implement. To do that, Gowl provides an interface to inject outside code into
45 | the pool. The process interface is as follows:
46 |
47 | ```go
48 | Process interface {
49 | Start(ctx context.Context) error
50 | Name() string
51 | PID() PID
52 | }
53 | ```
54 |
55 | The process interface has three methods. The Start function contains the user codes, and the pool workers use this
56 | function to run the process. The Name function returns the process name, and the monitor uses this function to provide
57 | reports. The PID function returns process id. The process id is unique in the entire pool, and it will use by the pool
58 | and monitor.
59 |
60 | If properly done it should respond to the context cancellation (if needed)
61 |
62 | Let's take a look at an example:
63 |
64 | ```go
65 | Document struct {
66 | content string
67 | hash string
68 | }
69 |
70 | func (d *Document) Start(ctx context.Context) error {
71 | hasher := sha1.New()
72 | hasher.Write(bv)
73 | h.hash = base64.URLEncoding.EncodeToString(hasher.Sum(nil))
74 | }
75 |
76 | func (d *Document) Name() string {
77 | return "hashing-process"
78 | }
79 |
80 | func (d *Document) PID() PID {
81 | return "p-1"
82 | }
83 |
84 | func (d *Document) Hash() string {
85 | return h.hash
86 | }
87 | ```
88 |
89 | As you can see, in this example, `Document` implements the Process interface. So now we can register it into the pool.
90 |
91 | ### Pool
92 |
93 | Creating Gowl pool is very easy. You must use the `NewPool(size int)`
94 | function and pass the pool size to this function. Pool size indicates the worker numbers in and the underlying queue
95 | size that workers consume process from it. Look at the following example:
96 |
97 | ```go
98 | pool := gowl.NewPool(4)
99 | ```
100 |
101 | In this example, Gowl will create a new instance of a Pool object with four workers and an underlying queue with the
102 | size of four.
103 |
104 | #### Start
105 |
106 | To start the Gowl, you must call the `Start()` method of the pool object. It will begin to create the workers, and
107 | workers start listening to the queue to consume process.
108 |
109 | #### Register process
110 |
111 | To register processes to the pool, you must use the `Register(args ...process)`
112 | method. Pass the processes to the register method, and it will create a new publisher to publish the process list to the
113 | queue. You can call multiple times when Gowl pool is running.
114 |
115 | #### Kill process
116 |
117 | One of the most remarkable features of Gowl is the ability to control the process after registered it into the pool. You
118 | can kill a process before any worker runs it, this also works after the job have started if it consideres the context
119 | cancellation. Killing a process is simple, and you need the process id to do it.
120 |
121 | ```go
122 | pool.Kill(PID("p-909"))
123 | ```
124 |
125 | #### Close
126 |
127 | Gowl is an infinite worker pool. However, you should have control over the pool and decide when you want to start it,
128 | register a new process on it, kill a process, and `close` the pool and terminate the workers. Gowl gives you this option
129 | to close the pool by the `Close()` method of the Pool object.
130 |
131 | ## Monitor
132 |
133 | Every process management tool needs a monitoring system to expose the internal stats to the outside world. Gowl gives
134 | you a monitoring API to see processes and workers stats.
135 |
136 | You can get the Monitor instance by calling the `Monitor()` method of the Pool. The monitor object is as follows:
137 |
138 | ```go
139 | Monitor interface {
140 | PoolStatus() pool.Status
141 | Error(PID) error
142 | WorkerList() []WorkerName
143 | WorkerStatus(name WorkerName) worker.Status
144 | ProcessStats(pid PID) ProcessStats
145 | }
146 | ```
147 |
148 | The Monitor gives you this opportunity to get the Pool status, process error, worker list, worker status, and process
149 | stats. Wis Monitor API, you can create your monitoring app with ease. The following example is using Monitor API to
150 | present the stats in the console in real-time.
151 |
152 | 
153 |
154 | Also, you can use the Monitor API to show worker status in the console:
155 |
156 | 
157 |
158 | ## License
159 |
160 | MIT License, please see [LICENSE](https://github.com/hamed-yousefi/gowl/blob/master/LICENSE) for details.
161 |
--------------------------------------------------------------------------------
/pool_test.go:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright © 2019 Hamed Yousefi .
3 | */
4 |
5 | package gowl
6 |
7 | import (
8 | "context"
9 | "errors"
10 | "fmt"
11 | "strconv"
12 | "testing"
13 | "time"
14 |
15 | "github.com/stretchr/testify/assert"
16 |
17 | "github.com/hamed-yousefi/gowl/status/pool"
18 | "github.com/hamed-yousefi/gowl/status/process"
19 | )
20 |
21 | type (
22 | pTestFunc func(ctx context.Context, pid PID, duration time.Duration) error
23 | mockProcess struct {
24 | name string
25 | pid PID
26 | sleepTime time.Duration
27 | pFunc pTestFunc
28 | }
29 | )
30 |
31 | func (t mockProcess) Start(ctx context.Context) error {
32 | return t.pFunc(ctx, t.pid, t.sleepTime)
33 | }
34 |
35 | func (t mockProcess) Name() string {
36 | return t.name
37 | }
38 |
39 | func (t mockProcess) PID() PID {
40 | return t.pid
41 | }
42 |
43 | func newTestProcess(name string, id int, duration time.Duration, f pTestFunc) Process {
44 | return mockProcess{
45 | name: name,
46 | pid: PID("p-" + strconv.Itoa(id)),
47 | sleepTime: duration,
48 | pFunc: f,
49 | }
50 | }
51 |
52 | var errCancelled = errors.New("task was cancelled")
53 |
54 | // Close pool before adding all processes to the queue
55 | func TestNewPool(t *testing.T) {
56 | a := assert.New(t)
57 | wp := NewPool(2)
58 |
59 | a.Equal(pool.Created, wp.Monitor().PoolStatus())
60 | wp.Register(createProcess(10, 1, 300*time.Millisecond, processFunc)...)
61 | err := wp.Start()
62 | a.NoError(err)
63 | a.Equal(pool.Running, wp.Monitor().PoolStatus())
64 | time.Sleep(500 * time.Millisecond)
65 | err = wp.Close()
66 | a.NoError(err)
67 | a.Equal(pool.Closed, wp.Monitor().PoolStatus())
68 | }
69 |
70 | // Four different goroutine will publish processes to the queue
71 | func TestNewPoolMultiPublisher(t *testing.T) {
72 | a := assert.New(t)
73 | wp := NewPool(2)
74 | a.Equal(pool.Created, wp.Monitor().PoolStatus())
75 | err := wp.Start()
76 | a.NoError(err)
77 | a.Equal(pool.Running, wp.Monitor().PoolStatus())
78 | wp.Register(createProcess(10, 1, 300*time.Millisecond, processFunc)...)
79 | wp.Register(createProcess(10, 2, 200*time.Millisecond, processFunc)...)
80 | wp.Register(createProcess(10, 3, 100*time.Millisecond, processFunc)...)
81 | wp.Register(createProcess(10, 4, 500*time.Millisecond, processFunc)...)
82 |
83 | time.Sleep(10 * time.Second)
84 | err = wp.Close()
85 | a.NoError(err)
86 | a.Equal(pool.Closed, wp.Monitor().PoolStatus())
87 | }
88 |
89 | // Kill a processFunc before it starts
90 | func TestWorkerPool_Kill(t *testing.T) {
91 | a := assert.New(t)
92 | wp := NewPool(5)
93 | a.Equal(pool.Created, wp.Monitor().PoolStatus())
94 | err := wp.Start()
95 | a.NoError(err)
96 | a.Equal(pool.Running, wp.Monitor().PoolStatus())
97 | wp.Register(createProcess(10, 1, 3*time.Second, processFunc)...)
98 | wp.Kill("p-18")
99 | time.Sleep(7 * time.Second)
100 | err = wp.Close()
101 | a.NoError(err)
102 | a.Equal(pool.Closed, wp.Monitor().PoolStatus())
103 | a.Equal(process.Killed, wp.Monitor().ProcessStats("p-18").Status)
104 | }
105 |
106 | // Kill a processFunc after it started
107 | func TestWorkerPoolStarted_Kill(t *testing.T) {
108 | a := assert.New(t)
109 | wp := NewPool(3)
110 | a.Equal(pool.Created, wp.Monitor().PoolStatus())
111 | err := wp.Start()
112 | a.NoError(err)
113 | a.Equal(pool.Running, wp.Monitor().PoolStatus())
114 | wp.Register(createProcess(3, 1, 3*time.Second, processFunc)...)
115 | time.Sleep(2 * time.Second)
116 | wp.Kill("p-12")
117 | err = wp.Close()
118 | a.NoError(err)
119 | a.Equal(pool.Closed, wp.Monitor().PoolStatus())
120 | a.Equal(process.Killed, wp.Monitor().ProcessStats("p-12").Status)
121 | a.Error(wp.Monitor().Error("p-12"))
122 | a.Equal("task was cancelled", wp.Monitor().Error("p-12").Error())
123 | }
124 |
125 | // Process returns error and monitor should cache it
126 | func TestMonitor_Error(t *testing.T) {
127 | a := assert.New(t)
128 | wp := NewPool(5)
129 | a.Equal(pool.Created, wp.Monitor().PoolStatus())
130 | err := wp.Start()
131 | a.NoError(err)
132 | a.Equal(pool.Running, wp.Monitor().PoolStatus())
133 | wp.Register(createProcess(1, 1, 1*time.Second, processFuncWithError)...)
134 | time.Sleep(2 * time.Second)
135 | err = wp.Close()
136 | a.NoError(err)
137 | a.Equal(pool.Closed, wp.Monitor().PoolStatus())
138 | a.Equal(process.Failed, wp.Monitor().ProcessStats("p-11").Status)
139 | a.Error(wp.Monitor().Error("p-11"))
140 | a.Equal("unable to start processFunc with id: p-11", wp.Monitor().Error("p-11").Error())
141 | }
142 |
143 | // Close a created pool should return error
144 | func TestWorkerPool_Close(t *testing.T) {
145 | a := assert.New(t)
146 | wp := NewPool(3)
147 | a.Equal(pool.Created, wp.Monitor().PoolStatus())
148 | err := wp.Close()
149 | a.Error(err)
150 | a.Equal("pool is not running, status "+wp.Monitor().PoolStatus().String(), err.Error())
151 | err = wp.Start()
152 | a.NoError(err)
153 | a.Equal(pool.Running, wp.Monitor().PoolStatus())
154 | wp.Register(createProcess(1, 1, 100*time.Millisecond, processFunc)...)
155 | time.Sleep(1 * time.Second)
156 | err = wp.Close()
157 | a.NoError(err)
158 | a.Equal(pool.Closed, wp.Monitor().PoolStatus())
159 | }
160 |
161 | // Get worker list and check their status
162 | func TestWorkerPool_WorkerList(t *testing.T) {
163 | a := assert.New(t)
164 | wp := NewPool(3)
165 | a.Equal(pool.Created, wp.Monitor().PoolStatus())
166 | err := wp.Close()
167 | a.Error(err)
168 | a.Equal("pool is not running, status "+wp.Monitor().PoolStatus().String(), err.Error())
169 | err = wp.Start()
170 | a.NoError(err)
171 | a.Equal(pool.Running, wp.Monitor().PoolStatus())
172 | wp.Register(createProcess(5, 1, 700*time.Millisecond, processFunc)...)
173 | time.Sleep(1 * time.Second)
174 | err = wp.Start()
175 | a.Error(err)
176 | a.Equal("unable to start the pool, status: "+pool.Running.String(), err.Error())
177 | wList := wp.Monitor().WorkerList()
178 | for _, wn := range wList {
179 | fmt.Println(wp.Monitor().WorkerStatus(wn))
180 | }
181 | err = wp.Close()
182 | a.NoError(err)
183 | a.Equal(pool.Closed, wp.Monitor().PoolStatus())
184 | }
185 |
186 | func createProcess(n int, g int, d time.Duration, f pTestFunc) []Process {
187 | pList := make([]Process, 0)
188 | for i := 1; i <= n; i++ {
189 | pList = append(pList, newTestProcess("p-"+strconv.Itoa(i), (g*10)+i, d, f))
190 | }
191 | return pList
192 | }
193 |
194 | func processFunc(ctx context.Context, pid PID, d time.Duration) error {
195 | fmt.Printf("process with id %v has been started.\n", pid)
196 | select {
197 | case <-time.After(d):
198 | case <-ctx.Done():
199 | return errCancelled
200 | }
201 | return nil
202 | }
203 |
204 | func processFuncWithError(ctx context.Context, pid PID, d time.Duration) error {
205 | return errors.New("unable to start processFunc with id: " + pid.String())
206 | }
207 |
--------------------------------------------------------------------------------
/pool.go:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright © 2019 Hamed Yousefi .
3 | */
4 |
5 | package gowl
6 |
7 | import (
8 | "context"
9 | "errors"
10 | "fmt"
11 | "log"
12 | "sync"
13 | "time"
14 |
15 | "github.com/hamed-yousefi/gowl/status/pool"
16 | "github.com/hamed-yousefi/gowl/status/process"
17 | "github.com/hamed-yousefi/gowl/status/worker"
18 | )
19 |
20 | const (
21 | // defaultWorkerName is the default worker name prefix.
22 | defaultWorkerName = "W%d"
23 | )
24 |
25 | type (
26 | // WorkerName is a custom type of string that represents worker's name.
27 | WorkerName string
28 |
29 | // PID is a custom type of string that represents process id.
30 | PID string
31 |
32 | // Process is an interface that represents a process
33 | Process interface {
34 | // Start runs the process. It returns an error object if any thing wrong
35 | // happens in runtime.
36 | Start(ctx context.Context) error
37 | // Name returns process name.
38 | Name() string
39 | // PID returns process id.
40 | PID() PID
41 | }
42 |
43 | // Pool is a mechanism to dispatch processes between a group of workers.
44 | Pool interface {
45 | // Start runs the pool.
46 | Start() error
47 | // Register adds the process to the pool queue.
48 | Register(p ...Process)
49 | // Close stops a running pool.
50 | Close() error
51 | // Kill cancels a process before it starts.
52 | Kill(pid PID)
53 | // Monitor returns pool monitor.
54 | Monitor() Monitor
55 | }
56 |
57 | // Monitor is a mechanism for observation processes and pool stats.
58 | Monitor interface {
59 | // PoolStatus returns pool status
60 | PoolStatus() pool.Status
61 | // Error returns process's error by process id.
62 | Error(PID) error
63 | // WorkerList returns the list of worker names of the pool.
64 | WorkerList() []WorkerName
65 | // WorkerStatus returns worker status. It accepts worker name as input.
66 | WorkerStatus(name WorkerName) worker.Status
67 | // ProcessStats returns process stats. It accepts process id as input.
68 | ProcessStats(pid PID) ProcessStats
69 | }
70 |
71 | // ProcessStats represents process statistics.
72 | ProcessStats struct {
73 | // WorkerName is the name of the worker that this process belongs to.
74 | WorkerName WorkerName
75 |
76 | // Process is process that this stats belongs to.
77 | Process Process
78 |
79 | // Status represents the current state of the process.
80 | Status process.Status
81 |
82 | // StartedAt represents the start date time of the process.
83 | StartedAt time.Time
84 |
85 | // FinishedAt represents the end date time of the process.
86 | FinishedAt time.Time
87 |
88 | err error
89 | }
90 |
91 | // workerPool is an implementation of Pool and Monitor interfaces.
92 | workerPool struct {
93 | status pool.Status
94 | size int
95 | queue chan Process
96 | wg *sync.WaitGroup
97 | processes *processStatusMap
98 | workers []WorkerName
99 | workersStats *workerStatsMap
100 | controlPanel *controlPanelMap
101 | mutex *sync.Mutex
102 | isClosed bool
103 | }
104 | )
105 |
106 | // NewPool makes a new instance of Pool. I accept an integer value as input
107 | // that represents pool size.
108 | func NewPool(size int) Pool {
109 | return &workerPool{
110 | status: pool.Created,
111 | size: size,
112 | queue: make(chan Process, size),
113 | workers: []WorkerName{},
114 | processes: new(processStatusMap),
115 | workersStats: new(workerStatsMap),
116 | controlPanel: new(controlPanelMap),
117 | mutex: new(sync.Mutex),
118 | wg: new(sync.WaitGroup),
119 | }
120 | }
121 |
122 | // Start runs the pool. It returns error if pool is already in running state.
123 | // It changes the pool state to Running and calls workerPool.run() function to
124 | // run the pool.
125 | func (w *workerPool) Start() error {
126 | if w.status == pool.Running {
127 | return errors.New("unable to start the pool, status: " + w.status.String())
128 | }
129 |
130 | w.status = pool.Running
131 | w.run()
132 |
133 | return nil
134 | }
135 |
136 | // run is the function that creates worker and starts the pool.
137 | func (w *workerPool) run() {
138 | // Create workers
139 | for i := 0; i < w.size; i++ {
140 | // For each worker add one to the waitGroup.
141 | w.wg.Add(1)
142 | wName := WorkerName(fmt.Sprintf(defaultWorkerName, i))
143 | w.workers = append(w.workers, wName)
144 |
145 | // Create worker.
146 | go func(wn WorkerName) {
147 | defer w.wg.Done()
148 |
149 | // Consume process from the queue.
150 | for p := range w.queue {
151 | w.workersStats.put(wn, worker.Busy)
152 | pStats := w.processes.get(p.PID())
153 | pStats.Status = process.Running
154 | pStats.StartedAt = time.Now()
155 | pStats.WorkerName = wn
156 | w.processes.put(p.PID(), pStats)
157 | wgp := new(sync.WaitGroup)
158 | wgp.Add(1)
159 |
160 | go func() {
161 | stats := w.processes.get(p.PID())
162 | defer func() {
163 | w.processes.put(p.PID(), stats)
164 | wgp.Done()
165 | }()
166 | pContext := w.controlPanel.get(p.PID())
167 | select {
168 | case <-pContext.ctx.Done():
169 | log.Printf("processFunc with id %s has been killed.\n", p.PID().String())
170 | stats.Status = process.Killed
171 | return
172 | default:
173 | if err := p.Start(pContext.ctx); err != nil { //nolint:typecheck
174 | stats.err = err
175 | stats.Status = process.Failed
176 | if errors.Is(pContext.ctx.Err(), context.Canceled) {
177 | stats.Status = process.Killed
178 | }
179 | } else {
180 | stats.Status = process.Succeeded
181 | }
182 | pContext.cancel()
183 | }
184 | }()
185 |
186 | wgp.Wait()
187 | pStats = w.processes.get(p.PID())
188 | pStats.FinishedAt = time.Now()
189 | w.processes.put(p.PID(), pStats)
190 | w.workersStats.put(wn, worker.Waiting)
191 | }
192 | }(wName)
193 | }
194 | }
195 |
196 | // Register adds the process to the pool queue. It accept a list of processes
197 | // and adds them to the queue. It publishes the process to queue in a separate
198 | // goroutine. It means that Register function provides multi-publisher that
199 | // each of them works asynchronously.
200 | func (w *workerPool) Register(args ...Process) {
201 | // Create control panel for each process and make process stat for each of them.
202 | for _, p := range args {
203 | ctx, cancel := context.WithCancel(context.Background())
204 | w.controlPanel.put(p.PID(), &processContext{
205 | ctx: ctx,
206 | cancel: cancel,
207 | })
208 | w.processes.put(p.PID(), ProcessStats{
209 | Process: p,
210 | Status: process.Waiting,
211 | })
212 | }
213 |
214 | // Publish processes to the queue.
215 | go func(args ...Process) {
216 | for i := range args {
217 | w.mutex.Lock()
218 | if w.isClosed {
219 | break
220 | }
221 | w.queue <- args[i]
222 | w.mutex.Unlock()
223 | }
224 | }(args...)
225 | }
226 |
227 | // Close stops a running pool. It returns an error if the pool is not running.
228 | // Close waits for all workers to finish their current job and then closes the
229 | // pool.
230 | func (w *workerPool) Close() error {
231 | if w.status != pool.Running {
232 | return errors.New("pool is not running, status " + w.status.String())
233 | }
234 |
235 | w.mutex.Lock()
236 | w.isClosed = true
237 | close(w.queue)
238 | w.mutex.Unlock()
239 |
240 | w.wg.Wait()
241 | w.status = pool.Closed
242 |
243 | return nil
244 | }
245 |
246 | // WorkerList returns the list of worker names of the pool.
247 | func (w *workerPool) WorkerList() []WorkerName {
248 | return w.workers
249 | }
250 |
251 | // Kill cancel a process before it starts.
252 | func (w *workerPool) Kill(pid PID) {
253 | w.controlPanel.get(pid).cancel()
254 | }
255 |
256 | // Monitor returns pool monitor.
257 | func (w *workerPool) Monitor() Monitor {
258 | return w
259 | }
260 |
261 | // String returns the string value of process id.
262 | func (p PID) String() string {
263 | return string(p)
264 | }
265 |
266 | // PoolStatus returns pool status
267 | func (w *workerPool) PoolStatus() pool.Status {
268 | return w.status
269 | }
270 |
271 | // Error returns process's error by process id.
272 | func (w *workerPool) Error(pid PID) error {
273 | return w.processes.get(pid).err
274 | }
275 |
276 | // WorkerStatus returns worker status. It accepts worker name as input.
277 | func (w *workerPool) WorkerStatus(name WorkerName) worker.Status {
278 | return w.workersStats.get(name)
279 | }
280 |
281 | // ProcessStats returns process stats. It accepts process id as input.
282 | func (w *workerPool) ProcessStats(pid PID) ProcessStats {
283 | return w.processes.get(pid)
284 | }
285 |
--------------------------------------------------------------------------------