Skip to content

Commit 80784a7

Browse files
authored
feat(go): Implement compatible mode with metashare mode (#2607)
## Why? Type-forward and backward-compatible serialization are crucial for online services where different services update their data schemas and deploy at different times. This ensures that a service running an older schema can still communicate with a service using a newer one, and vice versa.This PR is part of the work of it. The previous PR is #2554 , and following pr are still work in process. ## What does this PR do? This PR implements a metashare mode for the fory-go. Specifically, it: Provides a user option to enable compatible mode. If compatible mode is enabled, the serialization process not only serializes the data but also its detailed type information into a binary format. The reading peer, if compatible mode is enabled, will first attempt to read this type information before deserializing the data. This allows it to handle data serialized with a different schema. This implementation is heavily inspired by the existing Java and Python implementations, ensuring consistency across framework. All progress and todos: - [x] metadata encoding and decoding - [x] Integrate into the main flow to enable metadata sharing - [ ] Add support for collection types (map, slice, set, etc.) - [ ] Add support for compressed type definitions - [ ] Test with python ## Related issues #2192 ## Does this PR introduce any user-facing change? <!-- If any user-facing interface changes, please [open an issue](https://github.com/apache/fory/issues/new/choose) describing the need to do so and update the document if necessary. Delete section if not applicable. --> - [x] Does this PR introduce any public API change? Yes. - [x] Does this PR introduce any binary protocol compatibility change? No. ## Benchmark <!-- When the PR has an impact on performance (if you don't know whether the PR will have an impact on performance, you can submit the PR first, and if it will have impact on performance, the code reviewer will explain it), be sure to attach a benchmark data here. Delete section if not applicable. -->
1 parent 4c245dc commit 80784a7

13 files changed

+662
-43
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,4 @@ MODULE.bazel
4040
MODULE.bazel.lock
4141
.DS_Store
4242
**/.DS_Store
43+
.vscode/

go/README.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,38 @@ fory --force -file <your file>
146146
- Does generated code work across Go versions? Yes, it’s plain Go code; keep your toolchain consistent in CI.
147147
- Can I mix generated and non-generated structs? Yes, adoption is incremental and per file.
148148

149+
## Configuration Options
150+
151+
Fory Go supports several configuration options through the functional options pattern:
152+
153+
### Compatible Mode (Metashare)
154+
155+
Compatible mode enables meta information sharing, which allows for schema evolution:
156+
157+
```go
158+
// Enable compatible mode with metashare
159+
fory := NewForyWithOptions(WithCompatible(true))
160+
```
161+
162+
### Reference Tracking
163+
164+
Enable reference tracking:
165+
166+
```go
167+
fory := NewForyWithOptions(WithRefTracking(true))
168+
```
169+
170+
### Combined Options
171+
172+
You can combine multiple options:
173+
174+
```go
175+
fory := NewForyWithOptions(
176+
WithCompatible(true),
177+
WithRefTracking(true),
178+
)
179+
```
180+
149181
## How to test
150182

151183
```bash

go/fory/context.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
package fory
19+
20+
import "reflect"
21+
22+
// MetaContext used to share data across multiple serialization calls
23+
type MetaContext struct {
24+
// typeMap make sure each type just fully serialize once, the following serialization will use the index
25+
typeMap map[reflect.Type]uint32
26+
// record typeDefs need to be serialized during one serialization
27+
writingTypeDefs []*TypeDef
28+
// read from peer
29+
readTypeInfos []TypeInfo
30+
// scopedMetaShareEnabled controls whether meta sharing is scoped to single serialization
31+
scopedMetaShareEnabled bool
32+
}
33+
34+
// NewMetaContext creates a new MetaContext
35+
func NewMetaContext(scopedMetaShareEnabled bool) *MetaContext {
36+
return &MetaContext{
37+
typeMap: make(map[reflect.Type]uint32),
38+
scopedMetaShareEnabled: scopedMetaShareEnabled,
39+
}
40+
}
41+
42+
// resetRead resets the read-related state of the MetaContext
43+
func (mc *MetaContext) resetRead() {
44+
if mc.scopedMetaShareEnabled {
45+
mc.readTypeInfos = mc.readTypeInfos[:0] // Reset slice but keep capacity
46+
} else {
47+
mc.readTypeInfos = nil
48+
}
49+
}
50+
51+
// resetWrite resets the write-related state of the MetaContext
52+
func (mc *MetaContext) resetWrite() {
53+
if mc.scopedMetaShareEnabled {
54+
for k := range mc.typeMap {
55+
delete(mc.typeMap, k)
56+
}
57+
mc.writingTypeDefs = mc.writingTypeDefs[:0] // Reset slice but keep capacity
58+
} else {
59+
mc.typeMap = nil
60+
mc.writingTypeDefs = nil
61+
}
62+
}
63+
64+
// SetScopedMetaShareEnabled sets the scoped meta share mode
65+
func (mc *MetaContext) SetScopedMetaShareEnabled(enabled bool) {
66+
mc.scopedMetaShareEnabled = enabled
67+
}
68+
69+
// IsScopedMetaShareEnabled returns whether scoped meta sharing is enabled
70+
func (mc *MetaContext) IsScopedMetaShareEnabled() bool {
71+
return mc.scopedMetaShareEnabled
72+
}

go/fory/fory.go

Lines changed: 114 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,72 @@ import (
2424
"sync"
2525
)
2626

27-
func NewFory(referenceTracking bool) *Fory {
27+
// Option represents a configuration option for Fory instances.
28+
// This follows the functional options pattern, allowing flexible configuration
29+
// by passing variadic option functions to constructors like NewForyWithOptions.
30+
type Option func(*Fory)
31+
32+
// WithCompatible sets the compatible mode for the Fory instance.
33+
// When compatible=true, scoped meta sharing is automatically enabled.
34+
func WithCompatible(compatible bool) Option {
35+
return func(f *Fory) {
36+
f.compatible = compatible
37+
if compatible {
38+
f.metaContext = NewMetaContext(true) // Enable scoped meta sharing
39+
} else {
40+
f.metaContext = nil
41+
}
42+
}
43+
}
44+
45+
// WithRefTracking sets the reference tracking mode for the Fory instance
46+
func WithRefTracking(refTracking bool) Option {
47+
return func(f *Fory) {
48+
f.refTracking = refTracking
49+
}
50+
}
51+
52+
// WithScopedMetaShare sets the scoped meta share mode for the Fory instance.
53+
// Note: Compatible mode automatically enables scoped meta sharing.
54+
// This option is mainly used for fine-grained control when compatible mode is already enabled.
55+
func WithScopedMetaShare(enabled bool) Option {
56+
return func(f *Fory) {
57+
if f.metaContext == nil {
58+
f.metaContext = NewMetaContext(enabled)
59+
} else {
60+
f.metaContext.SetScopedMetaShareEnabled(enabled)
61+
}
62+
}
63+
}
64+
65+
func NewFory(refTracking bool) *Fory {
66+
return NewForyWithOptions(WithRefTracking(refTracking))
67+
}
68+
69+
// NewForyWithOptions creates a Fory instance with configurable options
70+
func NewForyWithOptions(options ...Option) *Fory {
2871
fory := &Fory{
29-
refResolver: newRefResolver(referenceTracking),
30-
referenceTracking: referenceTracking,
31-
language: XLANG,
32-
buffer: NewByteBuffer(nil),
72+
refResolver: nil,
73+
refTracking: false,
74+
language: XLANG,
75+
buffer: NewByteBuffer(nil),
76+
compatible: false,
3377
}
34-
// Create a new type resolver for this instance
78+
79+
// Apply options
80+
for _, option := range options {
81+
option(fory)
82+
}
83+
84+
// Create a new type resolver for this instance but copy generated serializers from global resolver
3585
fory.typeResolver = newTypeResolver(fory)
86+
87+
// Initialize meta context if compatible mode is enabled
88+
if fory.compatible {
89+
fory.metaContext = NewMetaContext(true)
90+
}
91+
92+
fory.refResolver = newRefResolver(fory.refTracking)
3693
return fory
3794
}
3895

@@ -105,14 +162,16 @@ const (
105162
const MAGIC_NUMBER int16 = 0x62D4
106163

107164
type Fory struct {
108-
typeResolver *typeResolver
109-
refResolver *RefResolver
110-
referenceTracking bool
111-
language Language
112-
bufferCallback BufferCallback
113-
peerLanguage Language
114-
buffer *ByteBuffer
115-
buffers []*ByteBuffer
165+
typeResolver *typeResolver
166+
refResolver *RefResolver
167+
refTracking bool
168+
language Language
169+
bufferCallback BufferCallback
170+
peerLanguage Language
171+
buffer *ByteBuffer
172+
buffers []*ByteBuffer
173+
compatible bool
174+
metaContext *MetaContext
116175
}
117176

118177
func (f *Fory) RegisterTagType(tag string, v interface{}) error {
@@ -246,7 +305,18 @@ func (f *Fory) readLength(buffer *ByteBuffer) int {
246305
}
247306

248307
func (f *Fory) WriteReferencable(buffer *ByteBuffer, value reflect.Value) error {
249-
return f.writeReferencableBySerializer(buffer, value, nil)
308+
metaOffset := buffer.writerIndex
309+
if f.compatible {
310+
buffer.WriteInt32(-1)
311+
}
312+
if err := f.writeReferencableBySerializer(buffer, value, nil); err != nil {
313+
return err
314+
}
315+
if f.compatible && f.metaContext != nil && len(f.metaContext.writingTypeDefs) > 0 {
316+
buffer.PutInt32(metaOffset, int32(buffer.writerIndex-metaOffset-4))
317+
f.typeResolver.writeTypeDefs(buffer)
318+
}
319+
return nil
250320
}
251321

252322
func (f *Fory) writeReferencableBySerializer(buffer *ByteBuffer, value reflect.Value, serializer Serializer) error {
@@ -367,6 +437,19 @@ func (f *Fory) Deserialize(buf *ByteBuffer, v interface{}, buffers []*ByteBuffer
367437
"produced with buffer_callback null")
368438
}
369439
}
440+
441+
if f.compatible {
442+
typeDefOffset := buf.ReadInt32()
443+
if typeDefOffset >= 0 {
444+
save := buf.readerIndex
445+
buf.SetReaderIndex(save + int(typeDefOffset))
446+
if err := f.typeResolver.readTypeDefs(buf); err != nil {
447+
return fmt.Errorf("failed to read typeDefs: %w", err)
448+
}
449+
buf.SetReaderIndex(save)
450+
}
451+
}
452+
370453
if isXLangFlag {
371454
return f.ReadReferencable(buf, reflect.ValueOf(v).Elem())
372455
} else {
@@ -475,11 +558,25 @@ func (f *Fory) Reset() {
475558
func (f *Fory) resetWrite() {
476559
f.typeResolver.resetWrite()
477560
f.refResolver.resetWrite()
561+
if f.metaContext != nil {
562+
if f.metaContext.IsScopedMetaShareEnabled() {
563+
f.metaContext.resetWrite()
564+
} else {
565+
f.metaContext = nil
566+
}
567+
}
478568
}
479569

480570
func (f *Fory) resetRead() {
481571
f.typeResolver.resetRead()
482572
f.refResolver.resetRead()
573+
if f.metaContext != nil {
574+
if f.metaContext.IsScopedMetaShareEnabled() {
575+
f.metaContext.resetRead()
576+
} else {
577+
f.metaContext = nil
578+
}
579+
}
483580
}
484581

485582
// methods for configure fory.
@@ -488,6 +585,6 @@ func (f *Fory) SetLanguage(language Language) {
488585
f.language = language
489586
}
490587

491-
func (f *Fory) SetReferenceTracking(referenceTracking bool) {
492-
f.referenceTracking = referenceTracking
588+
func (f *Fory) SetRefTracking(refTracking bool) {
589+
f.refTracking = refTracking
493590
}

0 commit comments

Comments
 (0)