feat(os/gcfg): Add file watcher with custom callback support (#4446)

为`gcfg`添加配置文件变更自定义回调,实现了`WatcherAdapter`接口,以下是`AdapterFile`的用法
test.yaml
```
b: "b"

```
```
package main

import (
	"fmt"
	"github.com/gogf/gf/v2/frame/g"
	"github.com/gogf/gf/v2/os/gcfg"
	"github.com/gogf/gf/v2/os/gctx"
)

func main() {
	ctx := gctx.New()
	file, _ := gcfg.NewAdapterFile("test.yaml")
	file.Data(ctx)
	file.AddWatcher("test", func() {
		value := file.MustGet(ctx, "b")
		fmt.Println(value.String())
	})
	server := g.Server()
	server.Run()
}
```
使用`g`和默认配置文件
```
	file := g.Cfg().GetAdapter().(*gcfg.AdapterFile)
	file.AddWatcher("test", func() {

	})
	file := g.Cfg().GetAdapter().(*gcfg.AdapterFile)
	file.RemoveWatcher("test")
```

注意:由于`gf`的`AdapterFile`使用的监听到文件变化删除缓存下一次重新初始化的懒加载方案,所有除了默认加载的`config.xxx`文件外,自定义的配置文件像`test.yaml`之类的都需要在`AddWatcher`前主动读取一次数据进行初始化监听(
`g.Cfg("test").Data(ctx)`)

---------

Co-authored-by: hailaz <739476267@qq.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Hunk Zhu <hunk@joy999.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Lance Add
2025-10-15 16:59:52 +08:00
committed by GitHub
parent 2744fe2212
commit ac3efe5a00
22 changed files with 1673 additions and 53 deletions

View File

@ -22,6 +22,12 @@ import (
"github.com/gogf/gf/v2/util/gconv"
)
var (
// Compile-time checking for interface implementation.
_ gcfg.Adapter = (*Client)(nil)
_ gcfg.WatcherAdapter = (*Client)(nil)
)
// Config is the configuration object for apollo client.
type Config struct {
AppID string `v:"required"` // See apolloConfig.Config.
@ -38,9 +44,10 @@ type Config struct {
// Client implements gcfg.Adapter implementing using apollo service.
type Client struct {
config Config // Config object when created.
client agollo.Client // Apollo client.
value *g.Var // Configmap content cached. It is `*gjson.Json` value internally.
config Config // Config object when created.
client agollo.Client // Apollo client.
value *g.Var // Configmap content cached. It is `*gjson.Json` value internally.
watchers *gcfg.WatcherRegistry // Watchers for watching file changes.
}
// New creates and returns gcfg.Adapter implementing using apollo service.
@ -54,8 +61,9 @@ func New(ctx context.Context, config Config) (adapter gcfg.Adapter, err error) {
config.NamespaceName = storage.GetDefaultNamespace()
}
client := &Client{
config: config,
value: g.NewVar(nil, true),
config: config,
value: g.NewVar(nil, true),
watchers: gcfg.NewWatcherRegistry(),
}
// Apollo client.
client.client, err = agollo.StartWithConfig(func() (*apolloConfig.AppConfig, error) {
@ -89,7 +97,7 @@ func (c *Client) Available(ctx context.Context, resource ...string) (ok bool) {
if len(resource) == 0 && !c.value.IsNil() {
return true
}
var namespace = c.config.NamespaceName
namespace := c.config.NamespaceName
if len(resource) > 0 {
namespace = resource[0]
}
@ -132,18 +140,46 @@ func (c *Client) OnNewestChange(event *storage.FullChangeEvent) {
}
func (c *Client) updateLocalValue(ctx context.Context) (err error) {
var j = gjson.New(nil)
j := gjson.New(nil)
content := gjson.New(nil, true)
cache := c.client.GetConfigCache(c.config.NamespaceName)
cache.Range(func(key, value any) bool {
err = j.Set(gconv.String(key), value)
if err != nil {
return false
}
err = content.Set(gconv.String(key), value)
if err != nil {
return false
}
return true
})
cache.Clear()
if err == nil {
c.value.Set(j)
adapterCtx := NewAdapterCtx(ctx).WithOperation(gcfg.OperationUpdate).WithNamespace(c.config.NamespaceName).
WithAppId(c.config.AppID).WithCluster(c.config.Cluster).WithContent(content)
c.notifyWatchers(adapterCtx.Ctx)
}
return
}
// AddWatcher adds a watcher for the specified configuration file.
func (c *Client) AddWatcher(name string, f func(ctx context.Context)) {
c.watchers.Add(name, f)
}
// RemoveWatcher removes the watcher for the specified configuration file.
func (c *Client) RemoveWatcher(name string) {
c.watchers.Remove(name)
}
// GetWatcherNames returns all watcher names.
func (c *Client) GetWatcherNames() []string {
return c.watchers.GetNames()
}
// notifyWatchers notifies all watchers.
func (c *Client) notifyWatchers(ctx context.Context) {
c.watchers.Notify(ctx)
}

View File

@ -0,0 +1,132 @@
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.
// Package apollo implements gcfg.Adapter using apollo service.
package apollo
import (
"context"
"github.com/gogf/gf/v2/encoding/gjson"
"github.com/gogf/gf/v2/os/gcfg"
"github.com/gogf/gf/v2/os/gctx"
)
const (
// ContextKeyNamespace is the context key for namespace
ContextKeyNamespace gctx.StrKey = "namespace"
// ContextKeyAppId is the context key for appId
ContextKeyAppId gctx.StrKey = "appId"
// ContextKeyCluster is the context key for cluster
ContextKeyCluster gctx.StrKey = "cluster"
)
// ApolloAdapterCtx is the context adapter for Apollo configuration
type ApolloAdapterCtx struct {
Ctx context.Context
}
// NewAdapterCtxWithCtx creates and returns a new ApolloAdapterCtx with the given context.
func NewAdapterCtxWithCtx(ctx context.Context) *ApolloAdapterCtx {
if ctx == nil {
ctx = context.Background()
}
return &ApolloAdapterCtx{Ctx: ctx}
}
// NewAdapterCtx creates and returns a new ApolloAdapterCtx.
// If context is provided, it will be used; otherwise, a background context is created.
func NewAdapterCtx(ctx ...context.Context) *ApolloAdapterCtx {
if len(ctx) > 0 {
return NewAdapterCtxWithCtx(ctx[0])
}
return NewAdapterCtxWithCtx(context.Background())
}
// GetAdapterCtx creates a new ApolloAdapterCtx with the given context
func GetAdapterCtx(ctx context.Context) *ApolloAdapterCtx {
return NewAdapterCtxWithCtx(ctx)
}
// WithOperation sets the operation in the context
func (a *ApolloAdapterCtx) WithOperation(operation gcfg.OperationType) *ApolloAdapterCtx {
a.Ctx = context.WithValue(a.Ctx, gcfg.ContextKeyOperation, operation)
return a
}
// WithNamespace sets the namespace in the context
func (a *ApolloAdapterCtx) WithNamespace(namespace string) *ApolloAdapterCtx {
a.Ctx = context.WithValue(a.Ctx, ContextKeyNamespace, namespace)
return a
}
// WithAppId sets the appId in the context
func (a *ApolloAdapterCtx) WithAppId(appId string) *ApolloAdapterCtx {
a.Ctx = context.WithValue(a.Ctx, ContextKeyAppId, appId)
return a
}
// WithCluster sets the cluster in the context
func (a *ApolloAdapterCtx) WithCluster(cluster string) *ApolloAdapterCtx {
a.Ctx = context.WithValue(a.Ctx, ContextKeyCluster, cluster)
return a
}
// WithContent sets the content in the context
func (a *ApolloAdapterCtx) WithContent(content *gjson.Json) *ApolloAdapterCtx {
a.Ctx = context.WithValue(a.Ctx, gcfg.ContextKeyContent, content)
return a
}
// GetNamespace retrieves the namespace from the context
func (a *ApolloAdapterCtx) GetNamespace() string {
if v := a.Ctx.Value(ContextKeyNamespace); v != nil {
if s, ok := v.(string); ok {
return s
}
}
return ""
}
// GetAppId retrieves the appId from the context
func (a *ApolloAdapterCtx) GetAppId() string {
if v := a.Ctx.Value(ContextKeyAppId); v != nil {
if s, ok := v.(string); ok {
return s
}
}
return ""
}
// GetCluster retrieves the cluster from the context
func (a *ApolloAdapterCtx) GetCluster() string {
if v := a.Ctx.Value(ContextKeyCluster); v != nil {
if s, ok := v.(string); ok {
return s
}
}
return ""
}
// GetContent retrieves the content from the context
func (a *ApolloAdapterCtx) GetContent() *gjson.Json {
if v := a.Ctx.Value(gcfg.ContextKeyContent); v != nil {
if s, ok := v.(*gjson.Json); ok {
return s
}
}
return gjson.New(nil)
}
// GetOperation retrieves the operation from the context
func (a *ApolloAdapterCtx) GetOperation() gcfg.OperationType {
if v := a.Ctx.Value(gcfg.ContextKeyOperation); v != nil {
if s, ok := v.(gcfg.OperationType); ok {
return s
}
}
return ""
}

View File

@ -21,6 +21,12 @@ import (
"github.com/gogf/gf/v2/os/glog"
)
var (
// Compile-time checking for interface implementation.
_ gcfg.Adapter = (*Client)(nil)
_ gcfg.WatcherAdapter = (*Client)(nil)
)
// Config is the configuration object for consul client.
type Config struct {
// api.Config in consul package
@ -41,6 +47,8 @@ type Client struct {
client *api.Client
// Configmap content cached. It is `*gjson.Json` value internally.
value *g.Var
// Watchers for watching file changes.
watchers *gcfg.WatcherRegistry
}
// New creates and returns gcfg.Adapter implementing using consul service.
@ -55,8 +63,9 @@ func New(ctx context.Context, config Config) (adapter gcfg.Adapter, err error) {
}
client := &Client{
config: config,
value: g.NewVar(nil, true),
config: config,
value: g.NewVar(nil, true),
watchers: gcfg.NewWatcherRegistry(),
}
client.client, err = api.NewClient(&config.ConsulConfig)
@ -156,13 +165,26 @@ func (c *Client) addWatcher() (err error) {
if v, ok = raw.(*api.KVPair); !ok {
return
}
if err = c.doUpdate(v.Value); err != nil {
err = c.doUpdate(v.Value)
if err != nil {
c.config.Logger.Errorf(
context.Background(),
"watch config from consul path %+v update failed: %s",
c.config.Path, err,
)
} else {
var m *gjson.Json
m, err = gjson.LoadContent(v.Value, true)
if err != nil {
c.config.Logger.Errorf(
context.Background(),
"watch config from consul path %+v parse failed: %s",
c.config.Path, err,
)
} else {
adapterCtx := NewAdapterCtx().WithOperation(gcfg.OperationUpdate).WithPath(c.config.Path).WithContent(m)
c.notifyWatchers(adapterCtx.Ctx)
}
}
}
@ -173,6 +195,7 @@ func (c *Client) addWatcher() (err error) {
return nil
}
// startAsynchronousWatch starts the asynchronous watch.
func (c *Client) startAsynchronousWatch(plan *watch.Plan) {
if err := plan.Run(c.config.ConsulConfig.Address); err != nil {
c.config.Logger.Errorf(
@ -182,3 +205,23 @@ func (c *Client) startAsynchronousWatch(plan *watch.Plan) {
)
}
}
// AddWatcher adds a watcher for the specified configuration file.
func (c *Client) AddWatcher(name string, f func(ctx context.Context)) {
c.watchers.Add(name, f)
}
// RemoveWatcher removes the watcher for the specified configuration file.
func (c *Client) RemoveWatcher(name string) {
c.watchers.Remove(name)
}
// GetWatcherNames returns all watcher names.
func (c *Client) GetWatcherNames() []string {
return c.watchers.GetNames()
}
// notifyWatchers notifies all watchers.
func (c *Client) notifyWatchers(ctx context.Context) {
c.watchers.Notify(ctx)
}

View File

@ -0,0 +1,96 @@
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.
// Package consul implements gcfg.Adapter using consul service.
package consul
import (
"context"
"github.com/gogf/gf/v2/encoding/gjson"
"github.com/gogf/gf/v2/os/gcfg"
"github.com/gogf/gf/v2/os/gctx"
)
const (
// ContextKeyPath is the context key for path
ContextKeyPath gctx.StrKey = "path"
)
// ConsulAdapterCtx is the context adapter for Consul configuration
type ConsulAdapterCtx struct {
Ctx context.Context
}
// NewAdapterCtxWithCtx creates and returns a new ConsulAdapterCtx with the given context.
func NewAdapterCtxWithCtx(ctx context.Context) *ConsulAdapterCtx {
if ctx == nil {
ctx = context.Background()
}
return &ConsulAdapterCtx{Ctx: ctx}
}
// NewAdapterCtx creates and returns a new ConsulAdapterCtx.
// If context is provided, it will be used; otherwise, a background context is created.
func NewAdapterCtx(ctx ...context.Context) *ConsulAdapterCtx {
if len(ctx) > 0 {
return NewAdapterCtxWithCtx(ctx[0])
}
return NewAdapterCtxWithCtx(context.Background())
}
// GetAdapterCtx creates a new ConsulAdapterCtx with the given context
func GetAdapterCtx(ctx context.Context) *ConsulAdapterCtx {
return NewAdapterCtxWithCtx(ctx)
}
// WithOperation sets the operation in the context
func (a *ConsulAdapterCtx) WithOperation(operation gcfg.OperationType) *ConsulAdapterCtx {
a.Ctx = context.WithValue(a.Ctx, gcfg.ContextKeyOperation, operation)
return a
}
// WithPath sets the path in the context
func (a *ConsulAdapterCtx) WithPath(path string) *ConsulAdapterCtx {
a.Ctx = context.WithValue(a.Ctx, ContextKeyPath, path)
return a
}
// WithContent sets the content in the context
func (a *ConsulAdapterCtx) WithContent(content *gjson.Json) *ConsulAdapterCtx {
a.Ctx = context.WithValue(a.Ctx, gcfg.ContextKeyContent, content)
return a
}
// GetContent retrieves the content from the context
func (a *ConsulAdapterCtx) GetContent() *gjson.Json {
if v := a.Ctx.Value(gcfg.ContextKeyContent); v != nil {
if s, ok := v.(*gjson.Json); ok {
return s
}
}
return gjson.New(nil)
}
// GetOperation retrieves the operation from the context
func (a *ConsulAdapterCtx) GetOperation() gcfg.OperationType {
if v := a.Ctx.Value(gcfg.ContextKeyOperation); v != nil {
if s, ok := v.(gcfg.OperationType); ok {
return s
}
}
return ""
}
// GetPath retrieves the path from the context
func (a *ConsulAdapterCtx) GetPath() string {
if v := a.Ctx.Value(ContextKeyPath); v != nil {
if s, ok := v.(string); ok {
return s
}
}
return ""
}

View File

@ -23,11 +23,18 @@ import (
"github.com/gogf/gf/v2/util/gutil"
)
var (
// Compile-time checking for interface implementation.
_ gcfg.Adapter = (*Client)(nil)
_ gcfg.WatcherAdapter = (*Client)(nil)
)
// Client implements gcfg.Adapter.
type Client struct {
config Config // Config object when created.
client *kubernetes.Clientset // Kubernetes client.
value *g.Var // Configmap content cached. It is `*gjson.Json` value internally.
config Config // Config object when created.
client *kubernetes.Clientset // Kubernetes client.
value *g.Var // Configmap content cached. It is `*gjson.Json` value internally.
watchers *gcfg.WatcherRegistry // Watchers for watching file changes.
}
// Config for Client.
@ -61,9 +68,10 @@ func New(ctx context.Context, config Config) (adapter gcfg.Adapter, err error) {
}
}
adapter = &Client{
config: config,
client: config.KubeClient,
value: g.NewVar(nil, true),
config: config,
client: config.KubeClient,
value: g.NewVar(nil, true),
watchers: gcfg.NewWatcherRegistry(),
}
return
}
@ -128,6 +136,7 @@ func (c *Client) updateLocalValueAndWatch(ctx context.Context) (err error) {
return nil
}
// doUpdate retrieves and caches the configmap content.
func (c *Client) doUpdate(ctx context.Context, namespace string) (err error) {
cm, err := c.client.CoreV1().ConfigMaps(namespace).Get(ctx, c.config.ConfigMap, kubeMetaV1.GetOptions{})
if err != nil {
@ -145,9 +154,19 @@ func (c *Client) doUpdate(ctx context.Context, namespace string) (err error) {
)
}
c.value.Set(j)
var content *gjson.Json
if content, err = gjson.LoadContent([]byte(cm.Data[c.config.DataItem])); err != nil {
return gerror.Wrapf(
err,
`parse config map item from %s[%s] failed`, c.config.ConfigMap, c.config.DataItem,
)
}
adapterCtx := NewAdapterCtx(ctx).WithOperation(gcfg.OperationUpdate).WithNamespace(namespace).WithConfigMap(c.config.ConfigMap).WithDataItem(c.config.DataItem).WithContent(content)
c.notifyWatchers(adapterCtx.Ctx)
return nil
}
// doWatch watches the configmap content.
func (c *Client) doWatch(ctx context.Context, namespace string) (err error) {
if !c.config.Watch {
return nil
@ -168,6 +187,7 @@ func (c *Client) doWatch(ctx context.Context, namespace string) (err error) {
return nil
}
// startAsynchronousWatch starts an asynchronous watch for the specified configuration file.
func (c *Client) startAsynchronousWatch(ctx context.Context, namespace string, watchHandler watch.Interface) {
for {
event := <-watchHandler.ResultChan()
@ -177,3 +197,23 @@ func (c *Client) startAsynchronousWatch(ctx context.Context, namespace string, w
}
}
}
// AddWatcher adds a watcher for the specified configuration file.
func (c *Client) AddWatcher(name string, f func(ctx context.Context)) {
c.watchers.Add(name, f)
}
// RemoveWatcher removes the watcher for the specified configuration file.
func (c *Client) RemoveWatcher(name string) {
c.watchers.Remove(name)
}
// GetWatcherNames returns all watcher names.
func (c *Client) GetWatcherNames() []string {
return c.watchers.GetNames()
}
// notifyWatchers notifies all watchers.
func (c *Client) notifyWatchers(ctx context.Context) {
c.watchers.Notify(ctx)
}

View File

@ -0,0 +1,132 @@
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.
// Package kubecm implements gcfg.Adapter using kubecm service.
package kubecm
import (
"context"
"github.com/gogf/gf/v2/encoding/gjson"
"github.com/gogf/gf/v2/os/gcfg"
"github.com/gogf/gf/v2/os/gctx"
)
const (
// ContextKeyNamespace is the context key for namespace
ContextKeyNamespace gctx.StrKey = "namespace"
// ContextKeyConfigMap is the context key for configmap
ContextKeyConfigMap gctx.StrKey = "configMap"
// ContextKeyDataItem is the context key for dataitem
ContextKeyDataItem gctx.StrKey = "dataItem"
)
// KubecmAdapterCtx is the context adapter for kubecm configuration
type KubecmAdapterCtx struct {
Ctx context.Context
}
// NewKubecmAdapterCtx creates and returns a new KubecmAdapterCtx with the given context.
func NewKubecmAdapterCtx(ctx context.Context) *KubecmAdapterCtx {
if ctx == nil {
ctx = context.Background()
}
return &KubecmAdapterCtx{Ctx: ctx}
}
// NewAdapterCtx creates and returns a new KubecmAdapterCtx.
// If context is provided, it will be used; otherwise, a background context is created.
func NewAdapterCtx(ctx ...context.Context) *KubecmAdapterCtx {
if len(ctx) > 0 {
return NewKubecmAdapterCtx(ctx[0])
}
return NewKubecmAdapterCtx(context.Background())
}
// GetAdapterCtx creates a new KubecmAdapterCtx with the given context
func GetAdapterCtx(ctx context.Context) *KubecmAdapterCtx {
return NewKubecmAdapterCtx(ctx)
}
// WithOperation sets the operation in the context
func (a *KubecmAdapterCtx) WithOperation(operation gcfg.OperationType) *KubecmAdapterCtx {
a.Ctx = context.WithValue(a.Ctx, gcfg.ContextKeyOperation, operation)
return a
}
// WithNamespace sets the namespace in the context
func (a *KubecmAdapterCtx) WithNamespace(namespace string) *KubecmAdapterCtx {
a.Ctx = context.WithValue(a.Ctx, ContextKeyNamespace, namespace)
return a
}
// WithConfigMap sets the configmap in the context
func (a *KubecmAdapterCtx) WithConfigMap(configMap string) *KubecmAdapterCtx {
a.Ctx = context.WithValue(a.Ctx, ContextKeyConfigMap, configMap)
return a
}
// WithDataItem sets the dataitem in the context
func (a *KubecmAdapterCtx) WithDataItem(dataItem string) *KubecmAdapterCtx {
a.Ctx = context.WithValue(a.Ctx, ContextKeyDataItem, dataItem)
return a
}
// WithContent sets the content in the context
func (a *KubecmAdapterCtx) WithContent(content *gjson.Json) *KubecmAdapterCtx {
a.Ctx = context.WithValue(a.Ctx, gcfg.ContextKeyContent, content)
return a
}
// GetOperation retrieves the operation from the context
func (a *KubecmAdapterCtx) GetOperation() gcfg.OperationType {
if v := a.Ctx.Value(gcfg.ContextKeyOperation); v != nil {
if s, ok := v.(gcfg.OperationType); ok {
return s
}
}
return ""
}
// GetNamespace retrieves the namespace from the context
func (a *KubecmAdapterCtx) GetNamespace() string {
if v := a.Ctx.Value(ContextKeyNamespace); v != nil {
if s, ok := v.(string); ok {
return s
}
}
return ""
}
// GetConfigMap retrieves the configmap from the context
func (a *KubecmAdapterCtx) GetConfigMap() string {
if v := a.Ctx.Value(ContextKeyConfigMap); v != nil {
if s, ok := v.(string); ok {
return s
}
}
return ""
}
// GetDataItem retrieves the dataitem from the context
func (a *KubecmAdapterCtx) GetDataItem() string {
if v := a.Ctx.Value(ContextKeyDataItem); v != nil {
if s, ok := v.(string); ok {
return s
}
}
return ""
}
// GetContent retrieves the content from the context
func (a *KubecmAdapterCtx) GetContent() *gjson.Json {
if v := a.Ctx.Value(gcfg.ContextKeyContent); v != nil {
if s, ok := v.(*gjson.Json); ok {
return s
}
}
return gjson.New(nil)
}

View File

@ -21,6 +21,12 @@ import (
"github.com/gogf/gf/v2/os/gcfg"
)
var (
// Compile-time checking for interface implementation.
_ gcfg.Adapter = (*Client)(nil)
_ gcfg.WatcherAdapter = (*Client)(nil)
)
// Config is the configuration object for nacos client.
type Config struct {
ServerConfigs []constant.ServerConfig `v:"required"` // See constant.ServerConfig
@ -32,9 +38,10 @@ type Config struct {
// Client implements gcfg.Adapter implementing using nacos service.
type Client struct {
config Config // Config object when created.
client config_client.IConfigClient // Nacos config client.
value *g.Var // Configmap content cached. It is `*gjson.Json` value internally.
config Config // Config object when created.
client config_client.IConfigClient // Nacos config client.
value *g.Var // Configmap content cached. It is `*gjson.Json` value internally.
watchers *gcfg.WatcherRegistry // Watchers for watching file changes.
}
// New creates and returns gcfg.Adapter implementing using nacos service.
@ -46,8 +53,9 @@ func New(ctx context.Context, config Config) (adapter gcfg.Adapter, err error) {
}
client := &Client{
config: config,
value: g.NewVar(nil, true),
config: config,
value: g.NewVar(nil, true),
watchers: gcfg.NewWatcherRegistry(),
}
client.client, err = clients.CreateConfigClient(map[string]any{
@ -127,10 +135,13 @@ func (c *Client) addWatcher() error {
return nil
}
c.config.ConfigParam.OnChange = func(namespace, group, dataId, data string) {
c.doUpdate(data)
_ = c.doUpdate(data)
if c.config.OnConfigChange != nil {
go c.config.OnConfigChange(namespace, group, dataId, data)
}
adapterCtx := NewAdapterCtx().WithOperation(gcfg.OperationUpdate).WithNamespace(namespace).
WithGroup(group).WithDataId(dataId).WithContent(data)
c.notifyWatchers(adapterCtx.Ctx)
}
if err := c.client.ListenConfig(c.config.ConfigParam); err != nil {
@ -139,3 +150,23 @@ func (c *Client) addWatcher() error {
return nil
}
// AddWatcher adds a watcher for the specified configuration file.
func (c *Client) AddWatcher(name string, f func(ctx context.Context)) {
c.watchers.Add(name, f)
}
// RemoveWatcher removes the watcher for the specified configuration file.
func (c *Client) RemoveWatcher(name string) {
c.watchers.Remove(name)
}
// GetWatcherNames returns all watcher names.
func (c *Client) GetWatcherNames() []string {
return c.watchers.GetNames()
}
// notifyWatchers notifies all watchers.
func (c *Client) notifyWatchers(ctx context.Context) {
c.watchers.Notify(ctx)
}

View File

@ -0,0 +1,131 @@
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.
// Package nacos implements gcfg.Adapter using nacos service.
package nacos
import (
"context"
"github.com/gogf/gf/v2/os/gcfg"
"github.com/gogf/gf/v2/os/gctx"
)
const (
// ContextKeyNamespace is the context key for namespace
ContextKeyNamespace gctx.StrKey = "namespace"
// ContextKeyGroup is the context key for group
ContextKeyGroup gctx.StrKey = "group"
// ContextKeyDataId is the context key for dataId
ContextKeyDataId gctx.StrKey = "dataId"
)
// NacosAdapterCtx is the context adapter for Nacos configuration
type NacosAdapterCtx struct {
Ctx context.Context
}
// NewAdapterCtxWithCtx creates and returns a new NacosAdapterCtx with the given context.
func NewAdapterCtxWithCtx(ctx context.Context) *NacosAdapterCtx {
if ctx == nil {
ctx = context.Background()
}
return &NacosAdapterCtx{Ctx: ctx}
}
// NewAdapterCtx creates and returns a new NacosAdapterCtx.
// If context is provided, it will be used; otherwise, a background context is created.
func NewAdapterCtx(ctx ...context.Context) *NacosAdapterCtx {
if len(ctx) > 0 {
return NewAdapterCtxWithCtx(ctx[0])
}
return NewAdapterCtxWithCtx(context.Background())
}
// GetAdapterCtx creates a new NacosAdapterCtx with the given context
func GetAdapterCtx(ctx context.Context) *NacosAdapterCtx {
return NewAdapterCtxWithCtx(ctx)
}
// WithOperation sets the operation in the context
func (n *NacosAdapterCtx) WithOperation(operation gcfg.OperationType) *NacosAdapterCtx {
n.Ctx = context.WithValue(n.Ctx, gcfg.ContextKeyOperation, operation)
return n
}
// WithNamespace sets the namespace in the context
func (n *NacosAdapterCtx) WithNamespace(namespace string) *NacosAdapterCtx {
n.Ctx = context.WithValue(n.Ctx, ContextKeyNamespace, namespace)
return n
}
// WithGroup sets the group in the context
func (n *NacosAdapterCtx) WithGroup(group string) *NacosAdapterCtx {
n.Ctx = context.WithValue(n.Ctx, ContextKeyGroup, group)
return n
}
// WithDataId sets the dataId in the context
func (n *NacosAdapterCtx) WithDataId(dataId string) *NacosAdapterCtx {
n.Ctx = context.WithValue(n.Ctx, ContextKeyDataId, dataId)
return n
}
// WithContent sets the content in the context
func (n *NacosAdapterCtx) WithContent(content string) *NacosAdapterCtx {
n.Ctx = context.WithValue(n.Ctx, gcfg.ContextKeyContent, content)
return n
}
// GetNamespace retrieves the namespace from the context
func (n *NacosAdapterCtx) GetNamespace() string {
if v := n.Ctx.Value(ContextKeyNamespace); v != nil {
if s, ok := v.(string); ok {
return s
}
}
return ""
}
// GetGroup retrieves the group from the context
func (n *NacosAdapterCtx) GetGroup() string {
if v := n.Ctx.Value(ContextKeyGroup); v != nil {
if s, ok := v.(string); ok {
return s
}
}
return ""
}
// GetDataId retrieves the dataId from the context
func (n *NacosAdapterCtx) GetDataId() string {
if v := n.Ctx.Value(ContextKeyDataId); v != nil {
if s, ok := v.(string); ok {
return s
}
}
return ""
}
// GetContent retrieves the content from the context
func (n *NacosAdapterCtx) GetContent() string {
if v := n.Ctx.Value(gcfg.ContextKeyContent); v != nil {
if s, ok := v.(string); ok {
return s
}
}
return ""
}
// GetOperation retrieves the operation from the context
func (n *NacosAdapterCtx) GetOperation() gcfg.OperationType {
if v := n.Ctx.Value(gcfg.ContextKeyOperation); v != nil {
if s, ok := v.(gcfg.OperationType); ok {
return s
}
}
return ""
}

View File

@ -7,6 +7,7 @@
package nacos_test
import (
"context"
"net/url"
"testing"
"time"
@ -16,6 +17,7 @@ import (
"github.com/gogf/gf/v2/encoding/gjson"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gcfg"
"github.com/gogf/gf/v2/os/gctx"
"github.com/gogf/gf/v2/test/gtest"
"github.com/gogf/gf/v2/util/guid"
@ -70,12 +72,22 @@ func TestNacosOnConfigChangeFunc(t *testing.T) {
ConfigParam: configParam,
Watch: true,
OnConfigChange: func(namespace, group, dataId, data string) {
gtest.Assert("public", namespace)
gtest.Assert("test", group)
gtest.Assert("config.toml", dataId)
gtest.Assert("gf", g.Cfg().MustGet(gctx.GetInitCtx(), "app.name").String())
t.Assert(namespace, "public")
t.Assert(group, "test")
t.Assert(dataId, "config.toml")
t.Assert(g.Cfg().MustGet(gctx.GetInitCtx(), "app.name").String(), "gf")
},
})
if watcherAdapter, ok := adapter.(gcfg.WatcherAdapter); ok {
watcherAdapter.AddWatcher("test", func(ctx context.Context) {
adapterCtx := nacos.GetAdapterCtx(ctx)
t.Assert(adapterCtx.GetNamespace(), "public")
t.Assert(adapterCtx.GetGroup(), "test")
t.Assert(adapterCtx.GetDataId(), "config.toml")
t.Assert(adapterCtx.GetOperation(), gcfg.OperationUpdate)
t.Assert(g.Cfg().MustGet(gctx.GetInitCtx(), "app.name").String(), "gf")
})
}
g.Cfg().SetAdapter(adapter)
t.Assert(g.Cfg().Available(ctx), true)
appName, err := g.Cfg().Get(ctx, "app.name")
@ -97,5 +109,8 @@ func TestNacosOnConfigChangeFunc(t *testing.T) {
t.AssertNil(err)
_, err = g.Client().Post(ctx, configPublishUrl+"&content="+url.QueryEscape(res2))
t.AssertNil(err)
if watcherAdapter, ok := adapter.(gcfg.WatcherAdapter); ok {
t.Assert(watcherAdapter.GetWatcherNames()[0], "test")
}
})
}

View File

@ -21,6 +21,12 @@ import (
"github.com/gogf/gf/v2/text/gstr"
)
var (
// Compile-time checking for interface implementation.
_ gcfg.Adapter = (*Client)(nil)
_ gcfg.WatcherAdapter = (*Client)(nil)
)
// Config is the configuration for polaris.
type Config struct {
// The namespace of the configuration.
@ -39,9 +45,10 @@ type Config struct {
// Client implements gcfg.Adapter implementing using polaris service.
type Client struct {
config Config
client model.ConfigFile
value *g.Var
config Config
client model.ConfigFile
value *g.Var
watchers *gcfg.WatcherRegistry
}
const defaultLogDir = "/tmp/polaris/log"
@ -54,8 +61,9 @@ func New(ctx context.Context, config Config) (adapter gcfg.Adapter, err error) {
}
var (
client = &Client{
config: config,
value: g.NewVar(nil, true),
config: config,
value: g.NewVar(nil, true),
watchers: gcfg.NewWatcherRegistry(),
}
configAPI polaris.ConfigAPI
)
@ -142,18 +150,24 @@ func (c *Client) updateLocalValueAndWatch(ctx context.Context) (err error) {
return nil
}
// doUpdate retrieves and caches the configmap content.
func (c *Client) doUpdate(ctx context.Context) (err error) {
if !c.client.HasContent() {
return gerror.New("config file is empty")
}
var j *gjson.Json
if j, err = gjson.LoadContent([]byte(c.client.GetContent())); err != nil {
content := c.client.GetContent()
if j, err = gjson.LoadContent([]byte(content)); err != nil {
return gerror.Wrap(err, `parse config map item from polaris failed`)
}
c.value.Set(j)
adapterCtx := NewAdapterCtx(ctx).WithNamespace(c.config.Namespace).WithFileGroup(c.config.FileGroup).
WithFileName(c.config.FileName).WithOperation(gcfg.OperationUpdate).WithContent(content)
c.notifyWatchers(adapterCtx.Ctx)
return nil
}
// doWatch watches the configmap content.
func (c *Client) doWatch(ctx context.Context) (err error) {
if !c.config.Watch {
return nil
@ -165,11 +179,29 @@ func (c *Client) doWatch(ctx context.Context) (err error) {
return nil
}
// startAsynchronousWatch starts the asynchronous watch for the specified configuration file.
func (c *Client) startAsynchronousWatch(ctx context.Context, changeChan <-chan model.ConfigFileChangeEvent) {
for {
select {
case <-changeChan:
_ = c.doUpdate(ctx)
}
for range changeChan {
_ = c.doUpdate(ctx)
}
}
// AddWatcher adds a watcher for the specified configuration file.
func (c *Client) AddWatcher(name string, f func(ctx context.Context)) {
c.watchers.Add(name, f)
}
// RemoveWatcher removes the watcher for the specified configuration file.
func (c *Client) RemoveWatcher(name string) {
c.watchers.Remove(name)
}
// GetWatcherNames returns all watcher names.
func (c *Client) GetWatcherNames() []string {
return c.watchers.GetNames()
}
// notifyWatchers notifies all watchers.
func (c *Client) notifyWatchers(ctx context.Context) {
c.watchers.Notify(ctx)
}

View File

@ -0,0 +1,129 @@
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.
// Package polaris implements gcfg.Adapter using polaris service.
package polaris
import (
"context"
"github.com/gogf/gf/v2/os/gcfg"
"github.com/gogf/gf/v2/os/gctx"
)
const (
// ContextKeyNamespace is the context key for namespace
ContextKeyNamespace gctx.StrKey = "namespace"
// ContextKeyFileGroup is the context key for group
ContextKeyFileGroup gctx.StrKey = "fileGroup"
)
// PolarisAdapterCtx is the context adapter for polaris configuration
type PolarisAdapterCtx struct {
Ctx context.Context
}
// NewAdapterCtxWithCtx creates and returns a new PolarisAdapterCtx with the given context.
func NewAdapterCtxWithCtx(ctx context.Context) *PolarisAdapterCtx {
if ctx == nil {
ctx = context.Background()
}
return &PolarisAdapterCtx{Ctx: ctx}
}
// NewAdapterCtx creates and returns a new PolarisAdapterCtx.
// If context is provided, it will be used; otherwise, a background context is created.
func NewAdapterCtx(ctx ...context.Context) *PolarisAdapterCtx {
if len(ctx) > 0 {
return NewAdapterCtxWithCtx(ctx[0])
}
return NewAdapterCtxWithCtx(context.Background())
}
// GetAdapterCtx creates a new PolarisAdapterCtx with the given context
func GetAdapterCtx(ctx context.Context) *PolarisAdapterCtx {
return NewAdapterCtxWithCtx(ctx)
}
// WithOperation sets the operation in the context
func (n *PolarisAdapterCtx) WithOperation(operation gcfg.OperationType) *PolarisAdapterCtx {
n.Ctx = context.WithValue(n.Ctx, gcfg.ContextKeyOperation, operation)
return n
}
// WithNamespace sets the namespace in the context
func (n *PolarisAdapterCtx) WithNamespace(namespace string) *PolarisAdapterCtx {
n.Ctx = context.WithValue(n.Ctx, ContextKeyNamespace, namespace)
return n
}
// WithFileGroup sets the group in the context
func (n *PolarisAdapterCtx) WithFileGroup(fileGroup string) *PolarisAdapterCtx {
n.Ctx = context.WithValue(n.Ctx, ContextKeyFileGroup, fileGroup)
return n
}
// WithFileName sets the fileName in the context
func (n *PolarisAdapterCtx) WithFileName(fileName string) *PolarisAdapterCtx {
n.Ctx = context.WithValue(n.Ctx, gcfg.ContextKeyFileName, fileName)
return n
}
// WithContent sets the content in the context
func (n *PolarisAdapterCtx) WithContent(content string) *PolarisAdapterCtx {
n.Ctx = context.WithValue(n.Ctx, gcfg.ContextKeyContent, content)
return n
}
// GetNamespace retrieves the namespace from the context
func (n *PolarisAdapterCtx) GetNamespace() string {
if v := n.Ctx.Value(ContextKeyNamespace); v != nil {
if s, ok := v.(string); ok {
return s
}
}
return ""
}
// GetFileGroup retrieves the group from the context
func (n *PolarisAdapterCtx) GetFileGroup() string {
if v := n.Ctx.Value(ContextKeyFileGroup); v != nil {
if s, ok := v.(string); ok {
return s
}
}
return ""
}
// GetFileName retrieves the fileName from the context
func (n *PolarisAdapterCtx) GetFileName() string {
if v := n.Ctx.Value(gcfg.ContextKeyFileName); v != nil {
if s, ok := v.(string); ok {
return s
}
}
return ""
}
// GetContent retrieves the content from the context
func (n *PolarisAdapterCtx) GetContent() string {
if v := n.Ctx.Value(gcfg.ContextKeyContent); v != nil {
if s, ok := v.(string); ok {
return s
}
}
return ""
}
// GetOperation retrieves the operation from the context
func (n *PolarisAdapterCtx) GetOperation() gcfg.OperationType {
if v := n.Ctx.Value(gcfg.ContextKeyOperation); v != nil {
if s, ok := v.(gcfg.OperationType); ok {
return s
}
}
return ""
}

View File

@ -28,3 +28,13 @@ type Adapter interface {
// you can implement this function if necessary.
Data(ctx context.Context) (data map[string]any, err error)
}
// WatcherAdapter is the interface for configuration watcher.
type WatcherAdapter interface {
// AddWatcher adds a watcher function for specified `pattern` and `resource`.
AddWatcher(name string, fn func(ctx context.Context))
// RemoveWatcher removes the watcher function for specified `pattern` and `resource`.
RemoveWatcher(name string)
// GetWatcherNames returns all watcher names.
GetWatcherNames() []string
}

View File

@ -14,17 +14,25 @@ import (
"github.com/gogf/gf/v2/errors/gerror"
)
var (
// Compile-time checking for interface implementation.
_ Adapter = (*AdapterContent)(nil)
_ WatcherAdapter = (*AdapterContent)(nil)
)
// AdapterContent implements interface Adapter using content.
// The configuration content supports the coding types as package `gjson`.
type AdapterContent struct {
jsonVar *gvar.Var // The pared JSON object for configuration content, type: *gjson.Json.
jsonVar *gvar.Var // The pared JSON object for configuration content, type: *gjson.Json.
watchers *WatcherRegistry // Watchers for watching file changes.
}
// NewAdapterContent returns a new configuration management object using custom content.
// The parameter `content` specifies the default configuration content for reading.
func NewAdapterContent(content ...string) (*AdapterContent, error) {
a := &AdapterContent{
jsonVar: gvar.New(nil, true),
jsonVar: gvar.New(nil, true),
watchers: NewWatcherRegistry(),
}
if len(content) > 0 {
if err := a.SetContent(content[0]); err != nil {
@ -42,6 +50,8 @@ func (a *AdapterContent) SetContent(content string) error {
return gerror.Wrap(err, `load configuration content failed`)
}
a.jsonVar.Set(j)
adapterCtx := NewAdapterContentCtx().WithOperation(OperationSet).WithContent(content)
a.notifyWatchers(adapterCtx.Ctx)
return nil
}
@ -74,3 +84,23 @@ func (a *AdapterContent) Data(ctx context.Context) (data map[string]any, err err
}
return a.jsonVar.Val().(*gjson.Json).Var().Map(), nil
}
// AddWatcher adds a watcher for the specified configuration file.
func (a *AdapterContent) AddWatcher(name string, fn func(ctx context.Context)) {
a.watchers.Add(name, fn)
}
// RemoveWatcher removes the watcher for the specified configuration file.
func (a *AdapterContent) RemoveWatcher(name string) {
a.watchers.Remove(name)
}
// GetWatcherNames returns all watcher names.
func (a *AdapterContent) GetWatcherNames() []string {
return a.watchers.GetNames()
}
// notifyWatchers notifies all watchers.
func (a *AdapterContent) notifyWatchers(ctx context.Context) {
a.watchers.Notify(ctx)
}

View File

@ -0,0 +1,74 @@
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.
// Package gcfg provides reading, caching and managing for configuration.
package gcfg
import (
"context"
)
// AdapterContentCtx is the context for AdapterContent.
type AdapterContentCtx struct {
// Ctx is the context with configuration values
Ctx context.Context
}
// NewAdapterContentCtxWithCtx creates and returns a new AdapterContentCtx with the given context.
func NewAdapterContentCtxWithCtx(ctx context.Context) *AdapterContentCtx {
if ctx == nil {
ctx = context.Background()
}
return &AdapterContentCtx{Ctx: ctx}
}
// NewAdapterContentCtx creates and returns a new AdapterContentCtx.
// If ctx is provided, it uses that context, otherwise it creates a background context.
func NewAdapterContentCtx(ctx ...context.Context) *AdapterContentCtx {
if len(ctx) > 0 {
return NewAdapterContentCtxWithCtx(ctx[0])
}
return NewAdapterContentCtxWithCtx(context.Background())
}
// GetAdapterContentCtx creates and returns an AdapterContentCtx with the given context.
func GetAdapterContentCtx(ctx context.Context) *AdapterContentCtx {
return NewAdapterContentCtxWithCtx(ctx)
}
// WithOperation sets the operation in the context and returns the updated AdapterContentCtx.
func (a *AdapterContentCtx) WithOperation(operation OperationType) *AdapterContentCtx {
a.Ctx = context.WithValue(a.Ctx, ContextKeyOperation, operation)
return a
}
// WithContent sets the content in the context and returns the updated AdapterContentCtx.
func (a *AdapterContentCtx) WithContent(content string) *AdapterContentCtx {
a.Ctx = context.WithValue(a.Ctx, ContextKeyContent, content)
return a
}
// GetOperation retrieves the operation from the context.
// Returns empty string if not found.
func (a *AdapterContentCtx) GetOperation() OperationType {
if v := a.Ctx.Value(ContextKeyOperation); v != nil {
if s, ok := v.(OperationType); ok {
return s
}
}
return ""
}
// GetContent retrieves the content from the context.
// Returns empty string if not found.
func (a *AdapterContentCtx) GetContent() string {
if v := a.Ctx.Value(ContextKeyContent); v != nil {
if s, ok := v.(string); ok {
return s
}
}
return ""
}

View File

@ -11,6 +11,7 @@ import (
"github.com/gogf/gf/v2/container/garray"
"github.com/gogf/gf/v2/container/gmap"
"github.com/gogf/gf/v2/container/gtype"
"github.com/gogf/gf/v2/container/gvar"
"github.com/gogf/gf/v2/encoding/gjson"
"github.com/gogf/gf/v2/errors/gerror"
@ -23,12 +24,19 @@ import (
"github.com/gogf/gf/v2/util/gutil"
)
var (
// Compile-time checking for interface implementation.
_ Adapter = (*AdapterFile)(nil)
_ WatcherAdapter = (*AdapterFile)(nil)
)
// AdapterFile implements interface Adapter using file.
type AdapterFile struct {
defaultFileNameOrPath string // Default configuration file name or file path.
defaultFileNameOrPath *gtype.String // Default configuration file name or file path.
searchPaths *garray.StrArray // Searching the path array.
jsonMap *gmap.StrAnyMap // The pared JSON objects for configuration files.
violenceCheck bool // Whether it does violence check in value index searching. It affects the performance when set true(false in default).
watchers *WatcherRegistry // Watchers for watching file changes.
}
const (
@ -67,9 +75,10 @@ func NewAdapterFile(fileNameOrPath ...string) (*AdapterFile, error) {
}
}
config := &AdapterFile{
defaultFileNameOrPath: usedFileNameOrPath,
defaultFileNameOrPath: gtype.NewString(usedFileNameOrPath),
searchPaths: garray.NewStrArray(true),
jsonMap: gmap.NewStrAnyMap(true),
watchers: NewWatcherRegistry(),
}
// Customized dir path from env/cmd.
if customPath := command.GetOptWithEnv(commandEnvKeyForPath); customPath != "" {
@ -121,12 +130,12 @@ func (a *AdapterFile) SetViolenceCheck(check bool) {
// SetFileName sets the default configuration file name.
func (a *AdapterFile) SetFileName(fileNameOrPath string) {
a.defaultFileNameOrPath = fileNameOrPath
a.defaultFileNameOrPath.Set(fileNameOrPath)
}
// GetFileName returns the default configuration file name.
func (a *AdapterFile) GetFileName() string {
return a.defaultFileNameOrPath
return a.defaultFileNameOrPath.String()
}
// Get retrieves and returns value by specified `pattern`.
@ -159,8 +168,17 @@ func (a *AdapterFile) Set(pattern string, value any) error {
return err
}
if j != nil {
return j.Set(pattern, value)
err = j.Set(pattern, value)
if err != nil {
return err
}
}
fileName := a.GetFileName()
filePath, _ := a.GetFilePath(fileName)
fileType := gfile.ExtName(fileName)
adapterCtx := NewAdapterFileCtx().WithOperation(OperationSet).WithKey(pattern).WithValue(value).
WithFileName(fileName).WithFilePath(filePath).WithFileType(fileType)
a.notifyWatchers(adapterCtx.Ctx)
return nil
}
@ -189,6 +207,11 @@ func (a *AdapterFile) MustGet(ctx context.Context, pattern string) *gvar.Var {
// which will force reload configuration content from the file.
func (a *AdapterFile) Clear() {
a.jsonMap.Clear()
fileName := a.GetFileName()
filePath, _ := a.GetFilePath(fileName)
fileType := gfile.ExtName(fileName)
adapterFileCtx := NewAdapterFileCtx().WithOperation(OperationClear).WithFileName(fileName).WithFilePath(filePath).WithFileType(fileType)
a.notifyWatchers(adapterFileCtx.Ctx)
}
// Dump prints current JSON object with more manually readable.
@ -200,7 +223,7 @@ func (a *AdapterFile) Dump() {
// Available checks and returns whether configuration of given `file` is available.
func (a *AdapterFile) Available(ctx context.Context, fileName ...string) bool {
checkFileName := gutil.GetOrDefaultStr(a.defaultFileNameOrPath, fileName...)
checkFileName := gutil.GetOrDefaultStr(a.defaultFileNameOrPath.String(), fileName...)
// Custom configuration content exists.
if a.GetContent(checkFileName) != "" {
return true
@ -228,13 +251,9 @@ func (a *AdapterFile) autoCheckAndAddMainPkgPathToSearchPaths() {
// getJson returns a *gjson.Json object for the specified `file` content.
// It would print error if file reading fails. It returns nil if any error occurs.
func (a *AdapterFile) getJson(fileNameOrPath ...string) (configJson *gjson.Json, err error) {
var (
usedFileNameOrPath = a.defaultFileNameOrPath
)
usedFileNameOrPath := a.GetFileName()
if len(fileNameOrPath) > 0 && fileNameOrPath[0] != "" {
usedFileNameOrPath = fileNameOrPath[0]
} else {
usedFileNameOrPath = a.defaultFileNameOrPath
}
// It uses JSON map to cache specified configuration file content.
result := a.jsonMap.GetOrSetFuncLock(usedFileNameOrPath, func() any {
@ -280,6 +299,23 @@ func (a *AdapterFile) getJson(fileNameOrPath ...string) (configJson *gjson.Json,
if filePath != "" && !gres.Contains(filePath) {
_, err = gfsnotify.Add(filePath, func(event *gfsnotify.Event) {
a.jsonMap.Remove(usedFileNameOrPath)
if event.IsWrite() || event.IsRemove() || event.IsCreate() || event.IsRename() || event.IsChmod() {
fileType := gfile.ExtName(usedFileNameOrPath)
adapterCtx := NewAdapterFileCtx().WithFileName(usedFileNameOrPath).WithFilePath(filePath).WithFileType(fileType)
switch {
case event.IsWrite():
adapterCtx.WithOperation(OperationWrite)
case event.IsRemove():
adapterCtx.WithOperation(OperationRemove)
case event.IsCreate():
adapterCtx.WithOperation(OperationCreate)
case event.IsRename():
adapterCtx.WithOperation(OperationRename)
case event.IsChmod():
adapterCtx.WithOperation(OperationChmod)
}
a.notifyWatchers(adapterCtx.Ctx)
}
})
if err != nil {
return nil
@ -292,3 +328,23 @@ func (a *AdapterFile) getJson(fileNameOrPath ...string) (configJson *gjson.Json,
}
return
}
// AddWatcher adds a watcher for the specified configuration file.
func (a *AdapterFile) AddWatcher(name string, fn func(ctx context.Context)) {
a.watchers.Add(name, fn)
}
// RemoveWatcher removes the watcher for the specified configuration file.
func (a *AdapterFile) RemoveWatcher(name string) {
a.watchers.Remove(name)
}
// GetWatcherNames returns all watcher names.
func (a *AdapterFile) GetWatcherNames() []string {
return a.watchers.GetNames()
}
// notifyWatchers notifies all watchers.
func (a *AdapterFile) notifyWatchers(ctx context.Context) {
a.watchers.Notify(ctx)
}

View File

@ -32,6 +32,8 @@ func (a *AdapterFile) SetContent(content string, fileNameOrPath ...string) {
}
customConfigContentMap.Set(usedFileNameOrPath, content)
})
adapterCtx := NewAdapterFileCtx().WithFileName(usedFileNameOrPath).WithOperation(OperationSet).WithContent(content)
a.notifyWatchers(adapterCtx.Ctx)
}
// GetContent returns customized configuration content for specified `file`.
@ -64,7 +66,8 @@ func (a *AdapterFile) RemoveContent(fileNameOrPath ...string) {
customConfigContentMap.Remove(usedFileNameOrPath)
}
})
adapterCtx := NewAdapterFileCtx().WithFileName(usedFileNameOrPath).WithOperation(OperationRemove)
a.notifyWatchers(adapterCtx.Ctx)
intlog.Printf(context.TODO(), `RemoveContent: %s`, usedFileNameOrPath)
}
@ -81,5 +84,7 @@ func (a *AdapterFile) ClearContent() {
}
}
})
adapterCtx := NewAdapterFileCtx().WithOperation(OperationClear)
a.notifyWatchers(adapterCtx.Ctx)
intlog.Print(context.TODO(), `RemoveConfig`)
}

View File

@ -0,0 +1,157 @@
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.
// Package gcfg provides reading, caching and managing for configuration.
package gcfg
import (
"context"
"github.com/gogf/gf/v2/container/gvar"
)
// AdapterFileCtx is the context for AdapterFile.
type AdapterFileCtx struct {
// Ctx is the context with configuration values
Ctx context.Context
}
// NewAdapterFileCtxWithCtx creates and returns a new AdapterFileCtx with the given context.
func NewAdapterFileCtxWithCtx(ctx context.Context) *AdapterFileCtx {
if ctx == nil {
ctx = context.Background()
}
return &AdapterFileCtx{Ctx: ctx}
}
// NewAdapterFileCtx creates and returns a new AdapterFileCtx.
// If ctx is provided, it uses that context, otherwise it creates a background context.
func NewAdapterFileCtx(ctx ...context.Context) *AdapterFileCtx {
if len(ctx) > 0 {
return NewAdapterFileCtxWithCtx(ctx[0])
}
return NewAdapterFileCtxWithCtx(context.Background())
}
// GetAdapterFileCtx creates and returns an AdapterFileCtx with the given context.
func GetAdapterFileCtx(ctx context.Context) *AdapterFileCtx {
return NewAdapterFileCtxWithCtx(ctx)
}
// WithFileName sets the file name in the context and returns the updated AdapterFileCtx.
func (a *AdapterFileCtx) WithFileName(fileName string) *AdapterFileCtx {
a.Ctx = context.WithValue(a.Ctx, ContextKeyFileName, fileName)
return a
}
// WithFilePath sets the file path in the context and returns the updated AdapterFileCtx.
func (a *AdapterFileCtx) WithFilePath(filePath string) *AdapterFileCtx {
a.Ctx = context.WithValue(a.Ctx, ContextKeyFilePath, filePath)
return a
}
// WithFileType sets the file type in the context and returns the updated AdapterFileCtx.
func (a *AdapterFileCtx) WithFileType(fileType string) *AdapterFileCtx {
a.Ctx = context.WithValue(a.Ctx, ContextKeyFileType, fileType)
return a
}
// WithOperation sets the operation in the context and returns the updated AdapterFileCtx.
func (a *AdapterFileCtx) WithOperation(operation OperationType) *AdapterFileCtx {
a.Ctx = context.WithValue(a.Ctx, ContextKeyOperation, operation)
return a
}
// WithKey sets the key in the context and returns the updated AdapterFileCtx.
func (a *AdapterFileCtx) WithKey(key string) *AdapterFileCtx {
a.Ctx = context.WithValue(a.Ctx, ContextKeyKey, key)
return a
}
// WithValue sets the value in the context and returns the updated AdapterFileCtx.
func (a *AdapterFileCtx) WithValue(value any) *AdapterFileCtx {
a.Ctx = context.WithValue(a.Ctx, ContextKeyValue, value)
return a
}
// WithContent sets the content in the context and returns the updated AdapterFileCtx.
func (a *AdapterFileCtx) WithContent(content any) *AdapterFileCtx {
a.Ctx = context.WithValue(a.Ctx, ContextKeyContent, content)
return a
}
// GetFileName retrieves the file name from the context.
// Returns empty string if not found.
func (a *AdapterFileCtx) GetFileName() string {
if v := a.Ctx.Value(ContextKeyFileName); v != nil {
if s, ok := v.(string); ok {
return s
}
}
return ""
}
// GetFilePath retrieves the file path from the context.
// Returns empty string if not found.
func (a *AdapterFileCtx) GetFilePath() string {
if v := a.Ctx.Value(ContextKeyFilePath); v != nil {
if s, ok := v.(string); ok {
return s
}
}
return ""
}
// GetFileType retrieves the file type from the context.
// Returns empty string if not found.
func (a *AdapterFileCtx) GetFileType() string {
if v := a.Ctx.Value(ContextKeyFileType); v != nil {
if s, ok := v.(string); ok {
return s
}
}
return ""
}
// GetOperation retrieves the operation from the context.
// Returns empty string if not found.
func (a *AdapterFileCtx) GetOperation() OperationType {
if v := a.Ctx.Value(ContextKeyOperation); v != nil {
if s, ok := v.(OperationType); ok {
return s
}
}
return ""
}
// GetKey retrieves the key from the context.
// Returns empty string if not found.
func (a *AdapterFileCtx) GetKey() string {
if v := a.Ctx.Value(ContextKeyKey); v != nil {
if s, ok := v.(string); ok {
return s
}
}
return ""
}
// GetValue retrieves the value from the context.
// Returns nil if not found.
func (a *AdapterFileCtx) GetValue() *gvar.Var {
if v := a.Ctx.Value(ContextKeyValue); v != nil {
return gvar.New(v)
}
return nil
}
// GetContent retrieves the set content from the context.
// Returns nil if not found.
func (a *AdapterFileCtx) GetContent() *gvar.Var {
if v := a.Ctx.Value(ContextKeyContent); v != nil {
return gvar.New(v)
}
return nil
}

View File

@ -244,7 +244,7 @@ func (a *AdapterFile) GetFilePath(fileNameOrPath ...string) (filePath string, er
var (
fileExtName string
tempFileNameOrPath string
usedFileNameOrPath = a.defaultFileNameOrPath
usedFileNameOrPath = a.defaultFileNameOrPath.String()
)
if len(fileNameOrPath) > 0 {
usedFileNameOrPath = fileNameOrPath[0]

51
os/gcfg/gcfg_ctx_keys.go Normal file
View File

@ -0,0 +1,51 @@
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.
// Package gcfg provides reading, caching and managing for configuration.
package gcfg
import "github.com/gogf/gf/v2/os/gctx"
// Context key constants for configuration operations.
const (
// ContextKeyFileName is the context key for file name
ContextKeyFileName gctx.StrKey = "fileName"
// ContextKeyFilePath is the context key for file path
ContextKeyFilePath gctx.StrKey = "filePath"
// ContextKeyFileType is the context key for file type
ContextKeyFileType gctx.StrKey = "fileType"
// ContextKeyOperation is the context key for operation type
ContextKeyOperation gctx.StrKey = "operation"
// ContextKeyKey is the context key for key
ContextKeyKey gctx.StrKey = "key"
// ContextKeyValue is the context key for value
ContextKeyValue gctx.StrKey = "value"
// ContextKeyContent is the context key for set content
ContextKeyContent gctx.StrKey = "content"
)
// OperationType defines the type for configuration operation.
type OperationType string
// Operation constants for configuration operations.
const (
// OperationSet represents set operation
OperationSet OperationType = "set"
// OperationWrite represents write operation
OperationWrite OperationType = "write"
// OperationRename represents rename operation
OperationRename OperationType = "rename"
// OperationRemove represents remove operation
OperationRemove OperationType = "remove"
// OperationCreate represents create operation
OperationCreate OperationType = "create"
// OperationChmod represents chmod operation
OperationChmod OperationType = "chmod"
// OperationClear represents clear operation
OperationClear OperationType = "clear"
// OperationUpdate represents update operation
OperationUpdate OperationType = "update"
)

View File

@ -0,0 +1,62 @@
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.
package gcfg
import (
"context"
"github.com/gogf/gf/v2/container/gmap"
"github.com/gogf/gf/v2/internal/intlog"
)
// WatcherRegistry is a helper type for managing configuration watchers.
// It provides a unified implementation of watcher management to avoid code duplication
// across different adapter implementations.
type WatcherRegistry struct {
watchers *gmap.StrAnyMap // Watchers map storing watcher callbacks.
}
// NewWatcherRegistry creates and returns a new WatcherRegistry instance.
func NewWatcherRegistry() *WatcherRegistry {
return &WatcherRegistry{
watchers: gmap.NewStrAnyMap(true),
}
}
// Add adds a watcher with the specified name and callback function.
func (r *WatcherRegistry) Add(name string, fn func(ctx context.Context)) {
r.watchers.Set(name, fn)
}
// Remove removes the watcher with the specified name.
func (r *WatcherRegistry) Remove(name string) {
r.watchers.Remove(name)
}
// GetNames returns all watcher names.
func (r *WatcherRegistry) GetNames() []string {
return r.watchers.Keys()
}
// Notify notifies all registered watchers by calling their callback functions.
// Each callback is executed in a separate goroutine with panic recovery to prevent
// one watcher's panic from affecting others.
func (r *WatcherRegistry) Notify(ctx context.Context) {
r.watchers.Iterator(func(k string, v any) bool {
if fn, ok := v.(func(ctx context.Context)); ok {
go func(k string, fn func(ctx context.Context), ctx context.Context) {
defer func() {
if r := recover(); r != nil {
intlog.Errorf(ctx, "watcher %s panic: %v", k, r)
}
}()
fn(ctx)
}(k, fn, ctx)
}
return true
})
}

View File

@ -0,0 +1,85 @@
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.
package gcfg_test
import (
"context"
"sync"
"testing"
"github.com/gogf/gf/v2/os/gcfg"
"github.com/gogf/gf/v2/test/gtest"
)
func TestWatcherRegistry_Basic(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
registry := gcfg.NewWatcherRegistry()
// Test Add and GetNames
var (
wg sync.WaitGroup
called bool
)
wg.Add(1)
registry.Add("test-watcher", func(ctx context.Context) {
defer wg.Done()
called = true
})
names := registry.GetNames()
t.AssertEQ(len(names), 1)
t.AssertEQ(names[0], "test-watcher")
// Test Notify
registry.Notify(context.Background())
wg.Wait()
t.AssertEQ(called, true)
// Test Remove
registry.Remove("test-watcher")
names = registry.GetNames()
t.AssertEQ(len(names), 0)
})
}
func TestWatcherRegistry_MultipleWatchers(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
registry := gcfg.NewWatcherRegistry()
var (
wg sync.WaitGroup
count1, count2, count3 int
)
wg.Add(3)
registry.Add("watcher1", func(ctx context.Context) {
defer wg.Done()
count1++
})
registry.Add("watcher2", func(ctx context.Context) {
defer wg.Done()
count2++
})
registry.Add("watcher3", func(ctx context.Context) {
defer wg.Done()
count3++
})
names := registry.GetNames()
t.AssertEQ(len(names), 3)
registry.Notify(context.Background())
wg.Wait()
t.AssertEQ(count1, 1)
t.AssertEQ(count2, 1)
t.AssertEQ(count3, 1)
// Remove one watcher
registry.Remove("watcher2")
names = registry.GetNames()
t.AssertEQ(len(names), 2)
})
}

View File

@ -0,0 +1,273 @@
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.
package gcfg_test
import (
"context"
"testing"
"time"
"github.com/gogf/gf/v2/container/gmap"
"github.com/gogf/gf/v2/container/gtype"
"github.com/gogf/gf/v2/os/gcfg"
"github.com/gogf/gf/v2/os/gfile"
"github.com/gogf/gf/v2/test/gtest"
"github.com/gogf/gf/v2/util/guid"
)
func TestWatcher_File_Ctx(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
var (
key1 = "test-ctx"
configFile = guid.S() + ".toml"
content1 = `key = "value1"`
content2 = `key = "value2"`
)
// Create config file.
err := gfile.PutContents(configFile, content1)
t.AssertNil(err)
defer gfile.RemoveFile(configFile)
// Create config instance.
c, err := gcfg.NewAdapterFile(configFile)
t.AssertNil(err)
c.Data(context.Background())
c.AddWatcher(key1, func(ctx context.Context) {
fileCtx := gcfg.GetAdapterFileCtx(ctx)
t.Assert(fileCtx.GetOperation(), gcfg.OperationWrite)
t.Assert(fileCtx.GetFileName(), configFile)
t.Assert(fileCtx.GetFilePath(), gfile.Abs(configFile))
})
gfile.PutContents(configFile, content2)
time.Sleep(1 * time.Second)
c.AddWatcher(key1, func(ctx context.Context) {
fileCtx := gcfg.GetAdapterFileCtx(ctx)
t.Assert(fileCtx.GetOperation(), gcfg.OperationSet)
t.Assert(fileCtx.GetKey(), "key")
t.Assert(fileCtx.GetValue().String(), "value2")
})
c.Set("key", "value2")
time.Sleep(1 * time.Second)
c.RemoveWatcher(key1)
})
}
func TestWatcher_AddWatcherAndNotify(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
var (
m = gmap.NewStrAnyMap(true)
key1 = "test-watcher1"
key2 = "test-watcher2"
configFile = guid.S() + ".toml"
content1 = `key = "value1"`
content2 = `key = "value2"`
)
// Create config file.
err := gfile.PutContents(configFile, content1)
t.AssertNil(err)
defer gfile.RemoveFile(configFile)
// Create config instance.
c, err := gcfg.NewAdapterFile(configFile)
t.AssertNil(err)
m.Set(key1, true)
m.Set(key2, true)
// Add watchers.
c.AddWatcher(key1, func(ctx context.Context) {
m.Set(key1, false)
})
c.AddWatcher(key2, func(ctx context.Context) {
m.Set(key2, false)
})
// Check initial values.
t.Assert(c.MustGet(ctx, "key").String(), "value1")
t.Assert(m.Get(key1), true)
t.Assert(m.Get(key2), true)
// Update config file content.
err = gfile.PutContents(configFile, content2)
t.AssertNil(err)
// Wait for watching notification.
time.Sleep(1 * time.Second)
// Check updated values.
t.Assert(c.MustGet(ctx, "key").String(), "value2")
t.AssertEQ(m.Get(key1), false)
t.AssertEQ(m.Get(key2), false)
})
}
func TestWatcher_RemoveWatcher(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
var (
m = gmap.NewStrAnyMap(true)
key1 = "test-watcher1"
key2 = "test-watcher2"
configFile = guid.S() + ".toml"
content1 = `key = "value1"`
content2 = `key = "value2"`
)
err := gfile.PutContents(configFile, content1)
t.AssertNil(err)
defer gfile.RemoveFile(configFile)
// Create config instance.
c, err := gcfg.NewAdapterFile(configFile)
t.AssertNil(err)
m.Set(key1, true)
m.Set(key2, true)
// Add watchers.
c.AddWatcher(key1, func(ctx context.Context) {
m.Set(key1, false)
})
c.AddWatcher(key2, func(ctx context.Context) {
m.Set(key2, false)
})
// Check initial values.
t.Assert(c.MustGet(ctx, "key").String(), "value1")
t.Assert(m.Get(key1), true)
t.Assert(m.Get(key2), true)
// Remove one watcher.
c.RemoveWatcher(key2)
// Update config file content.
err = gfile.PutContents(configFile, content2)
t.AssertNil(err)
// Wait for watching notification.
time.Sleep(1 * time.Second)
// Check updated values.
t.Assert(c.MustGet(ctx, "key").String(), "value2")
t.AssertEQ(m.Get(key1), false)
// watcherName2 should not be notified as it was removed
t.AssertEQ(m.Get(key2), true)
})
}
func TestWatcher_SetContentNotify(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
var (
count = gtype.NewInt(0)
key = "test-watcher"
content1 = `key = "value1"`
content2 = `key = "value2"`
)
// Create config instance.
c, err := gcfg.NewAdapterContent(content1)
t.AssertNil(err)
// Add watcher.
c.AddWatcher(key, func(ctx context.Context) {
count.Add(1)
})
// Check initial values.
value, err := c.Get(ctx, "key")
t.AssertNil(err)
t.Assert(value, "value1")
t.Assert(count.Val(), 0)
// Set custom content.
c.SetContent(content2)
// Wait for watching notification.
time.Sleep(2 * time.Second)
// Check that watcher was notified
t.Assert(count.Val(), 1)
value2, err := c.Get(ctx, "key")
t.AssertNil(err)
t.Assert(value2, "value2")
})
}
func TestWatcher_RemoveContentNotify(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
var (
count = gtype.NewInt(0)
key = "test-watcher"
configFile = guid.S() + ".toml"
content = `key = "value1"`
)
// Create config file.
err := gfile.PutContents(configFile, content)
t.AssertNil(err)
defer gfile.RemoveFile(configFile)
// Create config instance.
c, err := gcfg.NewAdapterFile(configFile)
t.AssertNil(err)
// Add watcher.
c.AddWatcher(key, func(ctx context.Context) {
count.Add(1)
})
// Check initial values.
t.Assert(c.MustGet(ctx, "key").String(), "value1")
t.Assert(count.Val(), 0)
// Remove custom content.
c.RemoveContent(configFile)
// Wait for watching notification.
time.Sleep(1 * time.Second)
// Check that watcher was notified again
t.Assert(count.Val(), 1)
t.Assert(c.MustGet(ctx, "key").String(), "value1") // Back to file content
})
}
func TestWatcher_ClearContentNotify(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
var (
count = gtype.NewInt(0)
key = "test-watcher"
configFile = guid.S() + ".toml"
content = `key = "value1"`
)
// Create config file.
err := gfile.PutContents(configFile, content)
t.AssertNil(err)
defer gfile.RemoveFile(configFile)
// Create config instance.
c, err := gcfg.NewAdapterFile(configFile)
t.AssertNil(err)
// Add watcher.
c.AddWatcher(key, func(ctx context.Context) {
count.Add(1)
})
// Check initial values.
t.Assert(c.MustGet(ctx, "key").String(), "value1")
t.Assert(count.Val(), 0)
// Clear all custom content.
c.ClearContent()
// Wait for watching notification.
time.Sleep(1 * time.Second)
// Check that watcher was notified again
t.Assert(count.Val(), 1)
t.Assert(c.MustGet(ctx, "key").String(), "value1") // Back to file content
})
}