diff --git a/datasource/dlock/dlock.go b/datasource/dlock/dlock.go new file mode 100644 index 000000000..a2d0ea4a3 --- /dev/null +++ b/datasource/dlock/dlock.go @@ -0,0 +1,33 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 dlock provide distributed lock function +package dlock + +import ( + "errors" +) + +var ErrDLockNotExists = errors.New("DLock do not exist") + +type DLock interface { + Lock(key string, ttl int64) error + TryLock(key string, ttl int64) error + Renew(key string) error + IsHoldLock(key string) bool + Unlock(key string) error +} diff --git a/datasource/dlock/dlock_test.go b/datasource/dlock/dlock_test.go new file mode 100644 index 000000000..640020d37 --- /dev/null +++ b/datasource/dlock/dlock_test.go @@ -0,0 +1,90 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 dlock_test + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "github.com/apache/servicecomb-service-center/datasource/dlock" + _ "github.com/apache/servicecomb-service-center/test" +) + +func TestDLock(t *testing.T) { + t.Run("test lock", func(t *testing.T) { + t.Run("lock the global key for 5s should pass", func(t *testing.T) { + err := dlock.Instance().Lock("global", 5) + assert.Nil(t, err) + isHold := dlock.Instance().IsHoldLock("global") + assert.Equal(t, true, isHold) + }) + t.Run("two locks fight for the same lock 5s, one lock should pass, another lock should fail", func(t *testing.T) { + err := dlock.Instance().Lock("same-lock", 5) + assert.Nil(t, err) + isHold := dlock.Instance().IsHoldLock("same-lock") + assert.Equal(t, true, isHold) + err = dlock.Instance().TryLock("same-lock", 5) + assert.NotNil(t, err) + }) + }) + t.Run("test try lock", func(t *testing.T) { + t.Run("try lock the try key for 5s should pass", func(t *testing.T) { + err := dlock.Instance().TryLock("try-lock", 5) + assert.Nil(t, err) + isHold := dlock.Instance().IsHoldLock("try-lock") + assert.Equal(t, true, isHold) + err = dlock.Instance().TryLock("try-lock", 5) + assert.NotNil(t, err) + }) + }) + t.Run("test renew", func(t *testing.T) { + t.Run("renew the renew key for 5s should pass", func(t *testing.T) { + err := dlock.Instance().Lock("renew", 5) + assert.Nil(t, err) + isHold := dlock.Instance().IsHoldLock("renew") + assert.Equal(t, true, isHold) + time.Sleep(3 * time.Second) + err = dlock.Instance().Renew("renew") + time.Sleep(2 * time.Second) + err = dlock.Instance().TryLock("renew", 5) + assert.NotNil(t, err) + }) + }) + t.Run("test isHoldLock", func(t *testing.T) { + t.Run("already owns the lock should pass", func(t *testing.T) { + err := dlock.Instance().Lock("hold-lock", 5) + assert.Nil(t, err) + isHold := dlock.Instance().IsHoldLock("hold-lock") + assert.Equal(t, true, isHold) + }) + t.Run("key does not exist should fail", func(t *testing.T) { + isHold := dlock.Instance().IsHoldLock("not-exist") + assert.Equal(t, false, isHold) + }) + }) + t.Run("test unlock", func(t *testing.T) { + t.Run("unlock the unlock key should pass", func(t *testing.T) { + err := dlock.Instance().Lock("unlock", 5) + assert.Nil(t, err) + err = dlock.Instance().Unlock("unlock") + assert.Nil(t, err) + }) + }) +} diff --git a/datasource/dlock/init.go b/datasource/dlock/init.go new file mode 100644 index 000000000..179b2552c --- /dev/null +++ b/datasource/dlock/init.go @@ -0,0 +1,56 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 dlock + +import ( + "fmt" + + "github.com/apache/servicecomb-service-center/pkg/log" +) + +type initFunc func(opts Options) (DLock, error) + +var ( + plugins = make(map[string]initFunc) + instance DLock +) + +func Install(pluginImplName string, f initFunc) { + plugins[pluginImplName] = f +} + +func Init(opts Options) error { + if opts.Kind == "" { + return nil + } + engineFunc, ok := plugins[opts.Kind] + if !ok { + return fmt.Errorf("plugin implement not supported [%s]", opts.Kind) + } + var err error + instance, err = engineFunc(opts) + if err != nil { + return err + } + log.Info(fmt.Sprintf("dlock plugin [%s] enabled", opts.Kind)) + return nil +} + +func Instance() DLock { + return instance +} diff --git a/datasource/dlock/options.go b/datasource/dlock/options.go new file mode 100644 index 000000000..1dfe18636 --- /dev/null +++ b/datasource/dlock/options.go @@ -0,0 +1,23 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 dlock + +// Options contains configuration for plugins +type Options struct { + Kind string +} diff --git a/datasource/etcd/dlock.go b/datasource/etcd/dlock.go new file mode 100644 index 000000000..13dcf2956 --- /dev/null +++ b/datasource/etcd/dlock.go @@ -0,0 +1,90 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 etcd + +import ( + "sync" + + "github.com/go-chassis/openlog" + "github.com/little-cui/etcdadpt" + + "github.com/apache/servicecomb-service-center/datasource/dlock" +) + +func init() { + dlock.Install("etcd", NewDLock) + dlock.Install("embeded_etcd", NewDLock) + dlock.Install("embedded_etcd", NewDLock) +} + +func NewDLock(opts dlock.Options) (dlock.DLock, error) { + return &DB{lockMap: sync.Map{}}, nil +} + +type DB struct { + lockMap sync.Map +} + +func (d *DB) Lock(key string, ttl int64) error { + lock, err := etcdadpt.Lock(key, ttl) + if err == nil { + d.lockMap.Store(key, lock) + } + return err +} + +func (d *DB) TryLock(key string, ttl int64) error { + lock, err := etcdadpt.TryLock(key, ttl) + if err == nil { + d.lockMap.Store(key, lock) + } + return err +} + +func (d *DB) Renew(key string) error { + if lock, ok := d.lockMap.Load(key); ok { + err := lock.(*etcdadpt.DLock).Refresh() + if err != nil { + openlog.Error("fail to renew key") + d.lockMap.Delete(key) + } + return err + } + return dlock.ErrDLockNotExists +} + +func (d *DB) IsHoldLock(key string) bool { + if lock, ok := d.lockMap.Load(key); ok { + if lock != nil { + return true + } + } + return false +} + +func (d *DB) Unlock(key string) error { + if lock, ok := d.lockMap.Load(key); ok { + err := lock.(*etcdadpt.DLock).Unlock() + if err != nil { + openlog.Error("fail to unlock") + } + d.lockMap.Delete(key) + return err + } + return dlock.ErrDLockNotExists +} diff --git a/datasource/manager.go b/datasource/manager.go index 5cb56dc65..0c78c719d 100644 --- a/datasource/manager.go +++ b/datasource/manager.go @@ -20,6 +20,7 @@ package datasource import ( "fmt" + "github.com/apache/servicecomb-service-center/datasource/dlock" "github.com/apache/servicecomb-service-center/datasource/rbac" "github.com/apache/servicecomb-service-center/datasource/schema" "github.com/apache/servicecomb-service-center/pkg/log" @@ -56,6 +57,14 @@ func Init(opts Options) error { if err != nil { return err } + + err = dlock.Init(dlock.Options{ + Kind: opts.Kind, + }) + if err != nil { + return err + } + return nil } diff --git a/go.mod b/go.mod index 63b01a4d1..882549220 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,6 @@ replace ( ) require ( - github.com/robfig/cron/v3 v3.0.1 github.com/NYTimes/gziphandler v1.1.1 github.com/apache/servicecomb-service-center/api v0.0.0 github.com/astaxie/beego v1.12.2 @@ -38,6 +37,7 @@ require ( github.com/prometheus/client_golang v1.11.0 github.com/prometheus/client_model v0.2.0 github.com/prometheus/procfs v0.6.0 + github.com/robfig/cron/v3 v3.0.1 github.com/rs/cors v1.7.0 // v1.1 github.com/satori/go.uuid v1.1.0 github.com/spf13/cobra v1.1.3 diff --git a/go.sum b/go.sum index 7f895cfe2..e1f4c4010 100644 --- a/go.sum +++ b/go.sum @@ -184,9 +184,6 @@ github.com/go-chassis/cari v0.5.1-0.20211208092532-78a52aa9d52e/go.mod h1:av/19f github.com/go-chassis/foundation v0.2.2-0.20201210043510-9f6d3de40234/go.mod h1:2PjwqpVwYEVaAldl5A58a08viH8p27pNeYaiE3ZxOBA= github.com/go-chassis/foundation v0.2.2/go.mod h1:2PjwqpVwYEVaAldl5A58a08viH8p27pNeYaiE3ZxOBA= github.com/go-chassis/foundation v0.3.0/go.mod h1:2PjwqpVwYEVaAldl5A58a08viH8p27pNeYaiE3ZxOBA= -github.com/go-chassis/foundation v0.3.1-0.20210806081520-3bd92d1ef787/go.mod h1:6NsIUaHghTFRGfCBcZN011zl196F6OR5QvD9N+P4oWU= -github.com/go-chassis/foundation v0.3.1-0.20210811025651-7f4d2b2b906c h1:6ooUyysnayGgoJHV++NLEcnnnXzsw3ud4VydjISGBqI= -github.com/go-chassis/foundation v0.3.1-0.20210811025651-7f4d2b2b906c/go.mod h1:6NsIUaHghTFRGfCBcZN011zl196F6OR5QvD9N+P4oWU= github.com/go-chassis/foundation v0.4.0 h1:z0xETnSxF+vRXWjoIhOdzt6rywjZ4sB++utEl4YgWEY= github.com/go-chassis/foundation v0.4.0/go.mod h1:6NsIUaHghTFRGfCBcZN011zl196F6OR5QvD9N+P4oWU= github.com/go-chassis/go-archaius v1.5.1 h1:1FrNyzzmD6o6BIjPF8uQ4Cc+u7qYIgQTpDk8uopBqfo= @@ -436,12 +433,6 @@ github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL github.com/ledisdb/ledisdb v0.0.0-20200510135210-d35789ec47e6/go.mod h1:n931TsDuKuq+uX4v1fulaMbA/7ZLLhjc85h7chZGBCQ= github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/little-cui/etcdadpt v0.2.1 h1:eT1A+BV1/2/dmmZA2Nl+cc7uTMuwd6T6DD+JrXr8xcA= -github.com/little-cui/etcdadpt v0.2.1/go.mod h1:727wftF2FS4vfkgFLmIvQue1XH+9u4lK2/hd6L7OAC8= -github.com/little-cui/etcdadpt v0.2.2-0.20211218040008-804e734f410b h1:GeNmlkSJKu1B8pNM18g+u8O/7BJNd9TP6KrfePy7Z6w= -github.com/little-cui/etcdadpt v0.2.2-0.20211218040008-804e734f410b/go.mod h1:HnRRpIrVEVNWobkiCvG2EHLWKKZ+L047EcI29ma2zA4= -github.com/little-cui/etcdadpt v0.2.2-0.20211222115540-fc5b1296d8b5 h1:SlD/2mPGjzkg2oj4ndoR9u/yadPcyE8om3njQEI65bE= -github.com/little-cui/etcdadpt v0.2.2-0.20211222115540-fc5b1296d8b5/go.mod h1:HnRRpIrVEVNWobkiCvG2EHLWKKZ+L047EcI29ma2zA4= github.com/little-cui/etcdadpt v0.3.1 h1:lAPIffcOR6jROu/mWf+zHscV8urIu1qbsJvwvziLWDY= github.com/little-cui/etcdadpt v0.3.1/go.mod h1:HnRRpIrVEVNWobkiCvG2EHLWKKZ+L047EcI29ma2zA4= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= diff --git a/server/service/dlock/dlock.go b/server/service/dlock/dlock.go new file mode 100644 index 000000000..ac7c914cd --- /dev/null +++ b/server/service/dlock/dlock.go @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 dlock provide distributed lock function +package dlock + +import ( + "github.com/apache/servicecomb-service-center/datasource/dlock" +) + +func Lock(key string, ttl int64) error { + return dlock.Instance().Lock(key, ttl) +} + +func TryLock(key string, ttl int64) error { + return dlock.Instance().TryLock(key, ttl) +} + +func Renew(key string) error { + return dlock.Instance().Renew(key) +} + +func IsHoldLock(key string) bool { + return dlock.Instance().IsHoldLock(key) +} + +func Unlock(key string) error { + return dlock.Instance().Unlock(key) +} diff --git a/server/service/dlock/dlock_test.go b/server/service/dlock/dlock_test.go new file mode 100644 index 000000000..625bb0ce9 --- /dev/null +++ b/server/service/dlock/dlock_test.go @@ -0,0 +1,94 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 dlock_test + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "github.com/apache/servicecomb-service-center/server/service/dlock" + "github.com/apache/servicecomb-service-center/test" + _ "github.com/apache/servicecomb-service-center/test" +) + +func TestDLock(t *testing.T) { + if !test.IsETCD() { + return + } + t.Run("test lock", func(t *testing.T) { + t.Run("lock the global key for 5s should pass", func(t *testing.T) { + err := dlock.Lock("global", 5) + assert.Nil(t, err) + isHold := dlock.IsHoldLock("global") + assert.Equal(t, true, isHold) + }) + t.Run("two locks fight for the same lock 5s, one lock should pass, another lock should fail", func(t *testing.T) { + err := dlock.Lock("same-lock", 5) + assert.Nil(t, err) + isHold := dlock.IsHoldLock("same-lock") + assert.Equal(t, true, isHold) + err = dlock.TryLock("same-lock", 5) + assert.NotNil(t, err) + }) + }) + t.Run("test try lock", func(t *testing.T) { + t.Run("try lock the try key for 5s should pass", func(t *testing.T) { + err := dlock.TryLock("try-lock", 5) + assert.Nil(t, err) + isHold := dlock.IsHoldLock("try-lock") + assert.Equal(t, true, isHold) + err = dlock.TryLock("try-lock", 5) + assert.NotNil(t, err) + }) + }) + t.Run("test renew", func(t *testing.T) { + t.Run("renew the renew key for 5s should pass", func(t *testing.T) { + err := dlock.Lock("renew", 5) + assert.Nil(t, err) + isHold := dlock.IsHoldLock("renew") + assert.Equal(t, true, isHold) + time.Sleep(3 * time.Second) + err = dlock.Renew("renew") + time.Sleep(2 * time.Second) + err = dlock.TryLock("renew", 5) + assert.NotNil(t, err) + }) + }) + t.Run("test isHoldLock", func(t *testing.T) { + t.Run("already owns the lock should pass", func(t *testing.T) { + err := dlock.Lock("hold-lock", 5) + assert.Nil(t, err) + isHold := dlock.IsHoldLock("hold-lock") + assert.Equal(t, true, isHold) + }) + t.Run("key does not exist should fail", func(t *testing.T) { + isHold := dlock.IsHoldLock("not-exist") + assert.Equal(t, false, isHold) + }) + }) + t.Run("test unlock", func(t *testing.T) { + t.Run("unlock the unlock key should pass", func(t *testing.T) { + err := dlock.Lock("unlock", 5) + assert.Nil(t, err) + err = dlock.Unlock("unlock") + assert.Nil(t, err) + }) + }) +}