Skip to content

Commit

Permalink
Initial commit.
Browse files Browse the repository at this point in the history
  • Loading branch information
lesovsky committed Jun 23, 2020
0 parents commit 2d057f6
Show file tree
Hide file tree
Showing 11 changed files with 526 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
bin/*
tmp/*
.idea/*
34 changes: 34 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
APPNAME = noisia

COMMIT=$(shell git rev-parse --short HEAD)
BRANCH=$(shell git rev-parse --abbrev-ref HEAD)

LDFLAGS = -a -installsuffix cgo -ldflags "-X main.appName=${APPNAME} -X main.gitCommit=${COMMIT} -X main.gitBranch=${BRANCH}"

.PHONY: help clean lint test race build

.DEFAULT_GOAL := help

help: ## Display this help screen
@echo "Makefile available targets:"
@grep -h -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " * \033[36m%-15s\033[0m %s\n", $$1, $$2}'

clean: ## Clean
rm -f ./bin/${APPNAME}
rmdir ./bin

dep: ## Get the dependencies
go mod download

lint: ## Lint the source files
golangci-lint run --timeout 5m -E golint -e '(method|func) [a-zA-Z]+ should be [a-zA-Z]+'

test: dep ## Run unittests
go test -short -timeout 300s -p 1 ./...

race: dep ## Run data race detector
go test -race -short -timeout 300s -p 1 ./...

build: dep ## Build
mkdir -p ./bin
CGO_ENABLED=0 GOOS=linux GOARCH=${GOARCH} go build ${LDFLAGS} -o bin/${APPNAME} ./app/cmd
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Noisia
Harmful workload generator for PostgreSQL.
- idle transactions
- see built-in help for more runtime options.

**ATTENTION: Use only for testing purposes, don't execute against production, reckless usage might cause problems.**

---
#### TODO
- rollbacks
- waiting transactions
- deadlocks
- temporary files
17 changes: 17 additions & 0 deletions app/app.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package app

import (
"context"
"github.com/lesovsky/noisia/app/internal/log"
)

func Start(ctx context.Context, c *Config) error {
if c.IdleXact {
err := runIdleXactWorkload(ctx, c)
if err != nil {
log.Errorf("idle transactions workload failed: %s", err)
}
}

return nil
}
69 changes: 69 additions & 0 deletions app/cmd/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package main

import (
"context"
"fmt"
"github.com/lesovsky/noisia/app"
"github.com/lesovsky/noisia/app/internal/log"
"gopkg.in/alecthomas/kingpin.v2"
"os"
"os/signal"
"syscall"
)

var (
appName, gitCommit, gitBranch string
)

func main() {
var (
showVersion = kingpin.Flag("version", "show version and exit").Default().Bool()
logLevel = kingpin.Flag("log-level", "set log level: debug, info, warn, error").Default("warn").Envar("NOISIA_LOG_LEVEL").String()
postgresConninfo = kingpin.Flag("conninfo", "Postgres connection string (DSN or URL), must be specified explicitly").Default("").Envar("NOISIA_POSTGRES_CONNINFO").String()
jobs = kingpin.Flag("jobs", "Run workload with specified number of workers").Default("1").Envar("NOISIA_JOBS").Uint16()
idleXact = kingpin.Flag("idle-xact", "Run idle transactions workload").Default("false").Envar("NOISIA_IDLE_XACT").Bool()
idleXactNaptimeMin = kingpin.Flag("idle-xact.naptime-min", "Min transactions naptime, in seconds").Default("5").Envar("NOISIA_IDLE_XACT_NAPTIME_MIN").Int()
idleXactNaptimeMax = kingpin.Flag("idle-xact.naptime-max", "Max transactions naptime, in seconds").Default("20").Envar("NOISIA_IDLE_XACT_NAPTIME_MAX").Int()
)
kingpin.Parse()
log.SetLevel(*logLevel)

if *showVersion {
fmt.Printf("%s %s-%s\n", appName, gitCommit, gitBranch)
os.Exit(0)
}

config := &app.Config{
PostgresConninfo: *postgresConninfo,
Jobs: *jobs,
IdleXact: *idleXact,
IdleXactNaptimeMin: *idleXactNaptimeMin,
IdleXactNaptimeMax: *idleXactNaptimeMax,
}

if err := config.Validate(); err != nil {
log.Errorln(err)
os.Exit(1)
}

ctx, cancel := context.WithCancel(context.Background())

var doExit = make(chan error, 2)
go func() {
doExit <- listenSignals()
cancel()
}()

go func() {
doExit <- app.Start(ctx, config)
cancel()
}()

log.Warnf("shutdown: %s", <-doExit)
}

func listenSignals() error {
c := make(chan os.Signal, 1)
signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
return fmt.Errorf("got %s", <-c)
}
25 changes: 25 additions & 0 deletions app/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package app

import (
"errors"
)

type Config struct {
PostgresConninfo string
Jobs uint16 // max 65535
IdleXact bool
IdleXactNaptimeMin int
IdleXactNaptimeMax int
}

func (c *Config) Validate() error {
if c.PostgresConninfo == "" {
return errors.New("'conninfo' is not specified")
}

if c.IdleXactNaptimeMin < 1 || c.IdleXactNaptimeMin > c.IdleXactNaptimeMax {
return errors.New("wrong 'idle-xact.naptime-min' or 'idle-xact.naptime-max' specified")
}

return nil
}
25 changes: 25 additions & 0 deletions app/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package app

import (
"github.com/stretchr/testify/assert"
"testing"
)

func TestConfig_Validate(t *testing.T) {
testcases := []struct {
config Config
valid bool
}{
{config: Config{PostgresConninfo: "127.0.0.1", IdleXactNaptimeMin: 10, IdleXactNaptimeMax: 20}, valid: true},
{config: Config{}, valid: false},
{config: Config{PostgresConninfo: "127.0.0.1", IdleXactNaptimeMin: 0, IdleXactNaptimeMax: 10}, valid: false},
{config: Config{PostgresConninfo: "127.0.0.1", IdleXactNaptimeMin: 10, IdleXactNaptimeMax: 5}, valid: false},
}
for _, tc := range testcases {
if tc.valid {
assert.NoError(t, tc.config.Validate())
} else {
assert.Error(t, tc.config.Validate())
}
}
}
62 changes: 62 additions & 0 deletions app/idle_xacts.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package app

import (
"context"
"github.com/jackc/pgx/v4/pgxpool"
"github.com/lesovsky/noisia/app/internal/log"
"math/rand"
"time"
)

func runIdleXactWorkload(ctx context.Context, config *Config) error {
log.Infoln("Starting idle transactions workload")

pool, err := pgxpool.Connect(context.Background(), config.PostgresConninfo)
if err != nil {
return err
}
defer pool.Close()

// keep specified number of workers using channel - run new workers until there is any free slot
guard := make(chan struct{}, config.Jobs)
for {
select {
// run workers only when it's possible to write into channel (channel is limited by number of jobs)
case guard <- struct{}{}:
go func() {
naptime := time.Duration(rand.Intn(config.IdleXactNaptimeMax-config.IdleXactNaptimeMin)+config.IdleXactNaptimeMin) * time.Second

log.Infof("starting xact with naptime %s", naptime)
err := startSingleIdleXact(context.Background(), pool, naptime)
if err != nil {
log.Errorln(err)
}

// when worker finished, read from the channel to allow starting another workers
<-guard
}()
case <-ctx.Done():
log.Info("exit signaled, stop idle transaction workload")
return nil
}
}
}

func startSingleIdleXact(ctx context.Context, pool *pgxpool.Pool, naptime time.Duration) error {
tx, err := pool.Begin(ctx)
if err != nil {
return err
}

rows, err := tx.Query(ctx, "SELECT * FROM pg_stat_database")
if err != nil {
return err
}
rows.Close()
time.Sleep(naptime)

if err := tx.Commit(ctx); err != nil {
log.Warnln(err)
}
return nil
}
126 changes: 126 additions & 0 deletions app/internal/log/log.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package log

import (
"fmt"
"github.com/rs/zerolog"
"os"
)

// Logger is the global logger with predefined settings
var Logger = zerolog.New(os.Stdout).With().Timestamp().Logger()

// KV is a simple key-value store
type KV map[string]string

// SetLevel sets logging level
func SetLevel(level string) {
switch level {
case "debug":
zerolog.SetGlobalLevel(zerolog.DebugLevel)
case "info":
zerolog.SetGlobalLevel(zerolog.InfoLevel)
case "warn":
zerolog.SetGlobalLevel(zerolog.WarnLevel)
case "error":
zerolog.SetGlobalLevel(zerolog.ErrorLevel)
default:
zerolog.SetGlobalLevel(zerolog.InfoLevel)
}
}

func New() zerolog.Logger {
var logger = Logger
return logger
}

// SetApplication appends application name string to log messages
func SetApplication(app string) {
Logger = Logger.With().Str("service", app).Logger()
}

// Debug prints message with DEBUG severity
func Debug(msg string) {
Logger.Debug().Msg(msg)
}

// Debugf prints formatted message with DEBUG severity
func Debugf(format string, v ...interface{}) {
Logger.Debug().Msgf(format, v...)
}

// Debugln concatenates arguments and prints them with DEBUG severity
func Debugln(v ...interface{}) {
Logger.Debug().Msg(fmt.Sprint(v...))
}

// Info prints message with INFO severity
func Info(msg string) {
Logger.Info().Msg(msg)
}

// Infof prints formatted message with INFO severity
func Infof(format string, v ...interface{}) {
Logger.Info().Msgf(format, v...)
}

// Infoln concatenates arguments and prints them with INFO severity
func Infoln(v ...interface{}) {
Logger.Info().Msg(fmt.Sprint(v...))
}

// Warn prints message with WARNING severity
func Warn(msg string) {
Logger.Warn().Msg(msg)
}

// Warnf prints formatted message with WARNING severity
func Warnf(format string, v ...interface{}) {
Logger.Warn().Msgf(format, v...)
}

// Warnln concatenates arguments and prints them with WARNING severity
func Warnln(v ...interface{}) {
Logger.Warn().Msg(fmt.Sprint(v...))
}

// Error prints message with ERROR severity
func Error(msg string) {
Logger.Error().Msg(msg)
}

// Errorf prints formatted message with ERROR severity
func Errorf(format string, v ...interface{}) {
Logger.Error().Msgf(format, v...)
}

// Errorln concatenates arguments and prints them with ERROR severity
func Errorln(v ...interface{}) {
Logger.Error().Msg(fmt.Sprint(v...))
}

// KVError prints message with ERROR severity with attached KV map
func KVError(kv KV, msg string) {
log := Logger.Error()
for k, v := range kv {
log.Str(k, v)
}
log.Msg(msg)
}

// KVErrorf prints formatted message with ERROR severity with attached KV map
func KVErrorf(kv KV, format string, v ...interface{}) {
log := Logger.Error()
for k, v := range kv {
log.Str(k, v)
}
log.Msgf(format, v...)
}

// KVErrorln concatenates arguments and prints them with ERROR severity with attached KV map
func KVErrorln(kv KV, v ...interface{}) {
log := Logger.Error()
for k, v := range kv {
log.Str(k, v)
}
log.Msg(fmt.Sprint(v...))
}
12 changes: 12 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
module github.com/lesovsky/noisia

go 1.14

require (
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d // indirect
github.com/jackc/pgx/v4 v4.6.0
github.com/rs/zerolog v1.19.0
github.com/stretchr/testify v1.5.1
gopkg.in/alecthomas/kingpin.v2 v2.2.6
)
Loading

0 comments on commit 2d057f6

Please sign in to comment.