diff --git a/go.work.sum b/go.work.sum index b32d3172de..a58792ad92 100644 --- a/go.work.sum +++ b/go.work.sum @@ -166,6 +166,7 @@ github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1 h1:xvqufLtNVwAhN8NMyWklVgxnWohi+wtMGQMhtxexlm0= github.com/envoyproxy/go-control-plane v0.11.1/go.mod h1:uhMcXKCQMEJHiAb0w+YGefQLaTEw+YhGluxZkrTmD0g= +github.com/envoyproxy/go-control-plane v0.12.0/go.mod h1:ZBTaoJ23lqITozF0M6G4/IragXCQKCnYbmlmtHvwRG0= github.com/envoyproxy/protoc-gen-validate v0.1.0 h1:EQciDnbrYxy13PgWoY8AqoxGiPrpgBZ1R8UNe3ddc+A= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/garyburd/redigo v1.1.1-0.20170914051019-70e1b1943d4f h1:Sk0u0gIncQaQD23zAoAZs2DNi2u2l5UTLi4CmCBL5v8= @@ -192,6 +193,7 @@ github.com/gobuffalo/syncx v0.0.0-20190224160051-33c29581e754 h1:tpom+2CJmpzAWj5 github.com/goccy/go-json v0.9.11 h1:/pAaQDLHEoCq/5FFmSKBswWmK6H0e8g4159Kc/X/nqk= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= github.com/golang/glog v1.1.2/go.mod h1:zR+okUeTbrL6EL3xHUDxZuEtGv04p5shwip1+mL/rLQ= +github.com/golang/glog v1.2.0/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= github.com/golang/lint v0.0.0-20170918230701-e5d664eb928e h1:ior8LN6127GsA53E9mD9nH/oP/LVbJplmLH5V8o+/Uk= github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= diff --git a/server/e2e/gql_nlslayer_test.go b/server/e2e/gql_nlslayer_test.go index 13b9f21021..f7a34e76b3 100644 --- a/server/e2e/gql_nlslayer_test.go +++ b/server/e2e/gql_nlslayer_test.go @@ -672,19 +672,23 @@ func moveInfoboxBlock(e *httpexpect.Expect, layerId, infoboxBlockId string, inde return requestBody, res, res.Path("$.data.moveNLSInfoboxBlock.infoboxBlockId").Raw().(string) } -func TestInfoboxBlocksCRUD(t *testing.T) { - mr, err := miniredis.Run() - if err != nil { - t.Fatal(err) +func infoboxBlocksCRUD(t *testing.T, isUseRedis bool) { + redisAddress := "" + if isUseRedis { + mr, err := miniredis.Run() + if err != nil { + t.Fatal(err) + } + defer mr.Close() + redisAddress = mr.Addr() } - defer mr.Close() e := StartServer(t, &config.Config{ Origins: []string{"https://example.com"}, AuthSrv: config.AuthSrvConfig{ Disabled: true, }, - RedisHost: mr.Addr(), + RedisHost: redisAddress, }, true, baseSeeder) pId := createProject(e) @@ -743,6 +747,14 @@ func TestInfoboxBlocksCRUD(t *testing.T) { Path("$.data.node.newLayers[0].infobox.blocks").Equal([]any{}) } +func TestInfoboxBlocksCRUD(t *testing.T) { + infoboxBlocksCRUD(t, false) +} + +func TestInfoboxBlocksCRUDWithRedis(t *testing.T) { + infoboxBlocksCRUD(t, true) +} + func addCustomProperties( e *httpexpect.Expect, layerId string, diff --git a/server/internal/usecase/interactor/common.go b/server/internal/usecase/interactor/common.go index ce4a673c93..46ff53e8a8 100644 --- a/server/internal/usecase/interactor/common.go +++ b/server/internal/usecase/interactor/common.go @@ -48,7 +48,7 @@ func NewContainer(r *repo.Container, g *gateway.Container, Plugin: NewPlugin(r, g), Policy: NewPolicy(r), Project: NewProject(r, g), - Property: NewProperty(r, g), + Property: NewProperty(r, g, redisAdapter), Published: published, Scene: NewScene(r, g), Tag: NewTag(r), diff --git a/server/internal/usecase/interactor/property.go b/server/internal/usecase/interactor/property.go index 4d83425663..5b50daca6b 100644 --- a/server/internal/usecase/interactor/property.go +++ b/server/internal/usecase/interactor/property.go @@ -4,13 +4,16 @@ import ( "context" "errors" + "github.com/go-redis/redis/v8" "github.com/reearth/reearth/server/internal/usecase" "github.com/reearth/reearth/server/internal/usecase/gateway" "github.com/reearth/reearth/server/internal/usecase/interfaces" "github.com/reearth/reearth/server/internal/usecase/repo" "github.com/reearth/reearth/server/pkg/id" "github.com/reearth/reearth/server/pkg/property" + "github.com/reearth/reearth/server/pkg/value" "github.com/reearth/reearthx/usecasex" + "github.com/vmihailenco/msgpack/v5" ) type Property struct { @@ -24,9 +27,10 @@ type Property struct { assetRepo repo.Asset file gateway.File transaction usecasex.Transaction + redis gateway.RedisGateway } -func NewProperty(r *repo.Container, gr *gateway.Container) interfaces.Property { +func NewProperty(r *repo.Container, gr *gateway.Container, redis gateway.RedisGateway) interfaces.Property { return &Property{ commonSceneLock: commonSceneLock{sceneLockRepo: r.SceneLock}, propertyRepo: r.Property, @@ -37,6 +41,7 @@ func NewProperty(r *repo.Container, gr *gateway.Container) interfaces.Property { assetRepo: r.Asset, transaction: r.Transaction, file: gr.File, + redis: redis, } } @@ -88,10 +93,20 @@ func (i *Property) UpdateValue(ctx context.Context, inp interfaces.UpdatePropert } }() - p, err = i.propertyRepo.FindByID(ctx, inp.PropertyID) + propertyCache, err := getPropertyFromCache(ctx, i.redis, property.PropertyCacheKey(inp.PropertyID)) if err != nil { return nil, nil, nil, nil, err } + + if propertyCache == nil { + p, err = i.propertyRepo.FindByID(ctx, inp.PropertyID) + if err != nil { + return nil, nil, nil, nil, err + } + } else { + p = propertyCache + } + if err := i.CanWriteScene(p.Scene(), operator); err != nil { return nil, nil, nil, nil, err } @@ -116,6 +131,12 @@ func (i *Property) UpdateValue(ctx context.Context, inp interfaces.UpdatePropert } tx.Commit() + + err = setPropertyToCache(ctx, i.redis, property.PropertyCacheKey(p.ID()), p) + if err != nil { + return nil, nil, nil, nil, err + } + return p, pgl, pg, field, nil } @@ -432,3 +453,182 @@ func (i *Property) UpdateItems(ctx context.Context, inp interfaces.UpdatePropert return p, nil } + +type PropertyForRedis struct { + ID string `msgpack:"ID"` + Scene string `msgpack:"Scene"` + Schema string `msgpack:"Schema"` + Items []GroupForRedis `msgpack:"Items"` +} + +type GroupForRedis struct { + ID string `msgpack:"ID"` + SchemaGroup string `msgpack:"SchemaGroup"` + Fields []*FieldForRedis `msgpack:"Fields"` +} + +type FieldForRedis struct { + Field string `msgpack:"Field"` + Links *LinksForRedis `msgpack:"Links,omitempty"` + V *OptionalValueForRedis `msgpack:"V,omitempty"` +} + +type LinksForRedis struct { + Links []*LinkForRedis `msgpack:"Links"` +} + +type LinkForRedis struct { + Dataset *string `msgpack:"Dataset,omitempty"` + Schema *string `msgpack:"Schema,omitempty"` + Field *string `msgpack:"Field,omitempty"` +} + +type OptionalValueForRedis struct { + Type string `msgpack:"Type"` + Value *ValueForRedis `msgpack:"Value,omitempty"` +} + +type ValueForRedis struct { + P map[string]interface{} `msgpack:"P"` + V interface{} `msgpack:"V"` + T string `msgpack:"T"` +} + +func getPropertyFromCache(ctx context.Context, redisClient any, cacheKey string) (*property.Property, error) { + redisAdapter, ok := checkRedisClient(redisClient) + if !ok { + return nil, nil + } + + val, err := redisAdapter.GetValue(ctx, cacheKey) + if err != nil { + if err == redis.Nil { + return nil, nil + } + return nil, err + } + + var p PropertyForRedis + if err := msgpack.Unmarshal([]byte(val), &p); err != nil { + return nil, err + } + + propertyDomain, err := convertPropertyFromRedis(p) + if err != nil { + return nil, err + } + + return propertyDomain, nil +} + +func setPropertyToCache(ctx context.Context, redisClient any, cacheKey string, data *property.Property) error { + redisAdapter, ok := checkRedisClient(redisClient) + if !ok { + return nil + } + + propertyForRedis := convertPropertyToRedis(data) + + serializedData, err := msgpack.Marshal(propertyForRedis) + if err != nil { + return err + } + + return redisAdapter.SetValue(ctx, cacheKey, serializedData) +} + +func convertPropertyToRedis(p *property.Property) PropertyForRedis { + ptr := &property.Pointer{} + fields := p.Items()[0].Fields(ptr) + + fieldsForRedis := make([]*FieldForRedis, 0, len(fields)) + for _, field := range fields { + valueForRedis := ValueForRedis{ + P: map[string]interface{}{}, + V: field.TypeAndValue().Value().Value(), + T: string(field.TypeAndValue().Value().Type()), + } + + optionalValueForRedis := OptionalValueForRedis{ + Type: string(field.TypeAndValue().Type()), + Value: &valueForRedis, + } + + fieldForRedis := FieldForRedis{ + Field: field.Field().String(), + Links: nil, + V: &optionalValueForRedis, + } + + fieldsForRedis = append(fieldsForRedis, &fieldForRedis) + } + + groupForRedis := GroupForRedis{ + ID: p.Items()[0].ID().String(), + SchemaGroup: p.Items()[0].SchemaGroup().String(), + Fields: fieldsForRedis, + } + + propertyForRedis := PropertyForRedis{ + ID: p.ID().String(), + Scene: p.Scene().String(), + Schema: p.Schema().String(), + Items: []GroupForRedis{groupForRedis}, + } + + return propertyForRedis +} + +func convertPropertyFromRedis(p PropertyForRedis) (*property.Property, error) { + + fieldsDomain := make([]*property.Field, 0, len(p.Items[0].Fields)) + for _, field := range p.Items[0].Fields { + + var v interface{} + if field.Field == "padding" { + m := field.V.Value.V.(map[string]interface{}) + v = property.Spacing{ + Top: m["Top"].(float64), + Bottom: m["Bottom"].(float64), + Left: m["Left"].(float64), + Right: m["Right"].(float64), + } + } else { + v = field.V.Value.V + } + + valueDomain := value.New( + property.DefaultTypes(), + v, + value.Type(field.V.Value.T), + ) + + optionalValueDomain := property.NewOptionalValue( + property.ValueType(field.V.Type), + property.NewValue(valueDomain), + ) + + fieldDomain := property.NewFieldDomain( + property.FieldID(field.Field), + nil, + optionalValueDomain, + ) + + fieldsDomain = append(fieldsDomain, fieldDomain) + } + + groupDomain := property.NewGroup(). + ID(property.MustItemID(p.Items[0].ID)). + SchemaGroup(property.SchemaGroupID(p.Items[0].SchemaGroup)). + Fields(fieldsDomain). + MustBuild() + + propertyDomain := property.New(). + ID(property.MustID(p.ID)). + Scene(property.MustSceneID(p.Scene)). + Schema(property.MustSchemaID(p.Schema)). + Items([]property.Item{groupDomain}). + MustBuild() + + return propertyDomain, nil +} diff --git a/server/pkg/property/field.go b/server/pkg/property/field.go index c83c66ff85..bef2d27750 100644 --- a/server/pkg/property/field.go +++ b/server/pkg/property/field.go @@ -20,6 +20,14 @@ type Field struct { v *OptionalValue } +func NewFieldDomain(field FieldID, links *Links, v *OptionalValue) *Field { + return &Field{ + field: field, + links: links, + v: v, + } +} + func (p *Field) Clone() *Field { if p == nil { return nil diff --git a/server/pkg/property/property.go b/server/pkg/property/property.go index 762401341e..2705077aef 100644 --- a/server/pkg/property/property.go +++ b/server/pkg/property/property.go @@ -641,3 +641,7 @@ func (p *Property) updateSchema(s SchemaID) bool { func (p *Property) SetSchema(schema SchemaID) { p.schema = schema.Clone() } + +func PropertyCacheKey(id ID) string { + return fmt.Sprintf("Property:%s", id) +} diff --git a/server/pkg/property/value.go b/server/pkg/property/value.go index 1f33d9da57..94501f587e 100644 --- a/server/pkg/property/value.go +++ b/server/pkg/property/value.go @@ -36,6 +36,16 @@ var types = value.TypePropertyMap{ value.Type(ValueTypeTimeline): &typePropertyTimeline{}, } +func NewValue(v *value.Value) *Value { + if v == nil { + return nil + } + + return &Value{ + v: *v, + } +} + func (vt ValueType) Valid() bool { if _, ok := types[value.Type(vt)]; ok { return true @@ -245,3 +255,7 @@ func ValueFromStringOrNumber(s string) *Value { return ValueTypeString.ValueFrom(s) } + +func DefaultTypes() value.TypePropertyMap { + return types +} diff --git a/server/pkg/value/value.go b/server/pkg/value/value.go index 5e3f71c98a..63aa48fd71 100644 --- a/server/pkg/value/value.go +++ b/server/pkg/value/value.go @@ -10,6 +10,21 @@ type Value struct { t Type } +func New( + p TypePropertyMap, + v interface{}, + t Type, +) *Value { + if t == TypeUnknown { + return nil + } + return &Value{ + p: p, + v: v, + t: t, + } +} + func (v *Value) IsEmpty() bool { return v == nil || v.t == TypeUnknown || v.v == nil }