diff --git a/README.md b/README.md index d93b74f..4bb4056 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,2 @@ -# loadrunner +# loadsprinter A Load Test tool written in Go diff --git a/src/itix.fr/loadsprinter/cmd/main.go b/src/itix.fr/loadsprinter/cmd/main.go new file mode 100644 index 0000000..00c173f --- /dev/null +++ b/src/itix.fr/loadsprinter/cmd/main.go @@ -0,0 +1,48 @@ +package main + +import "fmt" +import "os" +import "time" +import "itix.fr/loadsprinter/core" +import "itix.fr/loadsprinter/steps" + +type MyVirtualUserFactory struct { + count int + scenario *core.Scenario +} + +func (factory *MyVirtualUserFactory) CreateVirtualUser() *core.VirtualUser { + name := fmt.Sprintf("vu-%03d", factory.count) + factory.count++ + return core.NewVirtualUser(name, factory.scenario) +} + +func check(e error) { + if e != nil { + panic(e) + } +} + +func main() { + /*vulogwr, err := os.Create("/tmp/vu.log") + check(err) + defer vulogwr.Close()*/ + csvwr, err := os.Create("/tmp/vu.csv") + check(err) + defer csvwr.Close() + + s1 := steps.NewWaitStep(300 * time.Millisecond) + s2 := steps.NewLogStep("Hello World !") + s3 := steps.NewFailStep("BLAAAAAAH") + step1 := core.NewStep("wait_3s", true, s1) + step2 := core.NewStep("log_hello", true, s2) + step3 := core.NewStep("fail", false, s3) + steps := []core.Step{ *step1, *step2, *step3 } + scenario := core.NewScenario(steps, "test") + + f := &MyVirtualUserFactory{0, scenario} + + c := core.NewController(os.Stdout, os.Stdout, csvwr) + wg, err := c.StartWith(5, f) + wg.Wait() +} diff --git a/src/itix.fr/loadsprinter/cmd/test.go b/src/itix.fr/loadsprinter/cmd/test.go new file mode 100644 index 0000000..6162892 --- /dev/null +++ b/src/itix.fr/loadsprinter/cmd/test.go @@ -0,0 +1,59 @@ +package main + +import "fmt" +import "time" +//import "sync" + +type Thing struct { + a int + b string +} + +func (t Thing) String() string { + return fmt.Sprintf("%v-%v", t.b, t.a) +} + +func Do(a int, b string, ch chan Thing) { + var t Thing + t.a = a + t.b = b + ch <- t +} + +func Doit(id string, chan1 chan Thing, chan2 chan Thing) { + for { + var t Thing + t.a = 666 + t.b = fmt.Sprintf("%v-evil", id) + + Do(1, fmt.Sprintf("%v-one", id), chan1) + time.Sleep(300 * time.Millisecond) + Do(2, fmt.Sprintf("%v-two", id), chan1) + Do(3, fmt.Sprintf("%v-three", id), chan1) + time.Sleep(50 * time.Millisecond) + Do(4, fmt.Sprintf("%v-four", id), chan1) + + chan2 <- t + } +} + +func main() { + chan1 := make(chan Thing) + chan2 := make(chan Thing) + + go Doit("A", chan1, chan2); + go Doit("B", chan1, chan2); + go Doit("C", chan1, chan2); + go Doit("D", chan1, chan2); + go Doit("E", chan1, chan2); + + for { + time.Sleep(1*time.Second) + select { + case x := <- chan1: + fmt.Printf("1: %v\n", x) + case x := <- chan2: + fmt.Printf("2: %v\n", x) + } + } +} diff --git a/src/itix.fr/loadsprinter/core/Controller.go b/src/itix.fr/loadsprinter/core/Controller.go new file mode 100644 index 0000000..97c1196 --- /dev/null +++ b/src/itix.fr/loadsprinter/core/Controller.go @@ -0,0 +1,113 @@ +package core + +import "io" +import "log" +import "fmt" +import "sync" +import "time" +import "encoding/csv" + +type Controller struct { + vuserLog *log.Logger + controllerLog *log.Logger + users []*VirtualUser + wg sync.WaitGroup + csv *csv.Writer + stepStats chan StepIteration + scenarioStats chan ScenarioIteration +} + +type VirtualUserFactory interface { + CreateVirtualUser() *VirtualUser +} + +type StepIteration struct { + vuser string + scenario string + step string + success bool + elapsed time.Duration +} + +func (s StepIteration) String() string { + return fmt.Sprintf("step: %v > %v > %v: success = %v, elapsed = %v", s.vuser, s.scenario, s.step, s.success, s.elapsed) +} + +type ScenarioIteration struct { + vuser string + scenario string + success bool + elapsed time.Duration +} + +func (s ScenarioIteration) String() string { + return fmt.Sprintf("scenario: %v > %v: success = %v, elapsed = %v", s.vuser, s.scenario, s.success, s.elapsed) +} + +func NewController(vuserLogFile io.Writer, controllerLogFile io.Writer, csvfile io.Writer) *Controller { + c := Controller{} + c.vuserLog = log.New(vuserLogFile, "", log.Lshortfile | log.LstdFlags) + c.controllerLog = log.New(controllerLogFile, "", log.Lshortfile | log.LstdFlags) + c.csv = csv.NewWriter(csvfile) + c.stepStats = make(chan StepIteration) + c.scenarioStats = make(chan ScenarioIteration) + return &c; +} + +func (c *Controller) StartWith(n int, f VirtualUserFactory) (wg *sync.WaitGroup, err error) { + c.controllerLog.Printf("--> Controller::StartWith(%v, %v)", n, f) + c.wg.Add(n) // Initialize the WaitGroup with the number of routines to create + + // Create a channel that will be used to start all VirtualUsers together + start := make(chan int) + + for i := 0; i < n; i++ { + log := *c.vuserLog + vu := f.CreateVirtualUser() + log.SetPrefix(fmt.Sprintf("%v: ", vu.name)) + c.users = append(c.users, vu) + vu.Init(&log) + go vu.Run(&log, &c.wg, start, c.stepStats, c.scenarioStats) + c.controllerLog.Printf("Added one more VirtualUser with name = %v", vu.name) + } + + // Start collecting results as soon as virtual users are started + go func() { <- start; c.GatherResults() }() + + c.controllerLog.Println("Ready ? Set !") + time.Sleep(1 * time.Second) + // Start all VirtualUsers together + close(start) + c.controllerLog.Println("Go !") + + c.controllerLog.Println("<-- Controller::StartWith()") + return &c.wg, nil +} + +func (c *Controller) GatherResults() { + c.controllerLog.Println("--> Controller::GatherResults()") + + // Gather results every second + timer := make(chan int) + go func() { + for { + time.Sleep(1 * time.Second) + timer <- 0 + } + }() + + for { + c.controllerLog.Println("Waiting for message") + select { + case <- timer: + c.controllerLog.Println("Time's up !") + case stepStat := <- c.stepStats: + c.controllerLog.Println(stepStat) + case scenarioStat := <- c.scenarioStats: + c.controllerLog.Println(scenarioStat) + } + c.controllerLog.Println("Received a message") + } + + c.controllerLog.Println("<-- Controller::GatherResults()") +} diff --git a/src/itix.fr/loadsprinter/core/Scenario.go b/src/itix.fr/loadsprinter/core/Scenario.go new file mode 100644 index 0000000..fdd6d5e --- /dev/null +++ b/src/itix.fr/loadsprinter/core/Scenario.go @@ -0,0 +1,51 @@ +package core + +import "log" +import "time" + +type Scenario struct { + steps []Step + name string +} + +func NewScenario(steps []Step, name string) *Scenario{ + return &Scenario{ steps: steps, name: name }; +} + +func (s *Scenario) Do(log *log.Logger, vuser *VirtualUser, stepsToController chan<- StepIteration, scenarioToController chan<- ScenarioIteration) error { + log.Println("--> Scenario::Do()") + + // Prepare the statistics structure + var stat ScenarioIteration + stat.success = true; + stat.vuser = vuser.name + stat.scenario = s.name + + // Measure the beginning of the scenario + start := time.Now() + + // Run the scenario + var err error = nil + for _, step := range s.steps { + err = step.Do(log, vuser, s, stepsToController) + if (err != nil) { + if (step.required) { + log.Printf("Mandatory step %v ended with error %v, stopping scenario !", step.name, err) + stat.success = false; + break + } else { + log.Printf("Optional step %v ended with error %v, continuing...", step.name, err) + } + } + } + + // Compute the elapsed time since the beginning of this scenario + elapsed := time.Since(start) + stat.elapsed = elapsed + + // Send it to the controller + scenarioToController <- stat + + log.Printf("<-- Scenario::Do() : err = %v", err) + return err +} diff --git a/src/itix.fr/loadsprinter/core/Step.go b/src/itix.fr/loadsprinter/core/Step.go new file mode 100644 index 0000000..135e003 --- /dev/null +++ b/src/itix.fr/loadsprinter/core/Step.go @@ -0,0 +1,48 @@ +package core + +import "log" +import "time" + +type StepImpl interface { + Do(log *log.Logger) error +} + +type Step struct { + name string + required bool + impl StepImpl +} + +func NewStep(name string, required bool, impl StepImpl) *Step { + return &Step{ name: name, required: required, impl: impl } +} + +func (s *Step) Do(log *log.Logger, vuser *VirtualUser, scenario *Scenario, stepsToController chan<- StepIteration) error { + log.Printf("--> Step::Do(%v)", s.name) + + // Prepare the statistics structure + var stat StepIteration + stat.success = true + stat.vuser = vuser.name + stat.scenario = scenario.name + stat.step = s.name + + // Measure the beginning of this step + start := time.Now() + + err := s.impl.Do(log) + if (err != nil) { + log.Printf("Step %v ended with error %v", s.name, err) + stat.success = false; + } + + // Compute the elapsed time since the beginning of this step + elapsed := time.Since(start) + stat.elapsed = elapsed + + // Send it to the controller + stepsToController <- stat + + log.Printf("<-- Step::Do(%v)", s.name) + return err; +} diff --git a/src/itix.fr/loadsprinter/core/VirtualUser.go b/src/itix.fr/loadsprinter/core/VirtualUser.go new file mode 100644 index 0000000..324c2a7 --- /dev/null +++ b/src/itix.fr/loadsprinter/core/VirtualUser.go @@ -0,0 +1,42 @@ +package core; + +import "log" +import "sync" + +type VirtualUser struct { + name string + scenario *Scenario +} + +func NewVirtualUser(name string, scenario *Scenario) *VirtualUser { + return &VirtualUser{ name: name, scenario: scenario } +} + +func (vu *VirtualUser) Init(log *log.Logger) error { + log.Println("--> VirtualUser::Init()") + // TODO + log.Println("<-- VirtualUser::Init()") + return nil +} + +func (vu *VirtualUser) Run(log *log.Logger, wg *sync.WaitGroup, start <-chan int, stepsToController chan<- StepIteration, scenarioToController chan<- ScenarioIteration) error { + log.Println("--> VirtualUser::Run()") + + // Make sure we notify we are done when this method is finished + defer wg.Done() + + // Wait for the signal to start + log.Println("Waiting for the signal to start") + <- start + log.Println("GOOOOOO !") + + for { + err := vu.scenario.Do(log, vu, stepsToController, scenarioToController); + if (err != nil) { + log.Printf("Scenario ended with error %v, ", err) + + } + } + log.Println("<-- VirtualUser::Run()") + return nil +} diff --git a/src/itix.fr/loadsprinter/steps/FailStep.go b/src/itix.fr/loadsprinter/steps/FailStep.go new file mode 100644 index 0000000..0abfb35 --- /dev/null +++ b/src/itix.fr/loadsprinter/steps/FailStep.go @@ -0,0 +1,17 @@ +package steps; + +import "log" +import "errors" + +type FailStep struct { + message string +} + +func NewFailStep(message string) *FailStep { + return &FailStep{ message: message }; +} + +func (fs *FailStep) Do(log *log.Logger) error { + log.Printf("error: %v", fs.message) + return errors.New(fs.message); +} diff --git a/src/itix.fr/loadsprinter/steps/LogStep.go b/src/itix.fr/loadsprinter/steps/LogStep.go new file mode 100644 index 0000000..afbbfa0 --- /dev/null +++ b/src/itix.fr/loadsprinter/steps/LogStep.go @@ -0,0 +1,16 @@ +package steps; + +import "log" + +type LogStep struct { + message string +} + +func NewLogStep(message string) *LogStep { + return &LogStep{ message: message }; +} + +func (ls *LogStep) Do(log *log.Logger) error { + log.Println(ls.message) + return nil; +} diff --git a/src/itix.fr/loadsprinter/steps/WaitStep.go b/src/itix.fr/loadsprinter/steps/WaitStep.go new file mode 100644 index 0000000..0ecf6d4 --- /dev/null +++ b/src/itix.fr/loadsprinter/steps/WaitStep.go @@ -0,0 +1,19 @@ +package steps; + +import "time" +import "log" + +type WaitStep struct { + duration time.Duration +} + +func NewWaitStep(duration time.Duration) *WaitStep { + return &WaitStep{ duration: duration }; +} + +func (ws *WaitStep) Do(log *log.Logger) error { + log.Printf("Sleeping during %v", ws.duration) + time.Sleep(ws.duration) + log.Println("Woken up !") + return nil +}