Initial commit
This commit is contained in:
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
*~
|
||||
.fuse_hidden*
|
||||
.directory
|
||||
.Trash-*
|
||||
.nfs*
|
||||
.idea
|
||||
13
README.md
Normal file
13
README.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# ТЗ
|
||||
|
||||
Реализовать модель обработки данных в виде пайплайна, состоящего из следующих этапов
|
||||
1. Подача на вход пакетов данных. Пакет данных = слайс случайных целых чисел из 10 элементов. Новый пакет подается каждые N мс (N задается в виде env переменной)
|
||||
2. Обработка пакетов: нахождение 3-х наибольших чисел в пакете. Вход: слайс int из 10 элементов, выход: слайс из 3-х элементов. Обработка пакетов должна производиться M воркерами (M задается в виде env переменной)
|
||||
3. Аккумулятор: суммирование чисел обработанных пакетов, полученных на предыдущем этапе, и запись в единую переменную int
|
||||
4. Публикатор: вывод на консоль текущего значения аккумулятора каждые K секунд (K задается в виде env переменной)
|
||||
|
||||
Пример:
|
||||
вход: {1, 9, 6, 4, 4, 5, 7, 8, 0, 1}
|
||||
обработка: {9, 7, 8}
|
||||
аккумулятор: 9+7+8=24
|
||||
публикатор: 24
|
||||
40
accumulator/accumulator.go
Normal file
40
accumulator/accumulator.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package accumulator
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
func NewAccumulator(in <-chan [3]int) *Accumulator {
|
||||
return &Accumulator{in: in}
|
||||
}
|
||||
|
||||
type Accumulator struct {
|
||||
in <-chan [3]int
|
||||
summ atomic.Int64
|
||||
}
|
||||
|
||||
func (a *Accumulator) Start(ctx context.Context, wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
|
||||
case m, ok := <-a.in:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
for _, value := range m {
|
||||
a.summ.Add(int64(value))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Accumulator) Res() int64 {
|
||||
return a.summ.Load()
|
||||
}
|
||||
58
adapters/randomsource/service.go
Normal file
58
adapters/randomsource/service.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package randomsource
|
||||
|
||||
import (
|
||||
"context"
|
||||
crand "crypto/rand"
|
||||
"io"
|
||||
"math/big"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
func NewService(ch chan [10]int, interval time.Duration, maxInt int64) *Service {
|
||||
return &Service{
|
||||
interval: interval,
|
||||
ch: ch,
|
||||
rand: crand.Reader,
|
||||
maxInt: maxInt,
|
||||
}
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
interval time.Duration
|
||||
rand io.Reader
|
||||
ch chan [10]int
|
||||
maxInt int64
|
||||
}
|
||||
|
||||
func (s *Service) SourceCh() <-chan [10]int {
|
||||
return s.ch
|
||||
}
|
||||
|
||||
func (s *Service) Start(ctx context.Context, wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
|
||||
var res [10]int
|
||||
|
||||
ticker := time.NewTicker(s.interval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
|
||||
case <-ticker.C:
|
||||
for i := 0; i < 10; i++ {
|
||||
v, err := crand.Int(s.rand, big.NewInt(s.maxInt))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
res[i] = int(v.Int64())
|
||||
}
|
||||
|
||||
s.ch <- res
|
||||
}
|
||||
}
|
||||
}
|
||||
26
adapters/stdoutpub/service.go
Normal file
26
adapters/stdoutpub/service.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package stdoutpub
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
const outPrefix = "Result: "
|
||||
|
||||
func NewService() Service {
|
||||
return Service{
|
||||
fd: os.Stdout,
|
||||
}
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
fd *os.File
|
||||
}
|
||||
|
||||
func (s Service) Publish(i int64) error {
|
||||
if _, err := fmt.Fprintf(s.fd, "%s%d\n", outPrefix, i); err != nil {
|
||||
return fmt.Errorf("write to file: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
93
application/application.go
Normal file
93
application/application.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"marketlab/accumulator"
|
||||
"marketlab/adapters/randomsource"
|
||||
"marketlab/adapters/stdoutpub"
|
||||
"marketlab/worker"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type Source interface {
|
||||
Start(ctx context.Context, wg *sync.WaitGroup)
|
||||
SourceCh() <-chan [10]int
|
||||
}
|
||||
|
||||
type Publisher interface {
|
||||
Publish(int64) error
|
||||
}
|
||||
|
||||
func NewApplication(ctx context.Context) (Application, error) {
|
||||
cfg, err := NewConfig(ctx)
|
||||
if err != nil {
|
||||
return Application{}, fmt.Errorf("create config: %w", err)
|
||||
}
|
||||
|
||||
logger, err := zap.NewDevelopment()
|
||||
if err != nil {
|
||||
return Application{}, fmt.Errorf("create logger: %w", err)
|
||||
}
|
||||
|
||||
in := make(chan [10]int, cfg.WorkersCount)
|
||||
|
||||
pool := worker.NewPool(in, cfg.WorkersCount, logger.Named("pool"))
|
||||
|
||||
acc := accumulator.NewAccumulator(pool.Out())
|
||||
|
||||
src := randomsource.NewService(in, cfg.PacketInputInterval, 100)
|
||||
|
||||
pub := stdoutpub.NewService()
|
||||
|
||||
return Application{
|
||||
Logger: logger,
|
||||
Cfg: cfg,
|
||||
Source: src,
|
||||
Pool: pool,
|
||||
Accumulator: acc,
|
||||
Publisher: pub,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type Application struct {
|
||||
Logger *zap.Logger
|
||||
Cfg Config
|
||||
Source Source
|
||||
Pool *worker.Pool
|
||||
Accumulator *accumulator.Accumulator
|
||||
Publisher Publisher
|
||||
}
|
||||
|
||||
func (a *Application) Start(ctx context.Context, wg *sync.WaitGroup) error {
|
||||
wg.Add(4)
|
||||
|
||||
go a.Source.Start(ctx, wg)
|
||||
go a.Accumulator.Start(ctx, wg)
|
||||
go a.Pool.Start(ctx, wg)
|
||||
|
||||
go func() {
|
||||
wg.Done()
|
||||
|
||||
ticker := time.NewTicker(a.Cfg.OutputInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
|
||||
case <-ticker.C:
|
||||
if err := a.Publisher.Publish(a.Accumulator.Res()); err != nil {
|
||||
a.Logger.Error("failed to publish result", zap.Error(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
25
application/config.go
Normal file
25
application/config.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/sethvargo/go-envconfig"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
PacketInputInterval time.Duration `env:"PACKET_INPUT_INTERVAL,default=500ms"`
|
||||
WorkersCount uint `env:"WORKERS_COUNT,default=10"`
|
||||
OutputInterval time.Duration `env:"OUTPUT_INTERVAL,default=1s"`
|
||||
}
|
||||
|
||||
func NewConfig(ctx context.Context) (Config, error) {
|
||||
var cfg Config
|
||||
|
||||
if err := envconfig.Process(ctx, &cfg); err != nil {
|
||||
return Config{}, fmt.Errorf("failed to process env vars: %w", err)
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
30
cmd/service/main.go
Normal file
30
cmd/service/main.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os/signal"
|
||||
"sync"
|
||||
"syscall"
|
||||
|
||||
"marketlab/application"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func main() {
|
||||
ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
||||
defer cancel()
|
||||
|
||||
app, err := application.NewApplication(ctx)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
wg := &sync.WaitGroup{}
|
||||
if err := app.Start(ctx, wg); err != nil {
|
||||
app.Logger.Fatal("startup", zap.Error(err))
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
}
|
||||
10
go.mod
Normal file
10
go.mod
Normal file
@@ -0,0 +1,10 @@
|
||||
module marketlab
|
||||
|
||||
go 1.22
|
||||
|
||||
require (
|
||||
github.com/sethvargo/go-envconfig v1.0.3
|
||||
go.uber.org/zap v1.27.0
|
||||
)
|
||||
|
||||
require go.uber.org/multierr v1.10.0 // indirect
|
||||
18
go.sum
Normal file
18
go.sum
Normal file
@@ -0,0 +1,18 @@
|
||||
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/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/sethvargo/go-envconfig v1.0.3 h1:ZDxFGT1M7RPX0wgDOCdZMidrEB+NrayYr6fL0/+pk4I=
|
||||
github.com/sethvargo/go-envconfig v1.0.3/go.mod h1:JLd0KFWQYzyENqnEPWWZ49i4vzZo/6nRidxI8YvGiHw=
|
||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
|
||||
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
42
worker/pool.go
Normal file
42
worker/pool.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package worker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func NewPool(in <-chan [10]int, workersNum uint, log *zap.Logger) *Pool {
|
||||
out := make(chan [3]int, workersNum)
|
||||
|
||||
return &Pool{
|
||||
in: in,
|
||||
out: out,
|
||||
workersNum: int(workersNum),
|
||||
log: log,
|
||||
}
|
||||
}
|
||||
|
||||
type Pool struct {
|
||||
in <-chan [10]int
|
||||
out chan [3]int
|
||||
|
||||
workersNum int
|
||||
log *zap.Logger
|
||||
}
|
||||
|
||||
func (p *Pool) Start(ctx context.Context, wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
|
||||
wg.Add(p.workersNum)
|
||||
|
||||
for i := 0; i < p.workersNum; i++ {
|
||||
go NewWorker(p.in, p.out, p.log.Named(fmt.Sprintf("worker_%d", i))).Start(ctx, wg)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Pool) Out() <-chan [3]int {
|
||||
return p.out
|
||||
}
|
||||
51
worker/worker.go
Normal file
51
worker/worker.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package worker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
"sync"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func NewWorker(in <-chan [10]int, out chan<- [3]int, log *zap.Logger) *Worker {
|
||||
return &Worker{
|
||||
in: in,
|
||||
out: out,
|
||||
log: log,
|
||||
}
|
||||
}
|
||||
|
||||
type Worker struct {
|
||||
in <-chan [10]int
|
||||
out chan<- [3]int
|
||||
log *zap.Logger
|
||||
}
|
||||
|
||||
func (w *Worker) Start(ctx context.Context, wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
|
||||
case m, ok := <-w.in:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
w.out <- w.findMax(m)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (w *Worker) findMax(input [10]int) [3]int {
|
||||
sort.Ints(input[:])
|
||||
|
||||
res := [3]int(input[7:])
|
||||
|
||||
w.log.Debug("new data", zap.Ints("input", input[:]), zap.Ints("output", res[:]))
|
||||
|
||||
return res
|
||||
}
|
||||
43
worker/worker_test.go
Normal file
43
worker/worker_test.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package worker
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strconv"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestWorker_findMax(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
in [10]int
|
||||
out [3]int
|
||||
}{
|
||||
{
|
||||
in: [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10},
|
||||
out: [3]int{8, 9, 10},
|
||||
},
|
||||
{
|
||||
in: [10]int{10, 9, 8, 7, 6, 5, 4, 3, 2, 1},
|
||||
out: [3]int{8, 9, 10},
|
||||
},
|
||||
{
|
||||
in: [10]int{9, 7, 6, 10, 5, 4, 3, 8, 1},
|
||||
out: [3]int{8, 9, 10},
|
||||
},
|
||||
}
|
||||
|
||||
for idx, tt := range tests {
|
||||
tt := tt
|
||||
|
||||
t.Run(strconv.Itoa(idx), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
w := &Worker{}
|
||||
|
||||
if got := w.findMax(tt.in); !reflect.DeepEqual(got, tt.out) {
|
||||
t.Errorf("findMax() = %v, want %v", got, tt.out)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user