Repository: lf-edge/ekuiper
Pull request #2876 (2024-05-27 06:57:17)

feat: introduce bump version manager

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/lf-edge/ekuiper/pull/2876?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lf-edge) Report
Attention: Patch coverage is `48.64865%` with `38 lines` in your changes are missing coverage. Please review.
> Please [upload](https://docs.codecov.com/docs/codecov-uploader) report for BASE (`master-v2@e5d6e68`). [Learn more](https://docs.codecov.io/docs/error-reference?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lf-edge#section-missing-base-commit) about missing BASE report.

| [Files](https://app.codecov.io/gh/lf-edge/ekuiper/pull/2876?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lf-edge) | Patch % | Lines |
|---|---|---|
| [internal/server/bump/bump\_manager.go](https://app.codecov.io/gh/lf-edge/ekuiper/pull/2876?src=pr&el=tree&filepath=internal%2Fserver%2Fbump%2Fbump_manager.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lf-edge#diff-aW50ZXJuYWwvc2VydmVyL2J1bXAvYnVtcF9tYW5hZ2VyLmdv) | 48.28% | [20 Missing and 10 partials :warning: ](https://app.codecov.io/gh/lf-edge/ekuiper/pull/2876?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lf-edge) |
| [internal/conf/yaml\_config\_storage.go](https://app.codecov.io/gh/lf-edge/ekuiper/pull/2876?src=pr&el=tree&filepath=internal%2Fconf%2Fyaml_config_storage.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lf-edge#diff-aW50ZXJuYWwvY29uZi95YW1sX2NvbmZpZ19zdG9yYWdlLmdv) | 66.67% | [2 Missing and 2 partials :warning: ](https://app.codecov.io/gh/lf-edge/ekuiper/pull/2876?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lf-edge) |
| [internal/server/server.go](https://app.codecov.io/gh/lf-edge/ekuiper/pull/2876?src=pr&el=tree&filepath=internal%2Fserver%2Fserver.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lf-edge#diff-aW50ZXJuYWwvc2VydmVyL3NlcnZlci5nbw==) | 0.00% | [4 Missing :warning: ](https://app.codecov.io/gh/lf-edge/ekuiper/pull/2876?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lf-edge) |

<details><summary>Additional details and impacted files</summary>


```diff
@@             Coverage Diff              @@
##             master-v2    #2876   +/-   ##
============================================
  Coverage             ?   68.74%           
============================================
  Files                ?      277           
  Lines                ?    26374           
  Branches             ?        0           
============================================
  Hits                 ?    18129           
  Misses               ?     6610           
  Partials             ?     1635           
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/lf-edge/ekuiper/pull/2876?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lf-edge).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lf-edge).


Patch:
diff --git a/internal/conf/yaml_config_storage.go b/internal/conf/yaml_config_storage.go
index 5411cac9..9de1eb2a 100644
--- a/internal/conf/yaml_config_storage.go
+++ b/internal/conf/yaml_config_storage.go
@@ -175,3 +175,23 @@ func (s *sqlKVStore) GetByPrefix(prefix string) (map[string]map[string]interface
 	}
 	return r, nil
 }
+
+// WriteCfgIntoKVStorage only used for unit test
+func WriteCfgIntoKVStorage(typ string, plugin string, confKey string, confData map[string]interface{}) error {
+	key := buildKey(typ, plugin, confKey)
+	return saveCfgKeyToKV(key, confData)
+}
+
+// GotCfgFromKVStorage only used for unit test
+func GotCfgFromKVStorage(typ string, plugin string, confKey string) (map[string]interface{}, error) {
+	key := buildKey(typ, plugin, confKey)
+	kvStorage, err := getKVStorage()
+	if err != nil {
+		return nil, err
+	}
+	d, err := kvStorage.GetByPrefix(key)
+	if err != nil {
+		return nil, err
+	}
+	return d[key], nil
+}
diff --git a/internal/io/connection/connection_pool.go b/internal/io/connection/connection_pool.go
new file mode 100644
index 00000000..e829f880
--- /dev/null
+++ b/internal/io/connection/connection_pool.go
@@ -0,0 +1,258 @@
+// Copyright 2024 EMQ Technologies Co., Ltd.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package connection
+
+import (
+	"encoding/json"
+	"fmt"
+	"strings"
+	"sync"
+
+	"github.com/lf-edge/ekuiper/contract/v2/api"
+	"github.com/lf-edge/ekuiper/v2/internal/pkg/store"
+	"github.com/lf-edge/ekuiper/v2/internal/topo/context"
+	"github.com/lf-edge/ekuiper/v2/pkg/kv"
+	"github.com/lf-edge/ekuiper/v2/pkg/modules"
+)
+
+var isTest bool
+
+func GetAllConnectionsID() []string {
+	globalConnectionManager.RLock()
+	defer globalConnectionManager.RUnlock()
+	ids := make([]string, 0)
+	for key := range globalConnectionManager.connectionPool {
+		ids = append(ids, key)
+	}
+	return ids
+}
+
+func PingConnection(ctx api.StreamContext, id string) error {
+	conn, err := GetNameConnection(id)
+	if err != nil {
+		return err
+	}
+	return conn.Ping(ctx)
+}
+
+func GetNameConnection(selId string) (modules.Connection, error) {
+	if selId == "" {
+		return nil, fmt.Errorf("connection id should be defined")
+	}
+	globalConnectionManager.RLock()
+	defer globalConnectionManager.RUnlock()
+	meta, ok := globalConnectionManager.connectionPool[selId]
+	if !ok {
+		return nil, fmt.Errorf("connection %s not existed", selId)
+	}
+	return meta.conn, nil
+}
+
+func CreateNamedConnection(ctx api.StreamContext, id, typ string, props map[string]any) (modules.Connection, error) {
+	if id == "" || typ == "" {
+		return nil, fmt.Errorf("connection id and type should be defined")
+	}
+	globalConnectionManager.Lock()
+	defer globalConnectionManager.Unlock()
+	_, ok := globalConnectionManager.connectionPool[id]
+	if ok {
+		return nil, fmt.Errorf("connection %v already been created", id)
+	}
+	meta := ConnectionMeta{
+		ID:    id,
+		Typ:   typ,
+		Props: props,
+	}
+	if !isTest {
+		b, err := json.Marshal(meta)
+		if err != nil {
+			return nil, err
+		}
+		if err := globalConnectionManager.store.Set(id, string(b)); err != nil {
+			return nil, err
+		}
+	}
+	conn, err := createNamedConnection(ctx, meta)
+	if err != nil {
+		return nil, err
+	}
+	meta.conn = conn
+	globalConnectionManager.connectionPool[id] = meta
+	return conn, nil
+}
+
+func CreateNonStoredConnection(ctx api.StreamContext, id, typ string, props map[string]any) (modules.Connection, error) {
+	if id == "" || typ == "" {
+		return nil, fmt.Errorf("connection id and type should be defined")
+	}
+	globalConnectionManager.Lock()
+	defer globalConnectionManager.Unlock()
+	_, ok := globalConnectionManager.connectionPool[id]
+	if ok {
+		return nil, fmt.Errorf("connection %v already been created", id)
+	}
+	meta := ConnectionMeta{
+		ID:    id,
+		Typ:   typ,
+		Props: props,
+	}
+	conn, err := createNamedConnection(ctx, meta)
+	if err != nil {
+		return nil, err
+	}
+	meta.conn = conn
+	globalConnectionManager.connectionPool[id] = meta
+	return conn, nil
+}
+
+func DropNonStoredConnection(ctx api.StreamContext, selId string) error {
+	if selId == "" {
+		return fmt.Errorf("connection id should be defined")
+	}
+	globalConnectionManager.Lock()
+	defer globalConnectionManager.Unlock()
+	meta, ok := globalConnectionManager.connectionPool[selId]
+	if !ok {
+		return nil
+	}
+	conn := meta.conn
+	conn.Close(ctx)
+	delete(globalConnectionManager.connectionPool, selId)
+	return nil
+}
+
+func createNamedConnection(ctx api.StreamContext, meta ConnectionMeta) (modules.Connection, error) {
+	var conn modules.Connection
+	var err error
+	connRegister, ok := modules.ConnectionRegister[strings.ToLower(meta.Typ)]
+	if !ok {
+		return nil, fmt.Errorf("unknown connection type")
+	}
+	conn, err = connRegister(ctx, meta.ID, meta.Props)
+	if err != nil {
+		return nil, err
+	}
+	return conn, nil
+}
+
+func DropNameConnection(ctx api.StreamContext, selId string) error {
+	if selId == "" {
+		return fmt.Errorf("connection id should be defined")
+	}
+	globalConnectionManager.Lock()
+	defer globalConnectionManager.Unlock()
+	meta, ok := globalConnectionManager.connectionPool[selId]
+	if !ok {
+		return nil
+	}
+	conn := meta.conn
+	if conn.Ref(ctx) > 0 {
+		return fmt.Errorf("connection %s can't be dropped due to reference", selId)
+	}
+	if !isTest {
+		err := globalConnectionManager.store.Delete(selId)
+		if err != nil {
+			return fmt.Errorf("drop connection %s failed, err:%v", selId, err)
+		}
+	}
+	conn.Close(ctx)
+	delete(globalConnectionManager.connectionPool, selId)
+	return nil
+}
+
+var globalConnectionManager *ConnectionManager
+
+func InitConnectionManagerInTest() {
+	isTest = true
+	InitConnectionManager()
+}
+
+func InitConnectionManager() error {
+	globalConnectionManager = &ConnectionManager{
+		connectionPool: make(map[string]ConnectionMeta),
+	}
+	if !isTest {
+		globalConnectionManager.store, _ = store.GetKV("connectionMeta")
+		kvs, _ := globalConnectionManager.store.All()
+		for connectionID, raw := range kvs {
+			meta := ConnectionMeta{}
+			err := json.Unmarshal([]byte(raw), &meta)
+			if err != nil {
+				return fmt.Errorf("initialize connection:%v failed, err:%v", connectionID, err)
+			}
+			conn, err := createNamedConnection(context.Background(), meta)
+			if err != nil {
+				return fmt.Errorf("initialize connection:%v failed, err:%v", connectionID, err)
+			}
+			meta.conn = conn
+			globalConnectionManager.connectionPool[connectionID] = meta
+		}
+	}
+	return nil
+}
+
+type ConnectionManager struct {
+	sync.RWMutex
+	store          kv.KeyValue
+	connectionPool map[string]ConnectionMeta
+}
+
+type ConnectionMeta struct {
+	ID    string             `json:"id"`
+	Typ   string             `json:"typ"`
+	Props map[string]any     `json:"props"`
+	conn  modules.Connection `json:"-"`
+}
+
+type mockConnection struct {
+	id  string
+	ref int
+}
+
+func (m *mockConnection) Ping(ctx api.StreamContext) error {
+	return nil
+}
+
+func (m *mockConnection) Close(ctx api.StreamContext) {
+	return
+}
+
+func (m *mockConnection) Attach(ctx api.StreamContext) {
+	m.ref++
+	return
+}
+
+func (m *mockConnection) DetachSub(ctx api.StreamContext, props map[string]any) {
+	m.ref--
+	return
+}
+
+func (m *mockConnection) DetachPub(ctx api.StreamContext, props map[string]any) {
+	m.ref--
+	return
+}
+
+func (m *mockConnection) Ref(ctx api.StreamContext) int {
+	return m.ref
+}
+
+func CreateMockConnection(ctx api.StreamContext, id string, props map[string]any) (modules.Connection, error) {
+	m := &mockConnection{id: id, ref: 0}
+	return m, nil
+}
+
+func init() {
+	modules.ConnectionRegister["mock"] = CreateMockConnection
+}
diff --git a/internal/io/mqtt/connection.go b/internal/io/mqtt/client/client.go
similarity index 78%
rename from internal/io/mqtt/connection.go
rename to internal/io/mqtt/client/client.go
index 95132e1f..de87a527 100644
--- a/internal/io/mqtt/connection.go
+++ b/internal/io/mqtt/client/client.go
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package mqtt
+package client
 
 import (
 	"crypto/tls"
@@ -25,12 +25,16 @@ import (
 	"github.com/google/uuid"
 
 	"github.com/lf-edge/ekuiper/contract/v2/api"
-	"github.com/lf-edge/ekuiper/v2/internal/conf"
 	"github.com/lf-edge/ekuiper/v2/pkg/cast"
 	"github.com/lf-edge/ekuiper/v2/pkg/cert"
 	"github.com/lf-edge/ekuiper/v2/pkg/errorx"
+	"github.com/lf-edge/ekuiper/v2/pkg/modules"
 )
 
+func init() {
+	modules.ConnectionRegister["mqtt"] = CreateConnection
+}
+
 type Connection struct {
 	pahoMqtt.Client
 	selId     string
@@ -90,27 +94,27 @@ func (conn *Connection) onReconnecting(_ pahoMqtt.Client, _ *pahoMqtt.ClientOpti
 }
 
 // Do not call this directly. Call connection pool Attach method to get the connection
-func (conn *Connection) attach() {
+func (conn *Connection) Attach(ctx api.StreamContext) {
 	conn.refCount.Add(1)
 }
 
+func (conn *Connection) Ref(ctx api.StreamContext) int {
+	return int(conn.refCount.Load())
+}
+
 // Do not call this directly. Call connection pool Detach method to release the connection
-func (conn *Connection) detachSub(topic string) bool {
+func (conn *Connection) DetachSub(ctx api.StreamContext, props map[string]any) {
+	conn.refCount.Add(-1)
+	topic, err := getTopicFromProps(props)
+	if err != nil {
+		return
+	}
 	delete(conn.subscriptions, topic)
 	conn.Client.Unsubscribe(topic)
-	if conn.refCount.Add(-1) == 0 {
-		go conn.Close()
-		return true
-	}
-	return false
 }
 
-func (conn *Connection) detachPub() bool {
-	if conn.refCount.Add(-1) == 0 {
-		go conn.Close()
-		return true
-	}
-	return false
+func (conn *Connection) DetachPub(ctx api.StreamContext, props map[string]any) {
+	conn.refCount.Add(-1)
 }
 
 func (conn *Connection) Subscribe(topic string, info *SubscriptionInfo) error {
@@ -119,11 +123,11 @@ func (conn *Connection) Subscribe(topic string, info *SubscriptionInfo) error {
 	return handleToken(token)
 }
 
-func (conn *Connection) Close() {
+func (conn *Connection) Close(ctx api.StreamContext) {
 	conn.Client.Disconnect(1)
 }
 
-func (conn *Connection) Ping() error {
+func (conn *Connection) Ping(ctx api.StreamContext) error {
 	if conn.Client.IsConnected() {
 		return nil
 	} else {
@@ -131,22 +135,13 @@ func (conn *Connection) Ping() error {
 	}
 }
 
+func CreateConnection(ctx api.StreamContext, selId string, props map[string]any) (modules.Connection, error) {
+	return CreateClient(ctx, selId, props)
+}
+
 // CreateClient creates a new mqtt client. It is anonymous and does not require a name.
 func CreateClient(ctx api.StreamContext, selId string, props map[string]any) (*Connection, error) {
-	if selId != "" {
-		selectCfg := &conf.ConSelector{
-			ConnSelectorStr: selId,
-		}
-		if err := selectCfg.Init(); err != nil {
-			return nil, err
-		}
-		cf, err := selectCfg.ReadCfgFromYaml()
-		if err != nil {
-			return nil, err
-		}
-		props = cf
-	}
-	c, err := validateConfig(props)
+	c, err := ValidateConfig(props)
 	if err != nil {
 		return nil, err
 	}
@@ -180,7 +175,9 @@ func CreateClient(ctx api.StreamContext, selId string, props map[string]any) (*C
 	}
 	ctx.GetLogger().Infof("new mqtt client created")
 	con.Client = cli
-	con.attach()
+	if len(selId) > 0 {
+		con.Attach(ctx)
+	}
 	return con, nil
 }
 
@@ -193,7 +190,7 @@ func handleToken(token pahoMqtt.Token) error {
 	return nil
 }
 
-func validateConfig(props map[string]any) (*ConnectionConfig, error) {
+func ValidateConfig(props map[string]any) (*ConnectionConfig, error) {
 	c := &ConnectionConfig{PVersion: "3.1.1"}
 	err := cast.MapToStruct(props, c)
 	if err != nil {
@@ -224,6 +221,22 @@ func validateConfig(props map[string]any) (*ConnectionConfig, error) {
 	return c, nil
 }
 
-func (conn *Connection) GetClientId() string {
-	return conn.selId
+func CreateAnonymousConnection(ctx api.StreamContext, props map[string]any) (*Connection, error) {
+	cli, err := CreateClient(ctx, "", props)
+	if err != nil {
+		return nil, err
+	}
+	return cli, nil
+}
+
+const (
+	dataSourceProp = "datasource"
+)
+
+func getTopicFromProps(props map[string]any) (string, error) {
+	v, ok := props[dataSourceProp]
+	if ok {
+		return v.(string), nil
+	}
+	return "", fmt.Errorf("topic or datasource not defined")
 }
diff --git a/internal/io/mqtt/connection_pool.go b/internal/io/mqtt/connection_pool.go
deleted file mode 100644
index e63e1258..00000000
--- a/internal/io/mqtt/connection_pool.go
+++ /dev/null
@@ -1,59 +0,0 @@
-// Copyright 2024 EMQ Technologies Co., Ltd.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package mqtt
-
-import (
-	"sync"
-
-	"github.com/lf-edge/ekuiper/contract/v2/api"
-)
-
-var (
-	connectionPool = make(map[string]*Connection)
-	lock           sync.RWMutex
-)
-
-func GetConnection(ctx api.StreamContext, selId string, props map[string]any) (*Connection, error) {
-	if selId == "" {
-		return CreateClient(ctx, "", props)
-	}
-	lock.Lock()
-	defer lock.Unlock()
-	if conn, ok := connectionPool[selId]; ok {
-		conn.attach()
-		return conn, nil
-	} else {
-		cli, err := CreateClient(ctx, selId, props)
-		if err != nil {
-			return nil, err
-		}
-		connectionPool[selId] = cli
-		return cli, nil
-	}
-}
-
-func DetachConnection(conn *Connection, selId string, subscribedTopic string) {
-	var closed bool
-	if subscribedTopic != "" {
-		closed = conn.detachSub(subscribedTopic)
-	} else {
-		closed = conn.detachPub()
-	}
-	lock.Lock()
-	defer lock.Unlock()
-	if _, ok := connectionPool[selId]; closed && ok {
-		delete(connectionPool, selId)
-	}
-}
diff --git a/internal/io/mqtt/sink.go b/internal/io/mqtt/sink.go
index 791afe4f..0384464a 100644
--- a/internal/io/mqtt/sink.go
+++ b/internal/io/mqtt/sink.go
@@ -19,6 +19,8 @@ import (
 	"strings"
 
 	"github.com/lf-edge/ekuiper/contract/v2/api"
+	"github.com/lf-edge/ekuiper/v2/internal/io/connection"
+	"github.com/lf-edge/ekuiper/v2/internal/io/mqtt/client"
 	"github.com/lf-edge/ekuiper/v2/pkg/cast"
 )
 
@@ -33,11 +35,11 @@ type AdConf struct {
 type Sink struct {
 	adconf *AdConf
 	config map[string]interface{}
-	cli    *Connection
+	cli    *client.Connection
 }
 
 func (ms *Sink) Provision(_ api.StreamContext, ps map[string]any) error {
-	_, err := validateConfig(ps)
+	_, err := client.ValidateConfig(ps)
 	if err != nil {
 		return err
 	}
@@ -62,7 +64,26 @@ func (ms *Sink) Provision(_ api.StreamContext, ps map[string]any) error {
 
 func (ms *Sink) Connect(ctx api.StreamContext) error {
 	ctx.GetLogger().Infof("Connecting to mqtt server")
-	cli, err := GetConnection(ctx, ms.adconf.SelId, ms.config)
+	var cli *client.Connection
+	var err error
+	if len(ms.adconf.SelId) > 0 {
+		conn, err := connection.GetNameConnection(ms.adconf.SelId)
+		if err != nil {
+			return err
+		}
+		c, ok := conn.(*client.Connection)
+		if !ok {
+			return fmt.Errorf("connection %s should be mqtt connection", ms.adconf.SelId)
+		}
+		cli = c
+	} else {
+		id := fmt.Sprintf("%s-%s-%s-mqtt-sink", ctx.GetRuleId(), ctx.GetOpId(), ms.adconf.Tpc)
+		conn, err := connection.CreateNonStoredConnection(ctx, id, "mqtt", ms.config)
+		if err != nil {
+			return err
+		}
+		cli = conn.(*client.Connection)
+	}
 	ms.cli = cli
 	return err
 }
@@ -90,7 +111,12 @@ func (ms *Sink) Collect(ctx api.StreamContext, item api.RawTuple) error {
 func (ms *Sink) Close(ctx api.StreamContext) error {
 	ctx.GetLogger().Info("Closing mqtt sink connector")
 	if ms.cli != nil {
-		DetachConnection(ms.cli, ms.adconf.SelId, "")
+		if len(ms.adconf.SelId) < 1 {
+			id := fmt.Sprintf("%s-%s-%s-mqtt-sink", ctx.GetRuleId(), ctx.GetOpId(), ms.adconf.Tpc)
+			connection.DropNonStoredConnection(ctx, id)
+		} else {
+			ms.cli.DetachPub(ctx, nil)
+		}
 	}
 	return nil
 }
diff --git a/internal/io/mqtt/source.go b/internal/io/mqtt/source.go
index 8c132bef..c446a7ce 100644
--- a/internal/io/mqtt/source.go
+++ b/internal/io/mqtt/source.go
@@ -20,6 +20,8 @@ import (
 	pahoMqtt "github.com/eclipse/paho.mqtt.golang"
 
 	"github.com/lf-edge/ekuiper/contract/v2/api"
+	"github.com/lf-edge/ekuiper/v2/internal/io/connection"
+	"github.com/lf-edge/ekuiper/v2/internal/io/mqtt/client"
 	"github.com/lf-edge/ekuiper/v2/internal/topo/context"
 	"github.com/lf-edge/ekuiper/v2/pkg/cast"
 	"github.com/lf-edge/ekuiper/v2/pkg/timex"
@@ -32,7 +34,7 @@ type SourceConnector struct {
 	cfg   *Conf
 	props map[string]any
 
-	cli *Connection
+	cli *client.Connection
 }
 
 type Conf struct {
@@ -50,7 +52,7 @@ func (ms *SourceConnector) Provision(ctx api.StreamContext, props map[string]any
 	if cfg.Topic == "" {
 		return fmt.Errorf("topic is required")
 	}
-	_, err = validateConfig(props)
+	_, err = client.ValidateConfig(props)
 	if err != nil {
 		return err
 	}
@@ -60,18 +62,37 @@ func (ms *SourceConnector) Provision(ctx api.StreamContext, props map[string]any
 	return nil
 }
 
-func (ms *SourceConnector) Ping(props map[string]interface{}) error {
-	cli, err := CreateClient(context.Background(), "", ms.props)
+func (ms *SourceConnector) Ping(ctx api.StreamContext, props map[string]interface{}) error {
+	cli, err := client.CreateAnonymousConnection(context.Background(), props)
 	if err != nil {
 		return err
 	}
-	defer cli.Close()
-	return cli.Ping()
+	defer cli.Close(ctx)
+	return cli.Ping(ctx)
 }
 
 func (ms *SourceConnector) Connect(ctx api.StreamContext) error {
 	ctx.GetLogger().Infof("Connecting to mqtt server")
-	cli, err := GetConnection(ctx, ms.cfg.SelId, ms.props)
+	var cli *client.Connection
+	var err error
+	if len(ms.cfg.SelId) > 0 {
+		conn, err := connection.GetNameConnection(ms.cfg.SelId)
+		if err != nil {
+			return err
+		}
+		c, ok := conn.(*client.Connection)
+		if !ok {
+			return fmt.Errorf("connection %s should be mqtt connection", ms.cfg.SelId)
+		}
+		cli = c
+	} else {
+		id := fmt.Sprintf("%s-%s-%s-mqtt-source", ctx.GetRuleId(), ctx.GetOpId(), ms.tpc)
+		conn, err := connection.CreateNonStoredConnection(ctx, id, "mqtt", ms.props)
+		if err != nil {
+			return err
+		}
+		cli = conn.(*client.Connection)
+	}
 	ms.cli = cli
 	return err
 }
@@ -79,7 +100,7 @@ func (ms *SourceConnector) Connect(ctx api.StreamContext) error {
 // Subscribe is a one time only operation for source. It connects to the mqtt broker and subscribe to the topic
 // Run open before subscribe
 func (ms *SourceConnector) Subscribe(ctx api.StreamContext, ingest api.BytesIngest, ingestError api.ErrorIngest) error {
-	return ms.cli.Subscribe(ms.tpc, &SubscriptionInfo{
+	return ms.cli.Subscribe(ms.tpc, &client.SubscriptionInfo{
 		Qos: byte(ms.cfg.Qos),
 		Handler: func(client pahoMqtt.Client, message pahoMqtt.Message) {
 			ms.onMessage(ctx, message, ingest)
@@ -105,7 +126,12 @@ func (ms *SourceConnector) onMessage(ctx api.StreamContext, msg pahoMqtt.Message
 func (ms *SourceConnector) Close(ctx api.StreamContext) error {
 	ctx.GetLogger().Infof("Closing mqtt source connector to topic %s.", ms.tpc)
 	if ms.cli != nil {
-		DetachConnection(ms.cli, ms.cfg.SelId, ms.tpc)
+		if len(ms.cfg.SelId) < 1 {
+			id := fmt.Sprintf("%s-%s-%s-mqtt-source", ctx.GetRuleId(), ctx.GetOpId(), ms.tpc)
+			connection.DropNonStoredConnection(ctx, id)
+		} else {
+			ms.cli.DetachSub(ctx, ms.props)
+		}
 	}
 	return nil
 }
diff --git a/internal/server/bump/bump_manager.go b/internal/server/bump/bump_manager.go
new file mode 100644
index 00000000..2c6a0039
--- /dev/null
+++ b/internal/server/bump/bump_manager.go
@@ -0,0 +1,156 @@
+// Copyright 2024 EMQ Technologies Co., Ltd.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package bump
+
+import (
+	"errors"
+	"fmt"
+	"os"
+	"path"
+	"path/filepath"
+	"strings"
+
+	"github.com/pingcap/failpoint"
+
+	"github.com/lf-edge/ekuiper/v2/internal/conf"
+	"github.com/lf-edge/ekuiper/v2/internal/pkg/filex"
+	"github.com/lf-edge/ekuiper/v2/internal/pkg/store"
+	"github.com/lf-edge/ekuiper/v2/pkg/kv"
+)
+
+const currentVersion = 1
+
+var GlobalBumpManager *BumpManager
+
+type BumpManager struct {
+	Version int
+	store   kv.KeyValue
+}
+
+func InitBumpManager() error {
+	s, err := store.GetKV("$$eKuiperMeta")
+	failpoint.Inject("initManagerError", func() {
+		err = errors.New("initManagerError")
+	})
+	if err != nil {
+		return fmt.Errorf("init bump manager failed, err:%v", err)
+	}
+	b := &BumpManager{
+		store: s,
+	}
+	GlobalBumpManager = b
+	GlobalBumpManager.Version, err = loadVersionFromStorage()
+	return err
+}
+
+func loadVersionFromStorage() (int, error) {
+	var ver int
+	got, err := GlobalBumpManager.store.Get("version", &ver)
+	failpoint.Inject("loadVersionError", func() {
+		err = errors.New("loadVersionError")
+	})
+	if err != nil {
+		return 0, fmt.Errorf("init bump manager failed, err:%v", err)
+	}
+	if got {
+		return ver, nil
+	}
+	return 0, nil
+}
+
+func BumpToCurrentVersion(dataDir string) error {
+	for i := GlobalBumpManager.Version; i <= currentVersion; i++ {
+		switch i {
+		case 1:
+			if err := bumpFrom0To1(dataDir); err != nil {
+				return err
+			}
+		}
+	}
+	return nil
+}
+
+func bumpFrom0To1(dir string) error {
+	if err := migrateDataIntoStorage(dir, "sources"); err != nil {
+		return err
+	}
+	if err := migrateDataIntoStorage(dir, "connections"); err != nil {
+		return err
+	}
+	if err := migrateDataIntoStorage(dir, "sinks"); err != nil {
+		return err
+	}
+	if err := storeGlobalVersion(1); err != nil {
+		return err
+	}
+	GlobalBumpManager.Version = 1
+	return nil
+}
+
+func migrateDataIntoStorage(dataDir, confType string) error {
+	dir := path.Join(dataDir, confType)
+	files, err := os.ReadDir(dir)
+	failpoint.Inject("migrateReadError", func(val failpoint.Value) {
+		x := ""
+		switch val.(int) {
+		case 1:
+			x = "sources"
+		case 2:
+			x = "sinks"
+		case 3:
+			x = "connections"
+		}
+		if x == confType {
+			err = errors.New("migrateReadError")
+		}
+	})
+	if err != nil {
+		return err
+	}
+	for _, file := range files {
+		fname := file.Name()
+		if !strings.HasSuffix(fname, ".yaml") {
+			continue
+		}
+		pluginTyp := strings.TrimSuffix(fname, ".yaml")
+		filePath := filepath.Join(dir, fname)
+		confKeys := make(map[string]map[string]interface{})
+		err = filex.ReadYamlUnmarshal(filePath, &confKeys)
+		failpoint.Inject("migrateUnmarshalErr", func() {
+			err = errors.New("migrateUnmarshalErr")
+		})
+		if err != nil {
+			return err
+		}
+		for confKey, confData := range confKeys {
+			err = conf.WriteCfgIntoKVStorage(confType, pluginTyp, confKey, confData)
+			failpoint.Inject("migrateWriteErr", func() {
+				err = errors.New("migrateWriteErr")
+			})
+			if err != nil {
+				return err
+			}
+		}
+	}
+	return nil
+}
+
+func storeGlobalVersion(ver int) error {
+	err := GlobalBumpManager.store.Set("version", ver)
+	failpoint.Inject("storeVersionErr", func() {
+		err = errors.New("storeVersionErr")
+	})
+	return err
+}
diff --git a/internal/server/connection.go b/internal/server/connection.go
new file mode 100644
index 00000000..ee60d7a2
--- /dev/null
+++ b/internal/server/connection.go
@@ -0,0 +1,89 @@
+// Copyright 2021-2024 EMQ Technologies Co., Ltd.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package server
+
+import (
+	"encoding/json"
+	"io"
+	"net/http"
+
+	"github.com/gorilla/mux"
+
+	"github.com/lf-edge/ekuiper/v2/internal/io/connection"
+	"github.com/lf-edge/ekuiper/v2/internal/topo/context"
+)
+
+type ConnectionRequest struct {
+	ID    string                 `json:"id"`
+	Typ   string                 `json:"typ"`
+	Props map[string]interface{} `json:"props"`
+}
+
+func connectionsHandler(w http.ResponseWriter, r *http.Request) {
+	defer r.Body.Close()
+	switch r.Method {
+	case http.MethodPost:
+		body, err := io.ReadAll(r.Body)
+		if err != nil {
+			handleError(w, err, "Invalid body", logger)
+			return
+		}
+		req := &ConnectionRequest{}
+		if err := json.Unmarshal(body, req); err != nil {
+			handleError(w, err, "Invalid body", logger)
+			return
+		}
+		_, err = connection.CreateNamedConnection(context.Background(), req.ID, req.Typ, req.Props)
+		if err != nil {
+			handleError(w, err, "create connection failed", logger)
+			return
+		}
+		w.WriteHeader(http.StatusCreated)
+		w.Write([]byte("success"))
+	case http.MethodGet:
+		ids := connection.GetAllConnectionsID()
+		w.WriteHeader(http.StatusOK)
+		jsonResponse(ids, w, logger)
+	}
+}
+
+type ConnectionResponse struct {
+	ID  string `json:"id"`
+	Err string `json:"err"`
+}
+
+func connectionHandler(w http.ResponseWriter, r *http.Request) {
+	defer r.Body.Close()
+	id := mux.Vars(r)["id"]
+	switch r.Method {
+	case http.MethodGet:
+		res := &ConnectionResponse{
+			ID: id,
+		}
+		err := connection.PingConnection(context.Background(), id)
+		if err != nil {
+			res.Err = err.Error()
+		}
+		w.WriteHeader(http.StatusOK)
+		jsonResponse(res, w, logger)
+	case http.MethodDelete:
+		if err := connection.DropNameConnection(context.Background(), id); err != nil {
+			handleError(w, err, "drop connection failed", logger)
+			return
+		}
+		w.WriteHeader(http.StatusOK)
+		w.Write([]byte("success"))
+	}
+}
diff --git a/internal/server/rest.go b/internal/server/rest.go
index f505508f..6267e803 100644
--- a/internal/server/rest.go
+++ b/internal/server/rest.go
@@ -175,6 +175,8 @@ func createRestServer(ip string, port int, needToken bool) *http.Server {
 	r.HandleFunc("/data/export", configurationExportHandler).Methods(http.MethodGet, http.MethodPost)
 	r.HandleFunc("/data/import", configurationImportHandler).Methods(http.MethodPost)
 	r.HandleFunc("/data/import/status", configurationStatusHandler).Methods(http.MethodGet)
+	r.HandleFunc("/connections", connectionsHandler).Methods(http.MethodGet, http.MethodPost)
+	r.HandleFunc("/connection/{id}", connectionHandler).Methods(http.MethodGet, http.MethodDelete)
 	// r.HandleFunc("/connection/websocket", connectionHandler).Methods(http.MethodGet, http.MethodPost, http.MethodDelete)
 	// Register extended routes
 	for k, v := range components {
diff --git a/internal/server/server.go b/internal/server/server.go
index bff7b80a..cfaed7bd 100644
--- a/internal/server/server.go
+++ b/internal/server/server.go
@@ -35,6 +35,7 @@ import (
 	"github.com/lf-edge/ekuiper/v2/internal/binder/io"
 	"github.com/lf-edge/ekuiper/v2/internal/binder/meta"
 	"github.com/lf-edge/ekuiper/v2/internal/conf"
+	"github.com/lf-edge/ekuiper/v2/internal/io/connection"
 	"github.com/lf-edge/ekuiper/v2/internal/keyedstate"
 	meta2 "github.com/lf-edge/ekuiper/v2/internal/meta"
 	"github.com/lf-edge/ekuiper/v2/internal/pkg/def"
@@ -42,6 +43,7 @@ import (
 	"github.com/lf-edge/ekuiper/v2/internal/pkg/store"
 	"github.com/lf-edge/ekuiper/v2/internal/pkg/store/definition"
 	"github.com/lf-edge/ekuiper/v2/internal/processor"
+	"github.com/lf-edge/ekuiper/v2/internal/server/bump"
 	"github.com/lf-edge/ekuiper/v2/internal/server/promMetrics"
 	"github.com/lf-edge/ekuiper/v2/internal/topo/rule"
 	"github.com/lf-edge/ekuiper/v2/pkg/ast"
@@ -147,9 +149,21 @@ func StartUp(Version string) {
 	if err != nil {
 		panic(err)
 	}
+	if err := bump.InitBumpManager(); err != nil {
+		panic(err)
+	}
+	dataDir, _ := conf.GetDataLoc()
+	if err := bump.BumpToCurrentVersion(dataDir); err != nil {
+		panic(err)
+	}
+
 	keyedstate.InitKeyedStateKV()
 
 	meta2.InitYamlConfigManager()
+	if err := connection.InitConnectionManager(); err != nil {
+		panic(err)
+	}
+
 	ruleProcessor = processor.NewRuleProcessor()
 	streamProcessor = processor.NewStreamProcessor()
 	rulesetProcessor = processor.NewRulesetProcessor(ruleProcessor, streamProcessor)
diff --git a/pkg/modules/connection.go b/pkg/modules/connection.go
new file mode 100644
index 00000000..feab1007
--- /dev/null
+++ b/pkg/modules/connection.go
@@ -0,0 +1,34 @@
+// Copyright 2024 EMQ Technologies Co., Ltd.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package modules
+
+import "github.com/lf-edge/ekuiper/contract/v2/api"
+
+type Connection interface {
+	Ping(ctx api.StreamContext) error
+	Close(ctx api.StreamContext)
+	Attach(ctx api.StreamContext)
+	DetachSub(ctx api.StreamContext, props map[string]any)
+	DetachPub(ctx api.StreamContext, props map[string]any)
+	Ref(ctx api.StreamContext) int
+}
+
+type RegisterConnection func(ctx api.StreamContext, id string, props map[string]any) (Connection, error)
+
+var ConnectionRegister map[string]RegisterConnection
+
+func init() {
+	ConnectionRegister = map[string]RegisterConnection{}
+}


Test patch:
diff --git a/internal/io/connection/connection_pool_test.go b/internal/io/connection/connection_pool_test.go
new file mode 100644
index 00000000..dd0f4f9d
--- /dev/null
+++ b/internal/io/connection/connection_pool_test.go
@@ -0,0 +1,78 @@
+// Copyright 2024 EMQ Technologies Co., Ltd.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package connection
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/require"
+
+	"github.com/lf-edge/ekuiper/v2/internal/topo/context"
+)
+
+func TestConnection(t *testing.T) {
+	InitConnectionManagerInTest()
+	ctx := context.Background()
+	conn, err := CreateNamedConnection(ctx, "id1", "mock", nil)
+	require.NoError(t, err)
+	require.NotNil(t, conn)
+	require.NoError(t, conn.Ping(ctx))
+	_, err = CreateNamedConnection(ctx, "id1", "mock", nil)
+	require.Error(t, err)
+	require.Equal(t, 0, conn.Ref(ctx))
+	conn.Attach(ctx)
+	require.Equal(t, 1, conn.Ref(ctx))
+	conn.Attach(ctx)
+	require.Equal(t, 2, conn.Ref(ctx))
+	conn.DetachPub(ctx, nil)
+	require.Equal(t, 1, conn.Ref(ctx))
+	err = DropNameConnection(ctx, "id1")
+	require.Error(t, err)
+	conn2, err := GetNameConnection("id1")
+	require.NoError(t, err)
+	require.NotNil(t, conn2)
+	conn.DetachSub(ctx, nil)
+	require.Equal(t, 0, conn.Ref(ctx))
+	err = DropNameConnection(ctx, "id1")
+	require.NoError(t, err)
+	err = DropNameConnection(ctx, "id1")
+	require.NoError(t, err)
+	conn3, err := GetNameConnection("id1")
+	require.Error(t, err)
+	require.Nil(t, conn3)
+
+	_, err = CreateNamedConnection(ctx, "", "mock", nil)
+	require.Error(t, err)
+	err = DropNameConnection(ctx, "")
+	require.Error(t, err)
+	_, err = CreateNamedConnection(ctx, "12", "unknown", nil)
+	require.Error(t, err)
+	_, err = GetNameConnection("")
+	require.Error(t, err)
+	err = PingConnection(ctx, "")
+	require.Error(t, err)
+	_, err = CreateNonStoredConnection(ctx, "", "mock", nil)
+	require.Error(t, err)
+
+	conn4, err := CreateNonStoredConnection(ctx, "id2", "mock", nil)
+	require.NoError(t, err)
+	require.NotNil(t, conn4)
+	_, err = CreateNonStoredConnection(ctx, "id2", "mock", nil)
+	require.Error(t, err)
+	err = DropNonStoredConnection(ctx, "")
+	require.Error(t, err)
+	err = DropNonStoredConnection(ctx, "nonexists")
+	require.NoError(t, err)
+}
diff --git a/internal/io/mqtt/client/client_test.go b/internal/io/mqtt/client/client_test.go
new file mode 100644
index 00000000..b4e180bf
--- /dev/null
+++ b/internal/io/mqtt/client/client_test.go
@@ -0,0 +1,55 @@
+// Copyright 2024 EMQ Technologies Co., Ltd.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package client
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+// NOTICE!!! Need to run a MQTT broker in localhost:1883 for this test or change the url to your broker
+const url = "tcp://127.0.0.1:1883"
+
+func TestValidate(t *testing.T) {
+	tests := []struct {
+		name  string
+		props map[string]any
+		err   string
+	}{
+		{
+			name: "No server",
+			props: map[string]any{
+				"server": "",
+			},
+			err: "missing server property",
+		},
+		{
+			name: "invalid protocol",
+			props: map[string]any{
+				"server":          url,
+				"protocolVersion": "5.0",
+			},
+			err: "unsupported protocol version 5.0",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			_, err := ValidateConfig(tt.props)
+			assert.Error(t, err)
+			assert.Equal(t, tt.err, err.Error())
+		})
+	}
+}
diff --git a/internal/io/mqtt/connection_test.go b/internal/io/mqtt/connection_test.go
deleted file mode 100644
index 42704f30..00000000
--- a/internal/io/mqtt/connection_test.go
+++ /dev/null
@@ -1,94 +0,0 @@
-// Copyright 2024 EMQ Technologies Co., Ltd.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package mqtt
-
-import (
-	"testing"
-
-	"github.com/stretchr/testify/assert"
-
-	mockContext "github.com/lf-edge/ekuiper/v2/pkg/mock/context"
-)
-
-func TestConnectionLC(t *testing.T) {
-	propsShared := map[string]any{
-		"connectionSelector": "mqtt.localConnection",
-	}
-	propsNormal := map[string]any{
-		"server": url,
-	}
-	propsInvalid := map[string]any{
-		"server": "abc",
-	}
-	ctx := mockContext.NewMockContext("test", "op")
-	connShared1, err := GetConnection(ctx, "mqtt.localConnection", propsShared)
-	assert.NoError(t, err)
-	assert.Equal(t, 1, len(connectionPool))
-	_, err = GetConnection(ctx, "", propsNormal)
-	assert.NoError(t, err)
-	assert.Equal(t, 1, len(connectionPool))
-	connShared2, err := GetConnection(ctx, "mqtt.localConnection", propsShared)
-	assert.NoError(t, err)
-	assert.Equal(t, 1, len(connectionPool))
-	assert.Equal(t, connShared1, connShared2)
-	_, err = GetConnection(ctx, "", propsInvalid)
-	assert.Error(t, err)
-	assert.Equal(t, err.Error(), "found error when connecting for abc: network Error : dial tcp: address abc: missing port in address")
-	assert.Equal(t, 1, len(connectionPool))
-
-	c := connShared1.refCount.Load()
-	assert.Equal(t, int32(2), c)
-
-	err = connShared1.Ping()
-	assert.NoError(t, err)
-
-	// Test subscribe in the connector test.
-
-	DetachConnection(connShared1, "mqtt.localConnection", "")
-	assert.Equal(t, 1, len(connectionPool))
-	DetachConnection(connShared2, "mqtt.localConnection", "")
-	assert.Equal(t, 0, len(connectionPool))
-}
-
-func TestValidate(t *testing.T) {
-	tests := []struct {
-		name  string
-		props map[string]any
-		err   string
-	}{
-		{
-			name: "No server",
-			props: map[string]any{
-				"server": "",
-			},
-			err: "missing server property",
-		},
-		{
-			name: "invalid protocol",
-			props: map[string]any{
-				"server":          url,
-				"protocolVersion": "5.0",
-			},
-			err: "unsupported protocol version 5.0",
-		},
-	}
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			_, err := validateConfig(tt.props)
-			assert.Error(t, err)
-			assert.Equal(t, tt.err, err.Error())
-		})
-	}
-}
diff --git a/internal/io/mqtt/sink_test.go b/internal/io/mqtt/sink_test.go
index 903efd3e..5ce5af52 100644
--- a/internal/io/mqtt/sink_test.go
+++ b/internal/io/mqtt/sink_test.go
@@ -21,10 +21,12 @@ import (
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 
+	"github.com/lf-edge/ekuiper/v2/internal/io/connection"
 	mockContext "github.com/lf-edge/ekuiper/v2/pkg/mock/context"
 )
 
 func TestSinkConfigure(t *testing.T) {
+	connection.InitConnectionManagerInTest()
 	tests := []struct {
 		name           string
 		input          map[string]interface{}
@@ -135,6 +137,7 @@ func TestSinkConfigure(t *testing.T) {
 }
 
 func TestValidateMQTTSinkConf(t *testing.T) {
+	connection.InitConnectionManagerInTest()
 	testcases := []struct {
 		topic       string
 		expectError bool
diff --git a/internal/io/mqtt/source_sink_test.go b/internal/io/mqtt/source_sink_test.go
index 7a203db8..a7d4aeaf 100644
--- a/internal/io/mqtt/source_sink_test.go
+++ b/internal/io/mqtt/source_sink_test.go
@@ -20,12 +20,14 @@ import (
 	"github.com/stretchr/testify/assert"
 
 	"github.com/lf-edge/ekuiper/contract/v2/api"
+	"github.com/lf-edge/ekuiper/v2/internal/io/connection"
 	"github.com/lf-edge/ekuiper/v2/internal/topo/topotest/mockclock"
 	"github.com/lf-edge/ekuiper/v2/pkg/mock"
 	"github.com/lf-edge/ekuiper/v2/pkg/model"
 )
 
 func TestSourceSink(t *testing.T) {
+	connection.InitConnectionManagerInTest()
 	sc := GetSource().(api.BytesSource)
 	sk := GetSink().(api.BytesCollector)
 	mc := mockclock.GetMockClock()
@@ -57,10 +59,14 @@ func TestSourceSink(t *testing.T) {
 	mock.TestSourceConnector(t, sc, map[string]any{
 		"server":     url,
 		"datasource": "demo",
+		"qos":        0,
+		"topic":      "demo",
 	}, result, func() {
 		err := mock.RunBytesSinkCollect(sk, data, map[string]any{
-			"server": url,
-			"topic":  "demo",
+			"server":   url,
+			"topic":    "demo",
+			"qos":      0,
+			"retained": false,
 		})
 		assert.NoError(t, err)
 	})
diff --git a/internal/io/mqtt/source_test.go b/internal/io/mqtt/source_test.go
index 2bc05f24..6b74691e 100644
--- a/internal/io/mqtt/source_test.go
+++ b/internal/io/mqtt/source_test.go
@@ -21,6 +21,7 @@ import (
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 
+	"github.com/lf-edge/ekuiper/v2/internal/io/connection"
 	"github.com/lf-edge/ekuiper/v2/internal/testx"
 	mockContext "github.com/lf-edge/ekuiper/v2/pkg/mock/context"
 )
@@ -33,6 +34,7 @@ func init() {
 }
 
 func TestProvision(t *testing.T) {
+	connection.InitConnectionManagerInTest()
 	tests := []struct {
 		name  string
 		props map[string]any
diff --git a/internal/server/bump/bump_test.go b/internal/server/bump/bump_test.go
new file mode 100644
index 00000000..88603b23
--- /dev/null
+++ b/internal/server/bump/bump_test.go
@@ -0,0 +1,137 @@
+// Copyright 2024 EMQ Technologies Co., Ltd.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package bump
+
+import (
+	"os"
+	"path/filepath"
+	"testing"
+
+	"github.com/pingcap/failpoint"
+	"github.com/stretchr/testify/require"
+	"gopkg.in/yaml.v3"
+
+	"github.com/lf-edge/ekuiper/v2/internal/conf"
+	"github.com/lf-edge/ekuiper/v2/internal/pkg/store"
+)
+
+func TestBumpVersion(t *testing.T) {
+	conf.InitConf()
+	data, err := conf.GetDataLoc()
+	if err != nil {
+		t.Error(err)
+	}
+	err = store.SetupDefault(data)
+	if err != nil {
+		t.Error(err)
+	}
+	require.NoError(t, InitBumpManager())
+	GlobalBumpManager.Version = 0
+	testBumpVersion(t)
+}
+
+func prepareBumpVersion(t *testing.T, dir string) {
+	prepareBumpVersion1Data(t, dir, "sources")
+	prepareBumpVersion1Data(t, dir, "sinks")
+	prepareBumpVersion1Data(t, dir, "connections")
+}
+
+func testBumpVersion(t *testing.T) {
+	dir := os.TempDir()
+	prepareBumpVersion(t, dir)
+	require.NoError(t, BumpToCurrentVersion(dir))
+	require.Equal(t, 1, GlobalBumpManager.Version)
+	v, err := loadVersionFromStorage()
+	require.NoError(t, err)
+	require.Equal(t, 1, v)
+	assertBumpVersion1Data(t, "sources")
+	assertBumpVersion1Data(t, "sinks")
+	assertBumpVersion1Data(t, "connections")
+}
+
+func assertBumpVersion1Data(t *testing.T, typ string) {
+	d, err := conf.GotCfgFromKVStorage(typ, "mqtt", "conf1")
+	require.NoError(t, err)
+	require.Equal(t, map[string]interface{}{
+		"p1": 1,
+	}, d)
+	d, err = conf.GotCfgFromKVStorage(typ, "mqtt", "conf2")
+	require.NoError(t, err)
+	require.Equal(t, map[string]interface{}{
+		"p2": 2,
+	}, d)
+}
+
+func prepareBumpVersion1Data(t *testing.T, dir, typ string) {
+	sourceDir := filepath.Join(dir, typ)
+	require.NoError(t, os.MkdirAll(sourceDir, 0o755))
+	file, err := os.Create(filepath.Join(sourceDir, "mqtt.yaml"))
+	require.NoError(t, err)
+	defer file.Close()
+	m := make(map[string]map[string]interface{})
+	m["conf1"] = map[string]interface{}{
+		"p1": 1,
+	}
+	m["conf2"] = map[string]interface{}{
+		"p2": 2,
+	}
+	d, err := yaml.Marshal(m)
+	require.NoError(t, err)
+	_, err = file.Write(d)
+	require.NoError(t, err)
+}
+
+func TestBumpManager0To1(t *testing.T) {
+	conf.InitConf()
+	data, err := conf.GetDataLoc()
+	if err != nil {
+		t.Error(err)
+	}
+	err = store.SetupDefault(data)
+	if err != nil {
+		t.Error(err)
+	}
+
+	failpoint.Enable("github.com/lf-edge/ekuiper/v2/internal/server/bump/initManagerError", "return(true)")
+	require.Error(t, InitBumpManager())
+	failpoint.Disable("github.com/lf-edge/ekuiper/v2/internal/server/bump/initManagerError")
+	require.NoError(t, InitBumpManager())
+
+	failpoint.Enable("github.com/lf-edge/ekuiper/v2/internal/server/bump/loadVersionError", "return(true)")
+	_, err = loadVersionFromStorage()
+	require.Error(t, err)
+	failpoint.Disable("github.com/lf-edge/ekuiper/v2/internal/server/bump/loadVersionError")
+
+	dir := os.TempDir()
+	failpoint.Enable("github.com/lf-edge/ekuiper/v2/internal/server/bump/migrateReadError", "return(1)")
+	require.Error(t, bumpFrom0To1(dir))
+	failpoint.Enable("github.com/lf-edge/ekuiper/v2/internal/server/bump/migrateReadError", "return(2)")
+	require.Error(t, bumpFrom0To1(dir))
+	failpoint.Enable("github.com/lf-edge/ekuiper/v2/internal/server/bump/migrateReadError", "return(3)")
+	require.Error(t, bumpFrom0To1(dir))
+	failpoint.Disable("github.com/lf-edge/ekuiper/v2/internal/server/bump/migrateReadError")
+
+	failpoint.Enable("github.com/lf-edge/ekuiper/v2/internal/server/bump/storeVersionErr", "return(true)")
+	require.Error(t, bumpFrom0To1(dir))
+	failpoint.Disable("github.com/lf-edge/ekuiper/v2/internal/server/bump/storeVersionErr")
+
+	failpoint.Enable("github.com/lf-edge/ekuiper/v2/internal/server/bump/migrateUnmarshalErr", "return(true)")
+	require.Error(t, bumpFrom0To1(dir))
+	failpoint.Disable("github.com/lf-edge/ekuiper/v2/internal/server/bump/migrateUnmarshalErr")
+
+	failpoint.Enable("github.com/lf-edge/ekuiper/v2/internal/server/bump/migrateWriteErr", "return(true)")
+	require.Error(t, bumpFrom0To1(dir))
+	failpoint.Disable("github.com/lf-edge/ekuiper/v2/internal/server/bump/migrateWriteErr")
+}
diff --git a/internal/server/rest_test.go b/internal/server/rest_test.go
index 9e2b4119..df6239a3 100644
--- a/internal/server/rest_test.go
+++ b/internal/server/rest_test.go
@@ -32,6 +32,7 @@ import (
 	"github.com/stretchr/testify/suite"
 
 	"github.com/lf-edge/ekuiper/v2/internal/conf"
+	"github.com/lf-edge/ekuiper/v2/internal/io/connection"
 	"github.com/lf-edge/ekuiper/v2/internal/meta"
 	"github.com/lf-edge/ekuiper/v2/internal/pkg/def"
 	"github.com/lf-edge/ekuiper/v2/internal/pkg/store"
@@ -91,6 +92,8 @@ func (suite *RestTestSuite) SetupTest() {
 	r.HandleFunc("/data/export", configurationExportHandler).Methods(http.MethodGet, http.MethodPost)
 	r.HandleFunc("/data/import", configurationImportHandler).Methods(http.MethodPost)
 	r.HandleFunc("/data/import/status", configurationStatusHandler).Methods(http.MethodGet)
+	r.HandleFunc("/connections", connectionsHandler).Methods(http.MethodGet, http.MethodPost)
+	r.HandleFunc("/connection/{id}", connectionHandler).Methods(http.MethodGet, http.MethodDelete)
 	// r.HandleFunc("/connection/websocket", connectionHandler).Methods(http.MethodGet, http.MethodPost, http.MethodDelete)
 	r.HandleFunc("/metadata/sinks/{name}/confKeys/{confKey}", sinkConfKeyHandler).Methods(http.MethodDelete, http.MethodPut)
 	suite.r = r
@@ -802,3 +805,32 @@ func (suite *RestTestSuite) TestCreateDuplicateRule() {
 	returnVal, _ = io.ReadAll(w2.Result().Body)
 	require.Equal(suite.T(), `{"error":1000,"message":"rule test12345 already exists"}`+"\n", string(returnVal))
 }
+
+func (suite *RestTestSuite) TestConnection() {
+	connection.InitConnectionManagerInTest()
+	buf1 := bytes.NewBuffer([]byte(`{"id":"id1","typ":"mock","props":{}}`))
+	req1, _ := http.NewRequest(http.MethodPost, "http://localhost:8080/connections", buf1)
+	w1 := httptest.NewRecorder()
+	suite.r.ServeHTTP(w1, req1)
+	require.Equal(suite.T(), http.StatusCreated, w1.Code)
+
+	req, _ := http.NewRequest(http.MethodGet, "http://localhost:8080/connections", bytes.NewBufferString("any"))
+	w := httptest.NewRecorder()
+	suite.r.ServeHTTP(w, req)
+	assert.Equal(suite.T(), http.StatusOK, w.Code)
+	returnVal, _ := io.ReadAll(w.Result().Body)
+	require.Equal(suite.T(), `["id1"]`, string(returnVal))
+
+	req, _ = http.NewRequest(http.MethodGet, "http://localhost:8080/connection/id1", bytes.NewBufferString("any"))
+	w = httptest.NewRecorder()
+	suite.r.ServeHTTP(w, req)
+	assert.Equal(suite.T(), http.StatusOK, w.Code)
+
+	returnVal, _ = io.ReadAll(w.Result().Body)
+	require.Equal(suite.T(), `{"id":"id1","err":""}`, string(returnVal))
+
+	req, _ = http.NewRequest(http.MethodDelete, "http://localhost:8080/connection/id1", bytes.NewBufferString("any"))
+	w = httptest.NewRecorder()
+	suite.r.ServeHTTP(w, req)
+	assert.Equal(suite.T(), http.StatusOK, w.Code)
+}