From bf7f646eaab0e252c87bbdcd047b03586a8ee534 Mon Sep 17 00:00:00 2001 From: Alfonso Cantos Date: Thu, 20 Nov 2025 22:17:19 +0100 Subject: [PATCH 1/5] fix: race condition --- Makefile | 4 ++ collection/indexmap.go | 3 ++ collection/indexmap_benchmark_test.go | 49 +++++++++++++++++++++++++ collection/indexmap_race_test.go | 53 +++++++++++++++++++++++++++ 4 files changed, 109 insertions(+) create mode 100644 collection/indexmap_benchmark_test.go create mode 100644 collection/indexmap_race_test.go diff --git a/Makefile b/Makefile index c478ccc..ce591a0 100644 --- a/Makefile +++ b/Makefile @@ -52,3 +52,7 @@ book: .PHONY: version version: @echo $(VERSION) + +.PHONY: bench +bench: + go test -bench=BenchmarkIndexMap_RemoveRow_Concurrent ./collection/... diff --git a/collection/indexmap.go b/collection/indexmap.go index 77901f6..2d36cae 100644 --- a/collection/indexmap.go +++ b/collection/indexmap.go @@ -39,6 +39,9 @@ func (i *IndexMap) RemoveRow(row *Row) error { return nil } + i.RWmutex.Lock() + defer i.RWmutex.Unlock() + switch value := itemValue.(type) { case string: delete(entries, value) diff --git a/collection/indexmap_benchmark_test.go b/collection/indexmap_benchmark_test.go new file mode 100644 index 0000000..ba0f78e --- /dev/null +++ b/collection/indexmap_benchmark_test.go @@ -0,0 +1,49 @@ +package collection + +import ( + "fmt" + "math/rand" + "testing" +) + +func BenchmarkIndexMap_RemoveRow_Concurrent(b *testing.B) { + options := &IndexMapOptions{Field: "key", Sparse: false} + index := NewIndexMap(options) + + // Pre-populate the index with a large number of items to simulate a real scenario + const initialSize = 100000 + for i := 0; i < initialSize; i++ { + key := fmt.Sprintf("key-%d", i) + row := &Row{Payload: createPayload(key, options.Field)} + if err := index.AddRow(row); err != nil { + b.Fatalf("AddRow error: %v", err) + } + } + + b.ResetTimer() + + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + // Generate a random key to remove to simulate random access + // We use a key range slightly larger than initialSize to also test "not found" cases occasionally, + // but mostly we want to test removal contention. + // However, if we only remove, we will run out of items. + // So we will randomly add or remove to keep the map populated, + // OR we can just accept that we are removing items and the map shrinks. + // For a pure "RemoveRow" benchmark, we should probably just try to remove existing items. + // But if we remove them all, the benchmark becomes trivial. + // + // To strictly test RemoveRow with mutex contention, we can try to remove random keys. + // Even if they don't exist, the mutex is still acquired to check. + // + // Let's try to remove keys from the initial set. + + i := rand.Intn(initialSize) + key := fmt.Sprintf("key-%d", i) + row := &Row{Payload: createPayload(key, options.Field)} + + // We ignore the error because the item might have been already removed by another goroutine + _ = index.RemoveRow(row) + } + }) +} diff --git a/collection/indexmap_race_test.go b/collection/indexmap_race_test.go new file mode 100644 index 0000000..8897db6 --- /dev/null +++ b/collection/indexmap_race_test.go @@ -0,0 +1,53 @@ +package collection + +import ( + "encoding/json" + "sync" + "testing" +) + +func TestIndexMap_RemoveRow_Race(t *testing.T) { + index := NewIndexMap(&IndexMapOptions{ + Field: "id", + }) + + var wg sync.WaitGroup + const numGoroutines = 100 + + // Populate initial data + for i := 0; i < numGoroutines; i++ { + data := map[string]interface{}{ + "id": "test-id", + } + payload, _ := json.Marshal(data) + row := &Row{Payload: payload} + _ = index.AddRow(row) + } + + // Concurrently add and remove rows + for i := 0; i < numGoroutines; i++ { + wg.Add(2) + + go func() { + defer wg.Done() + data := map[string]interface{}{ + "id": "test-id", + } + payload, _ := json.Marshal(data) + row := &Row{Payload: payload} + _ = index.AddRow(row) + }() + + go func() { + defer wg.Done() + data := map[string]interface{}{ + "id": "test-id", + } + payload, _ := json.Marshal(data) + row := &Row{Payload: payload} + _ = index.RemoveRow(row) + }() + } + + wg.Wait() +} From a93c5dd3b2ca5f0a8f9bee72a3f97e4cf2364f3c Mon Sep 17 00:00:00 2001 From: Alfonso Cantos Date: Thu, 20 Nov 2025 22:31:33 +0100 Subject: [PATCH 2/5] fix: GOTOOLCHAIN to go1.25 --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index ce591a0..06aea55 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ FLAGS = -ldflags "\ " test: - go test -cover ./... + GOTOOLCHAIN=go1.25.0+auto go test -cover ./... run: STATICS=statics/www/ go run $(FLAGS) ./cmd/inceptiondb/... From 67d69c8e7d0e66c56d8a88f435c9a64fed8584b5 Mon Sep 17 00:00:00 2001 From: Alfonso Cantos Date: Thu, 20 Nov 2025 23:14:53 +0100 Subject: [PATCH 3/5] update: go-json-experiment --- go.mod | 2 +- go.sum | 4 +- .../go-json-experiment/json/README.md | 4 +- .../go-json-experiment/json/alias.go | 12 ++--- .../go-json-experiment/json/arshal.go | 10 ++-- .../go-json-experiment/json/arshal_any.go | 3 +- .../go-json-experiment/json/arshal_default.go | 53 +++++++++++++++---- .../go-json-experiment/json/arshal_funcs.go | 4 ++ .../go-json-experiment/json/arshal_inlined.go | 3 +- .../go-json-experiment/json/arshal_methods.go | 4 ++ .../go-json-experiment/json/arshal_time.go | 2 +- .../go-json-experiment/json/errors.go | 20 +++++-- .../json/internal/jsonopts/options.go | 8 +-- .../go-json-experiment/json/jsontext/alias.go | 4 +- .../json/jsontext/decode.go | 6 +++ .../go-json-experiment/json/jsontext/pools.go | 12 +++++ .../go-json-experiment/json/jsontext/state.go | 4 +- .../go-json-experiment/json/migrate.sh | 7 +-- .../go-json-experiment/json/options.go | 5 +- vendor/modules.txt | 2 +- 20 files changed, 119 insertions(+), 50 deletions(-) diff --git a/go.mod b/go.mod index 10bac91..488c7ea 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/fulldump/biff v1.3.0 github.com/fulldump/box v0.7.0 github.com/fulldump/goconfig v1.7.1 - github.com/go-json-experiment/json v0.0.0-20250910080747-cc2cfa0554c3 + github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e github.com/google/btree v1.1.3 github.com/google/uuid v1.6.0 ) diff --git a/go.sum b/go.sum index f3dc3ba..c1cf62c 100644 --- a/go.sum +++ b/go.sum @@ -9,8 +9,8 @@ github.com/fulldump/box v0.7.0 h1:aaGVNDmEOzizQ+U9bLtL8ST7RA5mjpT9i9q9h84GgoE= github.com/fulldump/box v0.7.0/go.mod h1:k1dcwIeNOar6zLlP9D8oF/4FjQeK8kAt7BtRUh/SrMg= github.com/fulldump/goconfig v1.7.1 h1:KTaig5QRf7ysL/0Om1q+J4OyMXbtsg+nonPY5SB+DUg= github.com/fulldump/goconfig v1.7.1/go.mod h1:qsSyOhlzhYkL2dJ3KWKxs1hX3Qv58Jzj8pRsIEkHmUY= -github.com/go-json-experiment/json v0.0.0-20250910080747-cc2cfa0554c3 h1:02WINGfSX5w0Mn+F28UyRoSt9uvMhKguwWMlOAh6U/0= -github.com/go-json-experiment/json v0.0.0-20250910080747-cc2cfa0554c3/go.mod h1:uNVvRXArCGbZ508SxYYTC5v1JWoz2voff5pm25jU1Ok= +github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e h1:Lf/gRkoycfOBPa42vU2bbgPurFong6zXeFtPoxholzU= +github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e/go.mod h1:uNVvRXArCGbZ508SxYYTC5v1JWoz2voff5pm25jU1Ok= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= diff --git a/vendor/github.com/go-json-experiment/json/README.md b/vendor/github.com/go-json-experiment/json/README.md index 937c398..649b901 100644 --- a/vendor/github.com/go-json-experiment/json/README.md +++ b/vendor/github.com/go-json-experiment/json/README.md @@ -1,7 +1,7 @@ # JSON Serialization (v2) [![GoDev](https://img.shields.io/static/v1?label=godev&message=reference&color=00add8)](https://pkg.go.dev/github.com/go-json-experiment/json) -[![Build Status](https://github.com/go-json-experiment/json/actions/workflows/test.yml/badge.svg?branch=master)](https://github.com/go-json-experiment/json/actions) +[![Build Status](https://github.com/go-json-experiment/json/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/go-json-experiment/json/actions) This module hosts an experimental implementation of v2 `encoding/json`. The API is unstable and breaking changes will regularly be made. @@ -13,6 +13,8 @@ It is your responsibility to inspect the list of commit changes when upgrading the module. Not all breaking changes will lead to build failures. A [proposal to include this module in Go as `encoding/json/v2` and `encoding/json/jsontext`](https://github.com/golang/go/issues/71497) has been started on the Go Github project on 2025-01-30. Please provide your feedback there. +At present, this module is available within the Go standard library as a Go experiment. +See ["A new experimental Go API for JSON"](https://go.dev/blog/jsonv2-exp) for more details. ## Goals and objectives diff --git a/vendor/github.com/go-json-experiment/json/alias.go b/vendor/github.com/go-json-experiment/json/alias.go index e239118..5e9008a 100644 --- a/vendor/github.com/go-json-experiment/json/alias.go +++ b/vendor/github.com/go-json-experiment/json/alias.go @@ -600,8 +600,9 @@ func UnmarshalRead(in io.Reader, out any, opts ...Options) (err error) { // Unlike [Unmarshal] and [UnmarshalRead], decode options are ignored because // they must have already been specified on the provided [jsontext.Decoder]. // -// The input may be a stream of one or more JSON values, +// The input may be a stream of zero or more JSON values, // where this only unmarshals the next JSON value in the stream. +// If there are no more top-level JSON values, it reports [io.EOF]. // The output must be a non-nil pointer. // See [Unmarshal] for details about the conversion of JSON into a Go value. func UnmarshalDecode(in *jsontext.Decoder, out any, opts ...Options) (err error) { @@ -789,8 +790,8 @@ type UnmarshalerFrom = json.UnmarshalerFrom // The name of an unknown JSON object member can be extracted as: // // err := ... -// var serr json.SemanticError -// if errors.As(err, &serr) && serr.Err == json.ErrUnknownName { +// serr, ok := errors.AsType[*json.SemanticError](err) +// if ok && serr.Err == json.ErrUnknownName { // ptr := serr.JSONPointer // JSON pointer to unknown name // name := ptr.LastToken() // unknown name itself // ... @@ -891,9 +892,8 @@ func GetOption[T any](opts Options, setter func(T) Options) (T, bool) { } // DefaultOptionsV2 is the full set of all options that define v2 semantics. -// It is equivalent to all options under [Options], [encoding/json.Options], -// and [encoding/json/jsontext.Options] being set to false or the zero value, -// except for the options related to whitespace formatting. +// It is equivalent to the set of options in [encoding/json.DefaultOptionsV1] +// all being set to false. All other options are not present. func DefaultOptionsV2() Options { return json.DefaultOptionsV2() } diff --git a/vendor/github.com/go-json-experiment/json/arshal.go b/vendor/github.com/go-json-experiment/json/arshal.go index be24cb8..5ab451c 100644 --- a/vendor/github.com/go-json-experiment/json/arshal.go +++ b/vendor/github.com/go-json-experiment/json/arshal.go @@ -11,8 +11,6 @@ import ( "encoding" "io" "reflect" - "slices" - "strings" "sync" "time" @@ -440,8 +438,9 @@ func UnmarshalRead(in io.Reader, out any, opts ...Options) (err error) { // Unlike [Unmarshal] and [UnmarshalRead], decode options are ignored because // they must have already been specified on the provided [jsontext.Decoder]. // -// The input may be a stream of one or more JSON values, +// The input may be a stream of zero or more JSON values, // where this only unmarshals the next JSON value in the stream. +// If there are no more top-level JSON values, it reports [io.EOF]. // The output must be a non-nil pointer. // See [Unmarshal] for details about the conversion of JSON into a Go value. func UnmarshalDecode(in *jsontext.Decoder, out any, opts ...Options) (err error) { @@ -572,9 +571,6 @@ func putStrings(s *stringSlice) { if cap(*s) > 1<<10 { *s = nil // avoid pinning arbitrarily large amounts of memory } + clear(*s) // avoid pinning a reference to each string stringsPools.Put(s) } - -func (ss *stringSlice) Sort() { - slices.SortFunc(*ss, func(x, y string) int { return strings.Compare(x, y) }) -} diff --git a/vendor/github.com/go-json-experiment/json/arshal_any.go b/vendor/github.com/go-json-experiment/json/arshal_any.go index a9db181..c9b9d7f 100644 --- a/vendor/github.com/go-json-experiment/json/arshal_any.go +++ b/vendor/github.com/go-json-experiment/json/arshal_any.go @@ -10,6 +10,7 @@ import ( "cmp" "math" "reflect" + "slices" "strconv" "github.com/go-json-experiment/json/internal" @@ -153,7 +154,7 @@ func marshalObjectAny(enc *jsontext.Encoder, obj map[string]any, mo *jsonopts.St (*names)[i] = name i++ } - names.Sort() + slices.Sort(*names) for _, name := range *names { if err := enc.WriteToken(jsontext.String(name)); err != nil { return err diff --git a/vendor/github.com/go-json-experiment/json/arshal_default.go b/vendor/github.com/go-json-experiment/json/arshal_default.go index 8084fcc..c393956 100644 --- a/vendor/github.com/go-json-experiment/json/arshal_default.go +++ b/vendor/github.com/go-json-experiment/json/arshal_default.go @@ -474,10 +474,21 @@ func makeIntArshaler(t reflect.Type) *arshaler { break } val = jsonwire.UnquoteMayCopy(val, flags.IsVerbatim()) - if uo.Flags.Get(jsonflags.StringifyWithLegacySemantics) && string(val) == "null" { - if !uo.Flags.Get(jsonflags.MergeWithLegacySemantics) { - va.SetInt(0) + if uo.Flags.Get(jsonflags.StringifyWithLegacySemantics) { + // For historical reasons, v1 parsed a quoted number + // according to the Go syntax and permitted a quoted null. + // See https://go.dev/issue/75619 + n, err := strconv.ParseInt(string(val), 10, bits) + if err != nil { + if string(val) == "null" { + if !uo.Flags.Get(jsonflags.MergeWithLegacySemantics) { + va.SetInt(0) + } + return nil + } + return newUnmarshalErrorAfterWithValue(dec, t, errors.Unwrap(err)) } + va.SetInt(n) return nil } fallthrough @@ -561,10 +572,21 @@ func makeUintArshaler(t reflect.Type) *arshaler { break } val = jsonwire.UnquoteMayCopy(val, flags.IsVerbatim()) - if uo.Flags.Get(jsonflags.StringifyWithLegacySemantics) && string(val) == "null" { - if !uo.Flags.Get(jsonflags.MergeWithLegacySemantics) { - va.SetUint(0) + if uo.Flags.Get(jsonflags.StringifyWithLegacySemantics) { + // For historical reasons, v1 parsed a quoted number + // according to the Go syntax and permitted a quoted null. + // See https://go.dev/issue/75619 + n, err := strconv.ParseUint(string(val), 10, bits) + if err != nil { + if string(val) == "null" { + if !uo.Flags.Get(jsonflags.MergeWithLegacySemantics) { + va.SetUint(0) + } + return nil + } + return newUnmarshalErrorAfterWithValue(dec, t, errors.Unwrap(err)) } + va.SetUint(n) return nil } fallthrough @@ -671,10 +693,21 @@ func makeFloatArshaler(t reflect.Type) *arshaler { if !stringify { break } - if uo.Flags.Get(jsonflags.StringifyWithLegacySemantics) && string(val) == "null" { - if !uo.Flags.Get(jsonflags.MergeWithLegacySemantics) { - va.SetFloat(0) + if uo.Flags.Get(jsonflags.StringifyWithLegacySemantics) { + // For historical reasons, v1 parsed a quoted number + // according to the Go syntax and permitted a quoted null. + // See https://go.dev/issue/75619 + n, err := strconv.ParseFloat(string(val), bits) + if err != nil { + if string(val) == "null" { + if !uo.Flags.Get(jsonflags.MergeWithLegacySemantics) { + va.SetFloat(0) + } + return nil + } + return newUnmarshalErrorAfterWithValue(dec, t, errors.Unwrap(err)) } + va.SetFloat(n) return nil } if n, err := jsonwire.ConsumeNumber(val); n != len(val) || err != nil { @@ -810,7 +843,7 @@ func makeMapArshaler(t reflect.Type) *arshaler { k.SetIterKey(iter) (*names)[i] = k.String() } - names.Sort() + slices.Sort(*names) for _, name := range *names { if err := enc.WriteToken(jsontext.String(name)); err != nil { return err diff --git a/vendor/github.com/go-json-experiment/json/arshal_funcs.go b/vendor/github.com/go-json-experiment/json/arshal_funcs.go index 2990e9a..381aa8c 100644 --- a/vendor/github.com/go-json-experiment/json/arshal_funcs.go +++ b/vendor/github.com/go-json-experiment/json/arshal_funcs.go @@ -9,6 +9,7 @@ package json import ( "errors" "fmt" + "io" "reflect" "sync" @@ -306,6 +307,9 @@ func UnmarshalFromFunc[T any](fn func(*jsontext.Decoder, T) error) *Unmarshalers fnc: func(dec *jsontext.Decoder, va addressableValue, uo *jsonopts.Struct) error { xd := export.Decoder(dec) prevDepth, prevLength := xd.Tokens.DepthLength() + if prevDepth == 1 && xd.AtEOF() { + return io.EOF // check EOF early to avoid fn reporting an EOF + } xd.Flags.Set(jsonflags.WithinArshalCall | 1) v, _ := reflect.TypeAssert[T](va.castTo(t)) err := fn(dec, v) diff --git a/vendor/github.com/go-json-experiment/json/arshal_inlined.go b/vendor/github.com/go-json-experiment/json/arshal_inlined.go index b071851..654844c 100644 --- a/vendor/github.com/go-json-experiment/json/arshal_inlined.go +++ b/vendor/github.com/go-json-experiment/json/arshal_inlined.go @@ -11,6 +11,7 @@ import ( "errors" "io" "reflect" + "slices" "github.com/go-json-experiment/json/internal/jsonflags" "github.com/go-json-experiment/json/internal/jsonopts" @@ -146,7 +147,7 @@ func marshalInlinedFallbackAll(enc *jsontext.Encoder, va addressableValue, mo *j mk.SetIterKey(iter) (*names)[i] = mk.String() } - names.Sort() + slices.Sort(*names) for _, name := range *names { mk.SetString(name) if err := marshalKey(mk); err != nil { diff --git a/vendor/github.com/go-json-experiment/json/arshal_methods.go b/vendor/github.com/go-json-experiment/json/arshal_methods.go index 5a2a11b..15054e0 100644 --- a/vendor/github.com/go-json-experiment/json/arshal_methods.go +++ b/vendor/github.com/go-json-experiment/json/arshal_methods.go @@ -9,6 +9,7 @@ package json import ( "encoding" "errors" + "io" "reflect" "github.com/go-json-experiment/json/internal" @@ -302,6 +303,9 @@ func makeMethodArshaler(fncs *arshaler, t reflect.Type) *arshaler { } xd := export.Decoder(dec) prevDepth, prevLength := xd.Tokens.DepthLength() + if prevDepth == 1 && xd.AtEOF() { + return io.EOF // check EOF early to avoid fn reporting an EOF + } xd.Flags.Set(jsonflags.WithinArshalCall | 1) unmarshaler, _ := reflect.TypeAssert[UnmarshalerFrom](va.Addr()) err := unmarshaler.UnmarshalJSONFrom(dec) diff --git a/vendor/github.com/go-json-experiment/json/arshal_time.go b/vendor/github.com/go-json-experiment/json/arshal_time.go index 64bdbda..fbf9baa 100644 --- a/vendor/github.com/go-json-experiment/json/arshal_time.go +++ b/vendor/github.com/go-json-experiment/json/arshal_time.go @@ -465,7 +465,7 @@ func appendDurationISO8601(b []byte, d time.Duration) []byte { } // daysPerYear is the exact average number of days in a year according to -// the Gregorian calender, which has an extra day each year that is +// the Gregorian calendar, which has an extra day each year that is // a multiple of 4, unless it is evenly divisible by 100 but not by 400. // This does not take into account leap seconds, which are not deterministic. const daysPerYear = 365.2425 diff --git a/vendor/github.com/go-json-experiment/json/errors.go b/vendor/github.com/go-json-experiment/json/errors.go index da17861..feb0964 100644 --- a/vendor/github.com/go-json-experiment/json/errors.go +++ b/vendor/github.com/go-json-experiment/json/errors.go @@ -10,6 +10,7 @@ import ( "cmp" "errors" "fmt" + "io" "reflect" "strconv" "strings" @@ -28,8 +29,8 @@ import ( // The name of an unknown JSON object member can be extracted as: // // err := ... -// var serr json.SemanticError -// if errors.As(err, &serr) && serr.Err == json.ErrUnknownName { +// serr, ok := errors.AsType[*json.SemanticError](err) +// if ok && serr.Err == json.ErrUnknownName { // ptr := serr.JSONPointer // JSON pointer to unknown name // name := ptr.LastToken() // unknown name itself // ... @@ -119,7 +120,7 @@ func newInvalidFormatError(c coder, t reflect.Type) error { // newMarshalErrorBefore wraps err in a SemanticError assuming that e // is positioned right before the next token or value, which causes an error. func newMarshalErrorBefore(e *jsontext.Encoder, t reflect.Type, err error) error { - return &SemanticError{action: "marshal", GoType: t, Err: err, + return &SemanticError{action: "marshal", GoType: t, Err: toUnexpectedEOF(err), ByteOffset: e.OutputOffset() + int64(export.Encoder(e).CountNextDelimWhitespace()), JSONPointer: jsontext.Pointer(export.Encoder(e).AppendStackPointer(nil, +1))} } @@ -135,7 +136,7 @@ func newUnmarshalErrorBefore(d *jsontext.Decoder, t reflect.Type, err error) err if export.Decoder(d).Flags.Get(jsonflags.ReportErrorsWithLegacySemantics) { k = d.PeekKind() } - return &SemanticError{action: "unmarshal", GoType: t, Err: err, + return &SemanticError{action: "unmarshal", GoType: t, Err: toUnexpectedEOF(err), ByteOffset: d.InputOffset() + int64(export.Decoder(d).CountNextDelimWhitespace()), JSONPointer: jsontext.Pointer(export.Decoder(d).AppendStackPointer(nil, +1)), JSONKind: k} @@ -158,7 +159,7 @@ func newUnmarshalErrorBeforeWithSkipping(d *jsontext.Decoder, t reflect.Type, er // is positioned right after the previous token or value, which caused an error. func newUnmarshalErrorAfter(d *jsontext.Decoder, t reflect.Type, err error) error { tokOrVal := export.Decoder(d).PreviousTokenOrValue() - return &SemanticError{action: "unmarshal", GoType: t, Err: err, + return &SemanticError{action: "unmarshal", GoType: t, Err: toUnexpectedEOF(err), ByteOffset: d.InputOffset() - int64(len(tokOrVal)), JSONPointer: jsontext.Pointer(export.Decoder(d).AppendStackPointer(nil, -1)), JSONKind: jsontext.Value(tokOrVal).Kind()} @@ -207,6 +208,7 @@ func newSemanticErrorWithPosition(c coder, t reflect.Type, prevDepth int, prevLe if serr == nil { serr = &SemanticError{Err: err} } + serr.Err = toUnexpectedEOF(serr.Err) var currDepth int var currLength int64 var coderState interface{ AppendStackPointer([]byte, int) []byte } @@ -433,3 +435,11 @@ func newDuplicateNameError(ptr jsontext.Pointer, quotedName []byte, offset int64 Err: jsontext.ErrDuplicateName, } } + +// toUnexpectedEOF converts [io.EOF] to [io.ErrUnexpectedEOF]. +func toUnexpectedEOF(err error) error { + if err == io.EOF { + return io.ErrUnexpectedEOF + } + return err +} diff --git a/vendor/github.com/go-json-experiment/json/internal/jsonopts/options.go b/vendor/github.com/go-json-experiment/json/internal/jsonopts/options.go index f0bd678..aba2d1b 100644 --- a/vendor/github.com/go-json-experiment/json/internal/jsonopts/options.go +++ b/vendor/github.com/go-json-experiment/json/internal/jsonopts/options.go @@ -48,16 +48,16 @@ type ArshalValues struct { // DefaultOptionsV2 is the set of all options that define default v2 behavior. var DefaultOptionsV2 = Struct{ Flags: jsonflags.Flags{ - Presence: uint64(jsonflags.AllFlags & ^jsonflags.WhitespaceFlags), - Values: uint64(0), + Presence: uint64(jsonflags.DefaultV1Flags), + Values: uint64(0), // all flags in DefaultV1Flags are false }, } // DefaultOptionsV1 is the set of all options that define default v1 behavior. var DefaultOptionsV1 = Struct{ Flags: jsonflags.Flags{ - Presence: uint64(jsonflags.AllFlags & ^jsonflags.WhitespaceFlags), - Values: uint64(jsonflags.DefaultV1Flags), + Presence: uint64(jsonflags.DefaultV1Flags), + Values: uint64(jsonflags.DefaultV1Flags), // all flags in DefaultV1Flags are true }, } diff --git a/vendor/github.com/go-json-experiment/json/jsontext/alias.go b/vendor/github.com/go-json-experiment/json/jsontext/alias.go index dc18d5d..ab501e8 100644 --- a/vendor/github.com/go-json-experiment/json/jsontext/alias.go +++ b/vendor/github.com/go-json-experiment/json/jsontext/alias.go @@ -414,8 +414,8 @@ func AppendUnquote[Bytes ~[]byte | ~string](dst []byte, src Bytes) ([]byte, erro // The name of a duplicate JSON object member can be extracted as: // // err := ... -// var serr jsontext.SyntacticError -// if errors.As(err, &serr) && serr.Err == jsontext.ErrDuplicateName { +// serr, ok := errors.AsType[*jsontext.SyntacticError](err) +// if ok && serr.Err == jsontext.ErrDuplicateName { // ptr := serr.JSONPointer // JSON pointer to duplicate name // name := ptr.LastToken() // duplicate name itself // ... diff --git a/vendor/github.com/go-json-experiment/json/jsontext/decode.go b/vendor/github.com/go-json-experiment/json/jsontext/decode.go index 9326ace..de145b4 100644 --- a/vendor/github.com/go-json-experiment/json/jsontext/decode.go +++ b/vendor/github.com/go-json-experiment/json/jsontext/decode.go @@ -792,6 +792,12 @@ func (d *decoderState) CheckNextValue(last bool) error { return nil } +// AtEOF reports whether the decoder is at EOF. +func (d *decoderState) AtEOF() bool { + _, err := d.consumeWhitespace(d.prevEnd) + return err == io.ErrUnexpectedEOF +} + // CheckEOF verifies that the input has no more data. func (d *decoderState) CheckEOF() error { return d.checkEOF(d.prevEnd) diff --git a/vendor/github.com/go-json-experiment/json/jsontext/pools.go b/vendor/github.com/go-json-experiment/json/jsontext/pools.go index cf59d99..75a91ae 100644 --- a/vendor/github.com/go-json-experiment/json/jsontext/pools.go +++ b/vendor/github.com/go-json-experiment/json/jsontext/pools.go @@ -54,6 +54,10 @@ func getBufferedEncoder(opts ...Options) *Encoder { return e } func putBufferedEncoder(e *Encoder) { + if cap(e.s.availBuffer) > 64<<10 { + e.s.availBuffer = nil // avoid pinning arbitrarily large amounts of memory + } + // Recycle large buffers only if sufficiently utilized. // If a buffer is under-utilized enough times sequentially, // then it is discarded, ensuring that a single large buffer @@ -95,9 +99,14 @@ func getStreamingEncoder(w io.Writer, opts ...Options) *Encoder { } } func putStreamingEncoder(e *Encoder) { + if cap(e.s.availBuffer) > 64<<10 { + e.s.availBuffer = nil // avoid pinning arbitrarily large amounts of memory + } if _, ok := e.s.wr.(*bytes.Buffer); ok { + e.s.wr, e.s.Buf = nil, nil // avoid pinning the provided bytes.Buffer bytesBufferEncoderPool.Put(e) } else { + e.s.wr = nil // avoid pinning the provided io.Writer if cap(e.s.Buf) > 64<<10 { e.s.Buf = nil // avoid pinning arbitrarily large amounts of memory } @@ -126,6 +135,7 @@ func getBufferedDecoder(b []byte, opts ...Options) *Decoder { return d } func putBufferedDecoder(d *Decoder) { + d.s.buf = nil // avoid pinning the provided buffer bufferedDecoderPool.Put(d) } @@ -142,8 +152,10 @@ func getStreamingDecoder(r io.Reader, opts ...Options) *Decoder { } func putStreamingDecoder(d *Decoder) { if _, ok := d.s.rd.(*bytes.Buffer); ok { + d.s.rd, d.s.buf = nil, nil // avoid pinning the provided bytes.Buffer bytesBufferDecoderPool.Put(d) } else { + d.s.rd = nil // avoid pinning the provided io.Reader if cap(d.s.buf) > 64<<10 { d.s.buf = nil // avoid pinning arbitrarily large amounts of memory } diff --git a/vendor/github.com/go-json-experiment/json/jsontext/state.go b/vendor/github.com/go-json-experiment/json/jsontext/state.go index 2da28c1..da24d32 100644 --- a/vendor/github.com/go-json-experiment/json/jsontext/state.go +++ b/vendor/github.com/go-json-experiment/json/jsontext/state.go @@ -24,8 +24,8 @@ import ( // The name of a duplicate JSON object member can be extracted as: // // err := ... -// var serr jsontext.SyntacticError -// if errors.As(err, &serr) && serr.Err == jsontext.ErrDuplicateName { +// serr, ok := errors.AsType[*jsontext.SyntacticError](err) +// if ok && serr.Err == jsontext.ErrDuplicateName { // ptr := serr.JSONPointer // JSON pointer to duplicate name // name := ptr.LastToken() // duplicate name itself // ... diff --git a/vendor/github.com/go-json-experiment/json/migrate.sh b/vendor/github.com/go-json-experiment/json/migrate.sh index 9c34f26..576920b 100644 --- a/vendor/github.com/go-json-experiment/json/migrate.sh +++ b/vendor/github.com/go-json-experiment/json/migrate.sh @@ -33,9 +33,10 @@ done sed -i 's/v2[.]struct/json.struct/' $JSONROOT/errors_test.go sed -i 's|jsonv1 "github.com/go-json-experiment/json/v1"|jsonv1 "encoding/json"|' $JSONROOT/bench_test.go -# TODO(go1.25): Remove test that relies on "synctest" that is not available yet. -sed -i '/Issue #73733/,+17d' $JSONROOT/v1/encode_test.go -goimports -w $JSONROOT/v1/encode_test.go +# TODO(go1.26): Remove this rewrite once errors.AsType is in the standard library. +sed -i 's/_, ok := errors\.AsType\[\*SyntacticError\](err)/ok := errors.As(err, new(*SyntacticError))/g' $JSONROOT/jsontext/*.go +sed -i 's/serr, ok := errors\.AsType\[\*json.SemanticError\](err)/var serr *json.SemanticError; ok := errors.As(err, \&serr)/g' $JSONROOT/example_test.go +gofmt -w $JSONROOT/example_test.go # Remove documentation that only makes sense within the stdlib. sed -i '/This package .* is experimental/,+4d' $JSONROOT/doc.go diff --git a/vendor/github.com/go-json-experiment/json/options.go b/vendor/github.com/go-json-experiment/json/options.go index 96758cb..bd38544 100644 --- a/vendor/github.com/go-json-experiment/json/options.go +++ b/vendor/github.com/go-json-experiment/json/options.go @@ -97,9 +97,8 @@ func GetOption[T any](opts Options, setter func(T) Options) (T, bool) { } // DefaultOptionsV2 is the full set of all options that define v2 semantics. -// It is equivalent to all options under [Options], [encoding/json.Options], -// and [encoding/json/jsontext.Options] being set to false or the zero value, -// except for the options related to whitespace formatting. +// It is equivalent to the set of options in [encoding/json.DefaultOptionsV1] +// all being set to false. All other options are not present. func DefaultOptionsV2() Options { return &jsonopts.DefaultOptionsV2 } diff --git a/vendor/modules.txt b/vendor/modules.txt index b69505e..eaa0d2d 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -16,7 +16,7 @@ github.com/fulldump/box/boxopenapi # github.com/fulldump/goconfig v1.7.1 ## explicit; go 1.5 github.com/fulldump/goconfig -# github.com/go-json-experiment/json v0.0.0-20250910080747-cc2cfa0554c3 +# github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e ## explicit; go 1.25 github.com/go-json-experiment/json github.com/go-json-experiment/json/internal From 1ae9d4767d14e8de2d4d9e27628f7a774a9924af Mon Sep 17 00:00:00 2001 From: Alfonso Cantos Date: Thu, 20 Nov 2025 23:20:14 +0100 Subject: [PATCH 4/5] fix: adds cover --- Makefile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Makefile b/Makefile index 06aea55..299c1b6 100644 --- a/Makefile +++ b/Makefile @@ -5,6 +5,9 @@ FLAGS = -ldflags "\ " test: + go test ./... + +cover: GOTOOLCHAIN=go1.25.0+auto go test -cover ./... run: From 86aa334e209db53e85c51e224b4b1f1b543ae1c5 Mon Sep 17 00:00:00 2001 From: Alfonso Cantos Date: Fri, 21 Nov 2025 02:20:25 +0100 Subject: [PATCH 5/5] review --- api/apicollectionv1/0_traverse.go | 2 +- api/apicollectionv1/patch.go | 23 +---- cmd/bench/main.go | 4 +- cmd/bench/main_test.go | 21 ++++ cmd/bench/test_remove.go | 148 +++++++++++++++++++++++++++ collection/collection.go | 159 +++++++++++++++++++----------- 6 files changed, 278 insertions(+), 79 deletions(-) create mode 100644 cmd/bench/main_test.go create mode 100644 cmd/bench/test_remove.go diff --git a/api/apicollectionv1/0_traverse.go b/api/apicollectionv1/0_traverse.go index 2209219..e62d3ac 100644 --- a/api/apicollectionv1/0_traverse.go +++ b/api/apicollectionv1/0_traverse.go @@ -28,7 +28,7 @@ func traverse(requestBody []byte, col *collection.Collection, f func(row *collec return err } - hasFilter := options.Filter != nil && len(options.Filter) > 0 + hasFilter := len(options.Filter) > 0 skip := options.Skip limit := options.Limit diff --git a/api/apicollectionv1/patch.go b/api/apicollectionv1/patch.go index cf91a85..e838b37 100644 --- a/api/apicollectionv1/patch.go +++ b/api/apicollectionv1/patch.go @@ -6,7 +6,6 @@ import ( "io" "net/http" - "github.com/SierraSoftworks/connor" "github.com/fulldump/box" "github.com/fulldump/inceptiondb/collection" @@ -36,26 +35,6 @@ func patch(ctx context.Context, w http.ResponseWriter, r *http.Request) error { traverse(requestBody, col, func(row *collection.Row) bool { - row.PatchMutex.Lock() - defer row.PatchMutex.Unlock() - - hasFilter := patch.Filter != nil && len(patch.Filter) > 0 - if hasFilter { - - rowData := map[string]interface{}{} - json.Unmarshal(row.Payload, &rowData) // todo: handle error here? - - match, err := connor.Match(patch.Filter, rowData) - if err != nil { - // todo: handle error? - // return fmt.Errorf("match: %w", err) - return false - } - if !match { - return false - } - } - err := col.Patch(row, patch.Patch) if err != nil { // TODO: handle err?? @@ -63,6 +42,8 @@ func patch(ctx context.Context, w http.ResponseWriter, r *http.Request) error { return true // todo: OR return false? } + row.PatchMutex.Lock() + defer row.PatchMutex.Unlock() e.Encode(row.Payload) // todo: handle err? return true diff --git a/cmd/bench/main.go b/cmd/bench/main.go index 162767a..7ba425a 100644 --- a/cmd/bench/main.go +++ b/cmd/bench/main.go @@ -27,7 +27,7 @@ func main() { }() c := Config{ - Test: "patch", + Test: "remove", Base: "", N: 1_000_000, Workers: 16, @@ -40,6 +40,8 @@ func main() { TestInsert(c) case "PATCH": TestPatch(c) + case "REMOVE": + TestRemove(c) default: log.Fatalf("Unknown test %s", c.Test) } diff --git a/cmd/bench/main_test.go b/cmd/bench/main_test.go new file mode 100644 index 0000000..953d437 --- /dev/null +++ b/cmd/bench/main_test.go @@ -0,0 +1,21 @@ +package main + +import ( + "testing" +) + +func Test_main(t *testing.T) { + + t.Skip() + + c := Config{ + Test: "remove", + Base: "", + N: 1_000_000, + Workers: 4, + } + + //TestRemove(c) + TestInsert(c) + +} diff --git a/cmd/bench/test_remove.go b/cmd/bench/test_remove.go new file mode 100644 index 0000000..d34d39d --- /dev/null +++ b/cmd/bench/test_remove.go @@ -0,0 +1,148 @@ +package main + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "path" + "strconv" + "strings" + "sync/atomic" + "time" + + "github.com/fulldump/inceptiondb/bootstrap" + "github.com/fulldump/inceptiondb/collection" + "github.com/fulldump/inceptiondb/configuration" +) + +func TestRemove(c Config) { + + createServer := c.Base == "" + + var start, stop func() + var dataDir string + if createServer { + dir, cleanup := TempDir() + dataDir = dir + cleanups = append(cleanups, cleanup) + + conf := configuration.Default() + conf.Dir = dir + c.Base = "http://" + conf.HttpAddr + + start, stop = bootstrap.Bootstrap(conf) + go start() + } + + collectionName := CreateCollection(c.Base) + + transport := &http.Transport{ + MaxConnsPerHost: 1024, + MaxIdleConns: 1024, + MaxIdleConnsPerHost: 1024, + } + defer transport.CloseIdleConnections() + + client := &http.Client{ + Transport: transport, + Timeout: 10 * time.Second, + } + + { + fmt.Println("Preload documents...") + r, w := io.Pipe() + + encoder := json.NewEncoder(w) + go func() { + for i := int64(0); i < c.N; i++ { + encoder.Encode(JSON{ + "id": strconv.FormatInt(i, 10), + "value": 0, + "worker": i % int64(c.Workers), + }) + } + w.Close() + }() + + req, err := http.NewRequest("POST", c.Base+"/v1/collections/"+collectionName+":insert", r) + if err != nil { + fmt.Println("ERROR: new request:", err.Error()) + os.Exit(3) + } + + resp, err := client.Do(req) + if err != nil { + fmt.Println("ERROR: do request:", err.Error()) + os.Exit(4) + } + io.Copy(io.Discard, resp.Body) + } + + req, err := http.NewRequest("POST", c.Base+"/v1/collections/"+collectionName+":createIndex", strings.NewReader(` + { + "fields": ["id"], + "name": "my-index", + "type": "btree" + }`)) + if err != nil { + fmt.Println("ERROR: new request:", err.Error()) + os.Exit(3) + } + + resp, err := client.Do(req) + if err != nil { + fmt.Println("ERROR: do request:", err.Error()) + os.Exit(4) + } + io.Copy(io.Discard, resp.Body) + resp.Body.Close() + if resp.StatusCode != http.StatusCreated { + fmt.Println("ERROR: bad status:", resp.Status) + os.Exit(5) + } + + removeURL := fmt.Sprintf("%s/v1/collections/%s:remove", c.Base, collectionName) + + t0 := time.Now() + worker := int64(-1) + Parallel(c.Workers, func() { + w := atomic.AddInt64(&worker, 1) + + body := fmt.Sprintf(`{"filter":{"worker":%d},"limit":-1}`, w) + req, err := http.NewRequest(http.MethodPost, removeURL, strings.NewReader(body)) + if err != nil { + fmt.Println("ERROR: new request:", err.Error()) + } + req.Header.Set("Content-Type", "application/json") + + resp, err := client.Do(req) + if err != nil { + fmt.Println("ERROR: do request:", err.Error()) + } + io.Copy(io.Discard, resp.Body) + resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + fmt.Println("ERROR: bad status:", resp.Status) + } + }) + + took := time.Since(t0) + fmt.Println("sent:", c.N) + fmt.Println("took:", took) + fmt.Printf("Throughput: %.2f rows/sec\n", float64(c.N)/took.Seconds()) + + if !createServer { + return + } + + stop() // Stop the server + + t1 := time.Now() + collection.OpenCollection(path.Join(dataDir, collectionName)) + tookOpen := time.Since(t1) + fmt.Println("open took:", tookOpen) + fmt.Printf("Throughput Open: %.2f rows/sec\n", float64(c.N)/tookOpen.Seconds()) +} diff --git a/collection/collection.go b/collection/collection.go index aa64340..46e8b71 100644 --- a/collection/collection.go +++ b/collection/collection.go @@ -30,6 +30,9 @@ type Collection struct { Defaults map[string]any Count int64 encoderMutex *sync.Mutex + commandQueue chan *Command + wg sync.WaitGroup + closeOnce sync.Once } type collectionIndex struct { @@ -85,6 +88,7 @@ func OpenCollection(filename string) (*Collection, error) { Filename: filename, Indexes: map[string]*collectionIndex{}, encoderMutex: &sync.Mutex{}, + commandQueue: make(chan *Command, 10000), } j := jsontext.NewDecoder(f, @@ -179,10 +183,43 @@ func OpenCollection(filename string) (*Collection, error) { collection.buffer = bufio.NewWriterSize(collection.file, 16*1024*1024) + collection.wg.Add(1) + go collection.runCommandWriter() + return collection, nil } +func (c *Collection) runCommandWriter() { + defer c.wg.Done() + + for command := range c.commandQueue { + em := encPool.Get().(*EncoderMachine) + em.Buffer.Reset() + + err := json2.MarshalEncode(em.Enc2, command) + if err != nil { + fmt.Printf("ERROR: encode command: %s\n", err.Error()) + encPool.Put(em) + continue + } + + b := em.Buffer.Bytes() + c.encoderMutex.Lock() + c.buffer.Write(b) + c.encoderMutex.Unlock() + + encPool.Put(em) + } +} + func (c *Collection) addRow(payload json.RawMessage) (*Row, error) { + c.rowsMutex.Lock() + defer c.rowsMutex.Unlock() + + return c.addRowLocked(payload) +} + +func (c *Collection) addRowLocked(payload json.RawMessage) (*Row, error) { row := &Row{ Payload: payload, @@ -193,10 +230,8 @@ func (c *Collection) addRow(payload json.RawMessage) (*Row, error) { return nil, err } - c.rowsMutex.Lock() row.I = len(c.Rows) c.Rows = append(c.Rows, row) - c.rowsMutex.Unlock() return row, nil } @@ -240,12 +275,6 @@ func (c *Collection) Insert(item map[string]any) (*Row, error) { return nil, fmt.Errorf("json encode payload: %w", err) } - // Add row - row, err := c.addRow(payload) - if err != nil { - return nil, err - } - // Persist command := &Command{ Name: "insert", @@ -255,6 +284,15 @@ func (c *Collection) Insert(item map[string]any) (*Row, error) { Payload: payload, } + c.rowsMutex.Lock() + defer c.rowsMutex.Unlock() + + // Add row + row, err := c.addRowLocked(payload) + if err != nil { + return nil, err + } + err = c.EncodeCommand(command) if err != nil { return nil, err @@ -446,28 +484,24 @@ func lockBlock(m *sync.Mutex, f func() error) error { func (c *Collection) removeByRow(row *Row, persist bool) error { // todo: rename to 'removeRow' - var i int - err := lockBlock(c.rowsMutex, func() error { - i = row.I - if len(c.Rows) <= i { - return fmt.Errorf("row %d does not exist", i) - } + c.rowsMutex.Lock() + defer c.rowsMutex.Unlock() - err := indexRemove(c.Indexes, row) - if err != nil { - return fmt.Errorf("could not free index") - } + i := row.I + if len(c.Rows) <= i { + return fmt.Errorf("row %d does not exist", i) + } - last := len(c.Rows) - 1 - c.Rows[i] = c.Rows[last] - c.Rows[i].I = i - c.Rows = c.Rows[:last] - return nil - }) + err := indexRemove(c.Indexes, row) if err != nil { - return err + return fmt.Errorf("could not free index") } + last := len(c.Rows) - 1 + c.Rows[i] = c.Rows[last] + c.Rows[i].I = i + c.Rows = c.Rows[:last] + if !persist { return nil } @@ -496,7 +530,12 @@ func (c *Collection) Patch(row *Row, patch interface{}) error { func (c *Collection) patchByRow(row *Row, patch interface{}, persist bool) error { // todo: rename to 'patchRow' - originalValue, err := decodeJSONValue(row.Payload) + // 1. Calculate new state (expensive, no lock) + row.PatchMutex.Lock() + originalPayload := row.Payload + row.PatchMutex.Unlock() + + originalValue, err := decodeJSONValue(originalPayload) if err != nil { return fmt.Errorf("decode row payload: %w", err) } @@ -520,13 +559,33 @@ func (c *Collection) patchByRow(row *Row, patch interface{}, persist bool) error return fmt.Errorf("marshal payload: %w", err) } + var diffValue interface{} + var hasDiff bool + if persist { + diffValue, hasDiff = createMergeDiff(originalValue, newValue) + if !hasDiff { + return nil + } + } + + // 2. Update state (cheap, lock) + c.rowsMutex.Lock() + defer c.rowsMutex.Unlock() + + // Validate row is still valid + if row.I >= len(c.Rows) || c.Rows[row.I] != row { + return fmt.Errorf("row modified or removed during patch") + } + // index update err = indexRemove(c.Indexes, row) if err != nil { return fmt.Errorf("indexRemove: %w", err) } + row.PatchMutex.Lock() row.Payload = newPayload + row.PatchMutex.Unlock() err = indexInsert(c.Indexes, row) if err != nil { @@ -537,11 +596,6 @@ func (c *Collection) patchByRow(row *Row, patch interface{}, persist bool) error return nil } - diffValue, hasDiff := createMergeDiff(originalValue, newValue) - if !hasDiff { - return nil - } - // Persist payload, err := json.Marshal(map[string]interface{}{ "i": row.I, @@ -786,17 +840,31 @@ func cloneJSONArray(values []interface{}) []interface{} { return cloned } +func (c *Collection) EncodeCommand(command *Command) error { + c.commandQueue <- command + return nil +} + func (c *Collection) Close() error { - { + c.closeOnce.Do(func() { + close(c.commandQueue) + c.wg.Wait() + }) + + if c.buffer != nil { err := c.buffer.Flush() if err != nil { return err } } - err := c.file.Close() - c.file = nil - return err + if c.file != nil { + err := c.file.Close() + c.file = nil + return err + } + + return nil } func (c *Collection) Drop() error { @@ -849,24 +917,3 @@ func (c *Collection) dropIndex(name string, persist bool) error { return c.EncodeCommand(command) } - -func (c *Collection) EncodeCommand(command *Command) error { - - em := encPool.Get().(*EncoderMachine) - defer encPool.Put(em) - em.Buffer.Reset() - - // err := em.Enc.Encode(command) - err := json2.MarshalEncode(em.Enc2, command) - // err := json2.MarshalWrite(em.Buffer, command) - if err != nil { - return err - } - - b := em.Buffer.Bytes() - c.encoderMutex.Lock() - c.buffer.Write(b) - // c.file.Write(b) - c.encoderMutex.Unlock() - return nil -}