Skip to content

Commit 04789ba

Browse files
authored
Add support for counter resets (#152)
1 parent 2732131 commit 04789ba

File tree

4 files changed

+185
-11
lines changed

4 files changed

+185
-11
lines changed

docs/fields-configuration.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,15 @@ For each config entry the following fields are available:
1515
- `range` *optional (`date` type only)*: value will be generated between `from` and `to`. Only one between `from` and `to` can be set, in this case the dates will be generated between `from`/`to` and `time.Now()`. Progressive order of the generated dates is always assured regardless the interval involving `from`, `to` and `time.Now()` is positive or negative. If both at least one of `from` or `to` and `period` settings are defined an error will be returned and the generator will stop. The format of the date must be parsable by the following golang date format: `2006-01-02T15:04:05.999999999-07:00`.
1616
- `cardinality` *optional*: exact number of different values to generate for the field; note that this setting may not be respected if not enough events are generated. For example, `cardinality: 1000` with `100` generated events would produce `100` different values, not `1000`. Similarly, the setting may not be respected if other settings prevents it. For example, `cardinality: 10` with an `enum` list of only 5 strings would produce `5` different values, not `10`. Or `cardinality: 10` for a `long` with `range.min: 1` and `range.max: 5` would produce `5` different values, not `10`.
1717
- `counter` *optional (`long` and `double` type only)*: if set to `true` values will be generated only ever-increasing. If `fuzziness` is not defined, the positive delta from the previous value will be totally random and unbounded. For example, assuming `counter: true`, assuming a `int` field type and with first value generated `10.`, will generate the second value with any random value greater than `10`, like `11` or `987615243`. If `fuzziness` is defined, the value will be generated within a positive delta defined by `fuzziness` from the previous value. For example, `fuzziness: 0.1`, assuming `counter: true` , assuming a `double` field type and with first value generated `10.`, will generate the second value in the range between `10.` and `11.`. Assuming the second value generated will be `10.5`, the third one will be generated in the range between `10.5` and `11.55`, and so on. If both `counter: true` and at least one of `range.min` or `range.max` settings are defined an error will be returned and the generator will stop.
18+
- `counter_reset` *optional (only applicable when `counter: true`)*: configures how and when the counter should reset. It has the following sub-fields:
19+
- `strategy` *mandatory*: defines the reset strategy. Possible values are:
20+
- `"random"`: resets the counter at random intervals.
21+
- `"probabilistic"`: resets the counter based on a probability.
22+
- `"after_n"`: resets the counter after a specific number of iterations.
23+
- `probability` *required when strategy is "probabilistic"*: an integer between 1 and 100 representing the percentage chance of reset for each generated value.
24+
- `reset_after_n` *required when strategy is "after_n"*: an integer specifying the number of values to generate before resetting the counter.
25+
26+
Note: The `counter_reset` configuration is only applicable when `counter` is set to `true`.
1827
- `period` *optional (`date` type only)*: values will be evenly generated between `time.Now()` and `time.Now().Add(period)`, where period is expressed as `time.Duration`. It accepts also a negative duration: in this case values will be evenly generated between `time.Now().Add(period)` and `time.Now()`. If both `period` and at least one of `range.from` or `range.to` settings are defined an error will be returned and the generator will stop.
1928
- `object_keys` *optional (`object` type only)*: list of field names to generate in a object field type; if not specified a random number of field names will be generated in the object filed type
2029
- `value` *optional*: hardcoded value to set for the field (any `cardinality` will be ignored)

pkg/genlib/config/config.go

Lines changed: 49 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -39,15 +39,55 @@ type Config struct {
3939
}
4040

4141
type ConfigField struct {
42-
Name string `config:"name"`
43-
Fuzziness float64 `config:"fuzziness"`
44-
Range Range `config:"range"`
45-
Cardinality int `config:"cardinality"`
46-
Period time.Duration `config:"period"`
47-
Enum []string `config:"enum"`
48-
ObjectKeys []string `config:"object_keys"`
49-
Value any `config:"value"`
50-
Counter bool `config:"counter"`
42+
Name string `config:"name"`
43+
Fuzziness float64 `config:"fuzziness"`
44+
Range Range `config:"range"`
45+
Cardinality int `config:"cardinality"`
46+
Period time.Duration `config:"period"`
47+
Enum []string `config:"enum"`
48+
ObjectKeys []string `config:"object_keys"`
49+
Value any `config:"value"`
50+
Counter bool `config:"counter"`
51+
CounterReset *CounterReset `config:"counter_reset"`
52+
}
53+
54+
const (
55+
CounterResetStrategyRandom string = "random"
56+
CounterResetStrategyProbabilistic string = "probabilistic"
57+
CounterResetStrategyAfterN string = "after_n"
58+
)
59+
60+
type CounterReset struct {
61+
Strategy string `config:"strategy"`
62+
Probability *uint64 `config:"probability"`
63+
ResetAfterN *uint64 `config:"reset_after_n"`
64+
}
65+
66+
func (cf ConfigField) ValidateCounterResetStrategy() error {
67+
if cf.Counter && cf.CounterReset != nil &&
68+
cf.CounterReset.Strategy != CounterResetStrategyRandom &&
69+
cf.CounterReset.Strategy != CounterResetStrategyProbabilistic &&
70+
cf.CounterReset.Strategy != CounterResetStrategyAfterN {
71+
return errors.New("counter_reset strategy must be one of 'random', 'probabilistic', 'after_n'")
72+
}
73+
74+
return nil
75+
}
76+
77+
func (cf ConfigField) ValidateCounterResetAfterN() error {
78+
if cf.Counter && cf.CounterReset != nil && cf.CounterReset.Strategy == CounterResetStrategyAfterN && cf.CounterReset.ResetAfterN == nil {
79+
return errors.New("counter_reset after_n requires 'reset_after_n' value to be set")
80+
}
81+
82+
return nil
83+
}
84+
85+
func (cf ConfigField) ValidateCounterResetProbabilistic() error {
86+
if cf.Counter && cf.CounterReset != nil && cf.CounterReset.Strategy == CounterResetStrategyProbabilistic && cf.CounterReset.Probability == nil {
87+
return errors.New("counter_reset probabilistic requires 'probability' value to be set")
88+
}
89+
90+
return nil
5191
}
5292

5393
func (cf ConfigField) ValidForDateField() error {

pkg/genlib/generator_interface.go

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,6 @@ func newGenState() *genState {
9999
}
100100

101101
func bindField(cfg Config, field Field, fieldMap map[string]any, withReturn bool) error {
102-
103102
// Check for hardcoded field value
104103
if len(field.Value) > 0 {
105104
if withReturn {
@@ -194,7 +193,6 @@ func bindByType(cfg Config, field Field, fieldMap map[string]any) (err error) {
194193
}
195194

196195
func bindByTypeWithReturn(cfg Config, field Field, fieldMap map[string]any) (err error) {
197-
198196
fieldCfg, _ := cfg.GetField(field.Name)
199197

200198
switch field.Type {
@@ -971,6 +969,18 @@ func bindLongWithReturn(fieldCfg ConfigField, field Field, fieldMap map[string]a
971969
return err
972970
}
973971

972+
if err := fieldCfg.ValidateCounterResetStrategy(); err != nil {
973+
return err
974+
}
975+
976+
if err := fieldCfg.ValidateCounterResetAfterN(); err != nil {
977+
return err
978+
}
979+
980+
if err := fieldCfg.ValidateCounterResetProbabilistic(); err != nil {
981+
return err
982+
}
983+
974984
if len(fieldCfg.Enum) > 0 {
975985
var emitF emitF
976986
idx := customRand.Intn(len(fieldCfg.Enum))
@@ -1008,6 +1018,26 @@ func bindLongWithReturn(fieldCfg ConfigField, field Field, fieldMap map[string]a
10081018
dummyInt = fuzzyIntCounter(previous, fieldCfg.Fuzziness)
10091019
}
10101020

1021+
if fieldCfg.CounterReset != nil {
1022+
switch fieldCfg.CounterReset.Strategy {
1023+
case config.CounterResetStrategyRandom:
1024+
// 50% chance to reset
1025+
if customRand.Intn(2) == 0 {
1026+
dummyInt = 0
1027+
}
1028+
case config.CounterResetStrategyProbabilistic:
1029+
// Probability% chance to reset
1030+
if customRand.Intn(100) < int(*fieldCfg.CounterReset.Probability) {
1031+
dummyInt = 0
1032+
}
1033+
case config.CounterResetStrategyAfterN:
1034+
// Reset after N
1035+
if state.counter%*fieldCfg.CounterReset.ResetAfterN == 0 {
1036+
dummyInt = 0
1037+
}
1038+
}
1039+
}
1040+
10111041
state.prevCache[field.Name] = dummyInt
10121042
return dummyInt
10131043
}
@@ -1090,6 +1120,18 @@ func bindDoubleWithReturn(fieldCfg ConfigField, field Field, fieldMap map[string
10901120
return err
10911121
}
10921122

1123+
if err := fieldCfg.ValidateCounterResetStrategy(); err != nil {
1124+
return err
1125+
}
1126+
1127+
if err := fieldCfg.ValidateCounterResetAfterN(); err != nil {
1128+
return err
1129+
}
1130+
1131+
if err := fieldCfg.ValidateCounterResetProbabilistic(); err != nil {
1132+
return err
1133+
}
1134+
10931135
if len(fieldCfg.Enum) > 0 {
10941136
var emitF emitF
10951137
idx := customRand.Intn(len(fieldCfg.Enum))
@@ -1127,6 +1169,26 @@ func bindDoubleWithReturn(fieldCfg ConfigField, field Field, fieldMap map[string
11271169
dummyFloat = fuzzyFloatCounter(previous, fieldCfg.Fuzziness)
11281170
}
11291171

1172+
if fieldCfg.CounterReset != nil {
1173+
switch fieldCfg.CounterReset.Strategy {
1174+
case config.CounterResetStrategyRandom:
1175+
// 50% chance to reset
1176+
if customRand.Intn(2) == 0 {
1177+
dummyFloat = 0
1178+
}
1179+
case config.CounterResetStrategyProbabilistic:
1180+
// Probability% chance to reset
1181+
if customRand.Intn(100) < int(*fieldCfg.CounterReset.Probability) {
1182+
dummyFloat = 0
1183+
}
1184+
case config.CounterResetStrategyAfterN:
1185+
// Reset after N
1186+
if state.counter%*fieldCfg.CounterReset.ResetAfterN == 0 {
1187+
dummyFloat = 0
1188+
}
1189+
}
1190+
}
1191+
11301192
state.prevCache[field.Name] = dummyFloat
11311193
return dummyFloat
11321194
}

pkg/genlib/generator_with_text_template_test.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -755,6 +755,69 @@ func Test_FieldIPWithTextTemplate(t *testing.T) {
755755
}
756756
}
757757

758+
func Test_FieldLongCounterResetAfterN5WithTextTemplate(t *testing.T) {
759+
fld := Field{
760+
Name: "counter_reset_test",
761+
Type: FieldTypeLong,
762+
}
763+
764+
afterN := 5
765+
766+
template := []byte(`{{$counter_reset_test := generate "counter_reset_test"}}{"counter_reset_test":"{{$counter_reset_test}}"}`)
767+
configYaml := []byte(fmt.Sprintf(`fields:
768+
- name: counter_reset_test
769+
counter: true
770+
counter_reset:
771+
strategy: after_n
772+
reset_after_n: %d`, afterN))
773+
t.Logf("with template: %s", string(template))
774+
775+
cfg, err := config.LoadConfigFromYaml(configYaml)
776+
if err != nil {
777+
t.Fatal(err)
778+
}
779+
780+
g := makeGeneratorWithTextTemplate(t, cfg, []Field{fld}, template, 40)
781+
782+
var buf bytes.Buffer
783+
784+
nSpins := int64(40)
785+
786+
var resetCount int64
787+
expectedResetCount := nSpins / int64(afterN) // 8
788+
789+
for i := int64(0); i < nSpins; i++ {
790+
if err := g.Emit(&buf); err != nil {
791+
t.Fatal(err)
792+
}
793+
794+
m := unmarshalJSONT[string](t, buf.Bytes())
795+
buf.Reset()
796+
797+
if len(m) != 1 {
798+
t.Errorf("Expected map size 1, got %d", len(m))
799+
}
800+
801+
v, ok := m[fld.Name]
802+
if !ok {
803+
t.Errorf("Missing key %v", fld.Name)
804+
}
805+
806+
if i%int64(afterN) == 0 {
807+
if v != "0" {
808+
t.Errorf("Expected counter to reset to 0, got %v", v)
809+
}
810+
resetCount++
811+
}
812+
813+
t.Logf("counter value: %v", v)
814+
}
815+
816+
if resetCount != expectedResetCount {
817+
t.Errorf("Expected counter to reset %d times, got %d", expectedResetCount, resetCount)
818+
}
819+
}
820+
758821
func Test_FieldFloatsWithTextTemplate(t *testing.T) {
759822
_testNumericWithTextTemplate[float64](t, FieldTypeDouble)
760823
_testNumericWithTextTemplate[float32](t, FieldTypeFloat)

0 commit comments

Comments
 (0)