Skip to content

Commit c35fda5

Browse files
committed
Add benchmarking tooling.
1 parent 341cf45 commit c35fda5

17 files changed

+199
-29
lines changed

.test-setup

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export KIEBITZ_SETTINGS=`readlink -f settings/test`

README.md

+12
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,18 @@ make test-races
162162
make bench
163163
```
164164

165+
### Load Testing
166+
167+
**Careful, the following commands will create massive amounts of fake data via the API client, never run this against the production system, except for initial load testing!**
168+
169+
You can simulate providers, appointments and bookings in the backend using the `testing benchmark` command. The following command will create 1.000 providers with 1.000 appointments with 20 slots each, hence a total of 20.000.000 appointments:
170+
171+
```bash
172+
kiebitz testing benchmark --providers 1000 --appointments 1000 --slots 20
173+
```
174+
175+
This command will use the given settings and connect to a running API server, hence to run it you need to also run the Kiebitz API. Probably you want to use the `test` settings for this
176+
165177
## Development
166178

167179
To auto-generate copyright headers for Golang files, simply run

appointments.go

+1
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,7 @@ type SignedProviderData struct {
242242
Data *ProviderData `json:"-" coerce:"name:data"`
243243
Signature []byte `json:"signature"`
244244
PublicKey []byte `json:"publicKey"`
245+
ID []byte `json:"id"`
245246
}
246247

247248
func (k *ProviderData) Sign(key *crypto.Key) (*SignedProviderData, error) {

cmd/commands.go

+4
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ var Commands = services.CommandsDefinitions{
2626
Name: "admin",
2727
Maker: helpers.Admin,
2828
},
29+
services.CommandsDefinition{
30+
Name: "testing",
31+
Maker: helpers.Testing,
32+
},
2933
services.CommandsDefinition{
3034
Name: "run",
3135
Maker: helpers.Run,

cmd/helpers/testing.go

+136
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
// Kiebitz - Privacy-Friendly Appointment Scheduling
2+
// Copyright (C) 2021-2021 The Kiebitz Authors
3+
//
4+
// This program is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU Affero General Public License as
6+
// published by the Free Software Foundation, either version 3 of the
7+
// License, or (at your option) any later version.
8+
//
9+
// This program is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU Affero General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU Affero General Public License
15+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
16+
17+
package helpers
18+
19+
import (
20+
"fmt"
21+
"github.com/kiebitz-oss/services"
22+
at "github.com/kiebitz-oss/services/testing"
23+
af "github.com/kiebitz-oss/services/testing/fixtures"
24+
"github.com/urfave/cli"
25+
)
26+
27+
type SettingsFixture struct {
28+
settings *services.Settings
29+
}
30+
31+
func (s SettingsFixture) Setup(fixtures map[string]interface{}) (interface{}, error) {
32+
return s.settings, nil
33+
}
34+
35+
func (s SettingsFixture) Teardown(fixture interface{}) error {
36+
return nil
37+
}
38+
39+
func benchmark(settings *services.Settings) func(c *cli.Context) error {
40+
return func(c *cli.Context) error {
41+
42+
safetyOff := c.Bool("safetyOff")
43+
appointments := c.Int("appointments")
44+
providers := c.Int("providers")
45+
slots := c.Int("slots")
46+
47+
if !settings.Test && !safetyOff {
48+
return fmt.Errorf("Non-test system detected, aborting! Override this by setting --safetyOff.")
49+
}
50+
51+
var fixturesConfig = []at.FC{
52+
53+
// we create the settings
54+
at.FC{SettingsFixture{settings: settings}, "settings"},
55+
56+
// we create a client (without a key)
57+
at.FC{af.Client{}, "client"},
58+
59+
// we create a mediator
60+
at.FC{af.Mediator{}, "mediator"},
61+
62+
at.FC{af.ProvidersAndAppointments{
63+
Providers: int64(providers),
64+
BaseProvider: af.Provider{
65+
ZipCode: "10707",
66+
Name: "Dr. Maier Müller",
67+
Street: "Mühlenstr. 55",
68+
City: "Berlin",
69+
StoreData: true,
70+
Confirm: true,
71+
},
72+
BaseAppointments: af.Appointments{
73+
N: int64(appointments),
74+
Start: af.TS("2022-10-01T12:00:00Z"),
75+
Duration: 30,
76+
Slots: int64(slots),
77+
Properties: map[string]interface{}{
78+
"vaccine": "moderna",
79+
},
80+
},
81+
}, "providersAndAppointments"},
82+
}
83+
84+
fixtures, err := at.SetupFixtures(fixturesConfig)
85+
86+
if err != nil {
87+
return err
88+
}
89+
90+
if err := at.TeardownFixtures(fixturesConfig, fixtures); err != nil {
91+
return err
92+
}
93+
94+
return nil
95+
}
96+
}
97+
98+
func Testing(settings *services.Settings) ([]cli.Command, error) {
99+
100+
return []cli.Command{
101+
{
102+
Name: "testing",
103+
Aliases: []string{"a"},
104+
Flags: []cli.Flag{},
105+
Usage: "Test functionality.",
106+
Subcommands: []cli.Command{
107+
{
108+
Name: "benchmark",
109+
Flags: []cli.Flag{
110+
&cli.IntFlag{
111+
Name: "providers",
112+
Value: 1000,
113+
Usage: "number of providers to create",
114+
},
115+
&cli.IntFlag{
116+
Name: "appointments",
117+
Value: 1000,
118+
Usage: "number of appointments to create per provider",
119+
},
120+
&cli.IntFlag{
121+
Name: "slots",
122+
Value: 20,
123+
Usage: "number of slots per appointments to create",
124+
},
125+
&cli.BoolFlag{
126+
Name: "safetyOff",
127+
Usage: "set if you want to run this test on a non-test system",
128+
},
129+
},
130+
Usage: "Run a comprehensive benchmark.",
131+
Action: benchmark(settings),
132+
},
133+
},
134+
},
135+
}, nil
136+
}

forms/appointments.go

-1
Original file line numberDiff line numberDiff line change
@@ -944,7 +944,6 @@ var AppointmentDataForm = forms.Form{
944944
{
945945
Name: "publicKey",
946946
Validators: []forms.Validator{
947-
forms.IsOptional{},
948947
forms.IsBytes{
949948
Encoding: "base64",
950949
MaxLength: 1000,

forms/settings.go

+7
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,13 @@ var MetricsForm = forms.Form{
380380

381381
var SettingsForm = forms.Form{
382382
Fields: []forms.Field{
383+
{
384+
Name: "test",
385+
Validators: []forms.Validator{
386+
forms.IsOptional{Default: false},
387+
forms.IsBoolean{},
388+
},
389+
},
383390
{
384391
Name: "admin",
385392
Validators: []forms.Validator{

servers/app_appointments_benchmark_test.go

+3-2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package servers_test
1818

1919
import (
2020
"github.com/kiebitz-oss/services"
21+
"github.com/kiebitz-oss/services/definitions"
2122
"github.com/kiebitz-oss/services/helpers"
2223
at "github.com/kiebitz-oss/services/testing"
2324
af "github.com/kiebitz-oss/services/testing/fixtures"
@@ -29,7 +30,7 @@ func BenchmarkAppointmentsEndpoints(b *testing.B) {
2930
var fixturesConfig = []at.FC{
3031

3132
// we create the settings
32-
at.FC{af.Settings{}, "settings"},
33+
at.FC{af.Settings{definitions.Default}, "settings"},
3334

3435
// we create the appointments API
3536
at.FC{af.AppointmentsServer{}, "appointmentsServer"},
@@ -49,7 +50,7 @@ func BenchmarkAppointmentsEndpoints(b *testing.B) {
4950
},
5051
BaseAppointments: af.Appointments{
5152
N: 100,
52-
Start: ts("2022-10-01T12:00:00Z"),
53+
Start: af.TS("2022-10-01T12:00:00Z"),
5354
Duration: 30,
5455
Slots: 20,
5556
Properties: map[string]interface{}{

servers/app_confirm_provider_test.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package servers_test
1818

1919
import (
2020
"encoding/json"
21+
"github.com/kiebitz-oss/services/definitions"
2122
"github.com/kiebitz-oss/services/helpers"
2223
at "github.com/kiebitz-oss/services/testing"
2324
af "github.com/kiebitz-oss/services/testing/fixtures"
@@ -63,7 +64,7 @@ func TestConfirmProvider(t *testing.T) {
6364
var fixturesConfig = []at.FC{
6465

6566
// we create the settings
66-
at.FC{af.Settings{}, "settings"},
67+
at.FC{af.Settings{definitions.Default}, "settings"},
6768

6869
// we create the appointments API
6970
at.FC{af.AppointmentsServer{}, "appointmentsServer"},

servers/app_get_appointments_by_zip_code.go

+15-2
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,12 @@ func (c *Appointments) getAppointmentsByZipCode(context *jsonrpc.Context, params
5151

5252
providerAppointmentsList := []*services.ProviderAppointments{}
5353

54-
for _, providerKey := range keys.Providers {
54+
for i, providerKey := range keys.Providers {
55+
56+
// max 10 providers
57+
if i > 10 {
58+
break
59+
}
5560

5661
pkd, err := providerKey.ProviderKeyData()
5762
if err != nil {
@@ -76,7 +81,7 @@ func (c *Appointments) getAppointmentsByZipCode(context *jsonrpc.Context, params
7681
if err != databases.NotFound {
7782
services.Log.Error(err)
7883
}
79-
services.Log.Info("provider data not found")
84+
services.Log.Warning("provider data not found")
8085
continue
8186
}
8287

@@ -110,6 +115,7 @@ func (c *Appointments) getAppointmentsByZipCode(context *jsonrpc.Context, params
110115

111116
visitedDates := make(map[string]bool)
112117

118+
getAppointments:
113119
for _, date := range allDates {
114120

115121
if _, ok := visitedDates[string(date)]; ok {
@@ -144,6 +150,10 @@ func (c *Appointments) getAppointmentsByZipCode(context *jsonrpc.Context, params
144150
signedAppointment.BookedSlots = slots
145151

146152
signedAppointments = append(signedAppointments, signedAppointment)
153+
154+
if len(signedAppointments) > 10 {
155+
break getAppointments
156+
}
147157
}
148158
}
149159

@@ -163,6 +173,9 @@ func (c *Appointments) getAppointmentsByZipCode(context *jsonrpc.Context, params
163173
Mediator: mediatorKey,
164174
}
165175

176+
// we add the hash for convenience
177+
providerData.ID = hash
178+
166179
providerAppointments := &services.ProviderAppointments{
167180
Provider: providerData,
168181
Offers: signedAppointments,

servers/app_publish_appointments_test.go

+3-11
Original file line numberDiff line numberDiff line change
@@ -17,26 +17,18 @@
1717
package servers_test
1818

1919
import (
20+
"github.com/kiebitz-oss/services/definitions"
2021
at "github.com/kiebitz-oss/services/testing"
2122
af "github.com/kiebitz-oss/services/testing/fixtures"
2223
"testing"
23-
"time"
2424
)
2525

26-
func ts(dt string) time.Time {
27-
if t, err := time.Parse(time.RFC3339, dt); err != nil {
28-
panic(err)
29-
} else {
30-
return t
31-
}
32-
}
33-
3426
func TestPublishAppointments(t *testing.T) {
3527

3628
var fixturesConfig = []at.FC{
3729

3830
// we create the settings
39-
at.FC{af.Settings{}, "settings"},
31+
at.FC{af.Settings{definitions.Default}, "settings"},
4032

4133
// we create the appointments API
4234
at.FC{af.AppointmentsServer{}, "appointmentsServer"},
@@ -56,7 +48,7 @@ func TestPublishAppointments(t *testing.T) {
5648

5749
at.FC{af.Appointments{
5850
N: 1000,
59-
Start: ts("2022-10-01T12:00:00Z"),
51+
Start: af.TS("2022-10-01T12:00:00Z"),
6052
Duration: 30,
6153
Slots: 20,
6254
Properties: map[string]interface{}{

servers/appointments_test.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package servers_test
1818

1919
import (
2020
"github.com/kiebitz-oss/services"
21+
"github.com/kiebitz-oss/services/definitions"
2122
"github.com/kiebitz-oss/services/helpers"
2223
at "github.com/kiebitz-oss/services/testing"
2324
af "github.com/kiebitz-oss/services/testing/fixtures"
@@ -29,7 +30,7 @@ func TestAppointmentsApi(t *testing.T) {
2930
var fixturesConfig = []at.FC{
3031

3132
// we create the settings
32-
at.FC{af.Settings{}, "settings"},
33+
at.FC{af.Settings{definitions.Default}, "settings"},
3334

3435
// we create the appointments API
3536
at.FC{af.AppointmentsServer{}, "appointmentsServer"},

settings.go

+1
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ type MeterSettings struct {
7272
}
7373

7474
type Settings struct {
75+
Test bool `json:"test"`
7576
Admin *AdminSettings `json:"admin,omitempty"`
7677
Definitions *Definitions `json:"definitions,omitempty"`
7778
Storage *StorageSettings `json:"storage,omitempty"`

settings/test/001_default.yml

+1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
name: kiebitz
2+
test: true
23
meter:
34
name: meter
45
type: redis

testing/fixtures/appointments.go

+9
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,14 @@ import (
2323
"time"
2424
)
2525

26+
func TS(dt string) time.Time {
27+
if t, err := time.Parse(time.RFC3339, dt); err != nil {
28+
panic(err)
29+
} else {
30+
return t
31+
}
32+
}
33+
2634
type Appointments struct {
2735
Start time.Time
2836
Duration int64
@@ -52,6 +60,7 @@ func (c Appointments) Setup(fixtures map[string]interface{}) (interface{}, error
5260
if appointment, err := services.MakeAppointment(ct, c.Slots, c.Duration); err != nil {
5361
return nil, err
5462
} else {
63+
appointment.PublicKey = provider.Actor.EncryptionKey.PublicKey
5564
appointment.Properties = c.Properties
5665
if signedAppointment, err := appointment.Sign(provider.Actor.SigningKey); err != nil {
5766
return nil, err

0 commit comments

Comments
 (0)