diff --git a/cmd/pluginpkg/pluginpkg.go b/cmd/pluginpkg/pluginpkg.go index fe646c6d78471..e1b1db5a3dba3 100644 --- a/cmd/pluginpkg/pluginpkg.go +++ b/cmd/pluginpkg/pluginpkg.go @@ -23,6 +23,7 @@ import ( "os/exec" "path" "path/filepath" + "strings" "text/template" "time" @@ -98,6 +99,10 @@ func main() { manifest["buildTime"] = time.Now().String() pluginName := manifest["name"].(string) + if strings.Contains(pluginName, "-") { + log.Printf("plugin name should not contain '-'\n") + os.Exit(1) + } if pluginName != filepath.Base(pkgDir) { log.Printf("plugin package must be same with plugin name in manifest file\n") os.Exit(1) diff --git a/go.mod b/go.mod index 45ba9b40cc01a..14dac1b54382c 100644 --- a/go.mod +++ b/go.mod @@ -48,7 +48,7 @@ require ( github.com/pingcap/gofail v0.0.0-20181217135706-6a951c1e42c3 github.com/pingcap/goleveldb v0.0.0-20171020122428-b9ff6c35079e github.com/pingcap/kvproto v0.0.0-20181203065228-c14302da291c - github.com/pingcap/parser v0.0.0-20190108104142-35fab0be7fca + github.com/pingcap/parser v0.0.0-20190114015132-c5c6ec2eb454 github.com/pingcap/pd v2.1.0-rc.4+incompatible github.com/pingcap/tidb-tools v2.1.3-0.20190104033906-883b07a04a73+incompatible github.com/pingcap/tipb v0.0.0-20181012112600-11e33c750323 diff --git a/go.sum b/go.sum index c7358410e7861..1e5e93967713c 100644 --- a/go.sum +++ b/go.sum @@ -110,8 +110,8 @@ github.com/pingcap/goleveldb v0.0.0-20171020122428-b9ff6c35079e h1:P73/4dPCL96rG github.com/pingcap/goleveldb v0.0.0-20171020122428-b9ff6c35079e/go.mod h1:O17XtbryoCJhkKGbT62+L2OlrniwqiGLSqrmdHCMzZw= github.com/pingcap/kvproto v0.0.0-20181203065228-c14302da291c h1:Qf5St5XGwKgKQLar9lEXoeO0hJMVaFBj3JqvFguWtVg= github.com/pingcap/kvproto v0.0.0-20181203065228-c14302da291c/go.mod h1:Ja9XPjot9q4/3JyCZodnWDGNXt4pKemhIYCvVJM7P24= -github.com/pingcap/parser v0.0.0-20190108104142-35fab0be7fca h1:BIk063GpkHOJzR4Bp9I8xfsjp3nSvElvhIUjDgmInoo= -github.com/pingcap/parser v0.0.0-20190108104142-35fab0be7fca/go.mod h1:1FNvfp9+J0wvc4kl8eGNh7Rqrxveg15jJoWo/a0uHwA= +github.com/pingcap/parser v0.0.0-20190114015132-c5c6ec2eb454 h1:8wqFaAY5HLvDH35UkzMhtuxb4Q0fk6/yeiOscfOmMpo= +github.com/pingcap/parser v0.0.0-20190114015132-c5c6ec2eb454/go.mod h1:1FNvfp9+J0wvc4kl8eGNh7Rqrxveg15jJoWo/a0uHwA= github.com/pingcap/pd v2.1.0-rc.4+incompatible h1:/buwGk04aHO5odk/+O8ZOXGs4qkUjYTJ2UpCJXna8NE= github.com/pingcap/pd v2.1.0-rc.4+incompatible/go.mod h1:nD3+EoYes4+aNNODO99ES59V83MZSI+dFbhyr667a0E= github.com/pingcap/tidb-tools v2.1.3-0.20190104033906-883b07a04a73+incompatible h1:Ba48wwPwPq5hd1kkQpgua49dqB5cthC2zXVo7fUUDec= diff --git a/plugin/conn_ip_example/conn_ip_example.go b/plugin/conn_ip_example/conn_ip_example.go new file mode 100644 index 0000000000000..30cafec357fed --- /dev/null +++ b/plugin/conn_ip_example/conn_ip_example.go @@ -0,0 +1,49 @@ +// Copyright 2019 PingCAP, Inc. +// +// 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, +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "context" + "fmt" + + "github.com/pingcap/tidb/plugin" + "github.com/pingcap/tidb/sessionctx/variable" +) + +// Validate implements TiDB plugin's Validate SPI. +func Validate(ctx context.Context, m *plugin.Manifest) error { + fmt.Println("conn_ip_example validate called") + return nil +} + +// OnInit implements TiDB plugin's OnInit SPI. +func OnInit(ctx context.Context, manifest *plugin.Manifest) error { + fmt.Println("conn_ip_example init called") + fmt.Println("read cfg in init", manifest.SysVars["conn_ip_example_test_variable"].Value) + return nil +} + +// OnShutdown implements TiDB plugin's OnShutdown SPI. +func OnShutdown(ctx context.Context, manifest *plugin.Manifest) error { + fmt.Println("conn_ip_examples hutdown called") + return nil +} + +// NotifyEvent implements TiDB Audit plugin's NotifyEvent SPI. +func NotifyEvent(ctx context.Context) error { + fmt.Println("conn_ip_example notifiy called") + fmt.Println("variable test: ", variable.GetSysVar("conn_ip_example_test_variable").Value) + fmt.Printf("new connection by %s\n", ctx.Value("ip")) + return nil +} diff --git a/plugin/conn_ip_example/conn_ip_example_test.go b/plugin/conn_ip_example/conn_ip_example_test.go new file mode 100644 index 0000000000000..9c35f1e67f73e --- /dev/null +++ b/plugin/conn_ip_example/conn_ip_example_test.go @@ -0,0 +1,61 @@ +// Copyright 2019 PingCAP, Inc. +// +// 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, +// See the License for the specific language governing permissions and +// limitations under the License. + +package main_test + +import ( + "context" + + "github.com/pingcap/tidb/plugin" + "github.com/pingcap/tidb/sessionctx/variable" +) + +func Example_LoadRunShutdownPlugin() { + ctx := context.Background() + var pluginVarNames []string + cfg := plugin.Config{ + Plugins: []string{"conn_ip_example-1"}, + PluginDir: "/home/robi/Code/go/src/github.com/pingcap/tidb/plugin/conn_ip_example", + GlobalSysVar: &variable.SysVars, + PluginVarNames: &pluginVarNames, + } + + err := plugin.Init(ctx, cfg) + if err != nil { + panic(err) + } + + ps := plugin.GetByKind(plugin.Audit) + for _, auditPlugin := range ps { + if auditPlugin.State != plugin.Ready { + continue + } + plugin.DeclareAuditManifest(auditPlugin.Manifest).NotifyEvent(context.Background(), nil) + } + + err = plugin.Reload(ctx, cfg, plugin.ID("conn_ip_example-2")) + if err != nil { + panic(err) + } + + for _, auditPlugin := range plugin.GetByKind(plugin.Audit) { + if auditPlugin.State != plugin.Ready { + continue + } + plugin.DeclareAuditManifest(auditPlugin.Manifest).NotifyEvent( + context.WithValue(context.Background(), "ip", "1.1.1.2"), nil, + ) + } + + plugin.Shutdown(context.Background()) +} diff --git a/plugin/conn_ip_example/manifest.toml b/plugin/conn_ip_example/manifest.toml new file mode 100644 index 0000000000000..8f1a2c74ba7f8 --- /dev/null +++ b/plugin/conn_ip_example/manifest.toml @@ -0,0 +1,15 @@ +name = "conn_ip_example" +kind = "Audit" +description = "just a test" +version = "1" +license = "" +sysVars = [ + {name="conn_ip_example_test_variable", scope="Global", value="2"}, + {name="conn_ip_example_test_variable2", scope="Session", value="2"}, +] +validate = "Validate" +onInit = "OnInit" +onShutdown = "OnShutdown" +export = [ + {extPoint="NotifyEvent", impl="NotifyEvent"} +] diff --git a/plugin/const.go b/plugin/const.go new file mode 100644 index 0000000000000..88ba5432110d6 --- /dev/null +++ b/plugin/const.go @@ -0,0 +1,70 @@ +// Copyright 2019 PingCAP, Inc. +// +// 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, +// See the License for the specific language governing permissions and +// limitations under the License. + +package plugin + +// Kind presents the kind of plugin. +type Kind uint8 + +const ( + // Audit indicates it is a Audit plugin. + Audit Kind = 1 + iota + // Authentication indicate it is a Authentication plugin. + Authentication + // Schema indicate a plugin that can change TiDB schema. + Schema + // Daemon indicate a plugin that can run as daemon task. + Daemon +) + +func (k Kind) String() (str string) { + switch k { + case Audit: + str = "Audit" + case Authentication: + str = "Authentication" + case Schema: + str = "Schema" + case Daemon: + str = "Daemon" + } + return +} + +// State present the state of plugin. +type State uint8 + +const ( + // Uninitialized indicates plugin is uninitialized. + Uninitialized State = iota + // Ready indicates plugin is ready to work. + Ready + // Dying indicates plugin will be close soon. + Dying + // Disable indicate plugin is disabled. + Disable +) + +func (s State) String() (str string) { + switch s { + case Uninitialized: + str = "Uninitialized" + case Ready: + str = "Ready" + case Dying: + str = "Dying" + case Disable: + str = "Disable" + } + return +} diff --git a/plugin/errors.go b/plugin/errors.go new file mode 100644 index 0000000000000..938b5635cf4d4 --- /dev/null +++ b/plugin/errors.go @@ -0,0 +1,50 @@ +// Copyright 2019 PingCAP, Inc. +// +// 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, +// See the License for the specific language governing permissions and +// limitations under the License. + +package plugin + +import ( + "github.com/pingcap/parser/mysql" + "github.com/pingcap/parser/terror" +) + +var ( + errInvalidPluginID = createPluginError(mysql.ErrInvalidPluginID) + errInvalidPluginManifest = createPluginError(mysql.ErrInvalidPluginManifest) + errInvalidPluginName = createPluginError(mysql.ErrInvalidPluginName) + errInvalidPluginVersion = createPluginError(mysql.ErrInvalidPluginVersion) + errDuplicatePlugin = createPluginError(mysql.ErrDuplicatePlugin) + errInvalidPluginSysVarName = createPluginError(mysql.ErrInvalidPluginSysVarName) + errRequireVersionCheckFail = createPluginError(mysql.ErrRequireVersionCheckFail) + errUnsupportedReloadPlugin = createPluginError(mysql.ErrUnsupportedReloadPlugin) + errUnsupportedReloadPluginVar = createPluginError(mysql.ErrUnsupportedReloadPluginVar) +) + +func createPluginError(code terror.ErrCode) *terror.Error { + return terror.ClassPlugin.New(code, mysql.MySQLErrName[uint16(code)]) +} + +func init() { + pluginMySQLErrCodes := map[terror.ErrCode]uint16{ + mysql.ErrInvalidPluginID: mysql.ErrInvalidPluginID, + mysql.ErrInvalidPluginManifest: mysql.ErrInvalidPluginManifest, + mysql.ErrInvalidPluginName: mysql.ErrInvalidPluginName, + mysql.ErrInvalidPluginVersion: mysql.ErrInvalidPluginVersion, + mysql.ErrDuplicatePlugin: mysql.ErrDuplicatePlugin, + mysql.ErrInvalidPluginSysVarName: mysql.ErrInvalidPluginSysVarName, + mysql.ErrRequireVersionCheckFail: mysql.ErrRequireVersionCheckFail, + mysql.ErrUnsupportedReloadPlugin: mysql.ErrUnsupportedReloadPlugin, + mysql.ErrUnsupportedReloadPluginVar: mysql.ErrUnsupportedReloadPluginVar, + } + terror.ErrClassToMySQLCodes[terror.ClassPlugin] = pluginMySQLErrCodes +} diff --git a/plugin/helper.go b/plugin/helper.go new file mode 100644 index 0000000000000..1d81cd9ac952b --- /dev/null +++ b/plugin/helper.go @@ -0,0 +1,54 @@ +// Copyright 2019 PingCAP, Inc. +// +// 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, +// See the License for the specific language governing permissions and +// limitations under the License. + +package plugin + +import ( + "strings" + "unsafe" +) + +// DeclareAuditManifest declares manifest as AuditManifest. +func DeclareAuditManifest(m *Manifest) *AuditManifest { + return (*AuditManifest)(unsafe.Pointer(m)) +} + +// DeclareAuthenticationManifest declares manifest as AuthenticationManifest. +func DeclareAuthenticationManifest(m *Manifest) *AuthenticationManifest { + return (*AuthenticationManifest)(unsafe.Pointer(m)) +} + +// DeclareSchemaManifest declares manifest as SchemaManifest. +func DeclareSchemaManifest(m *Manifest) *SchemaManifest { + return (*SchemaManifest)(unsafe.Pointer(m)) +} + +// DeclareDaemonManifest declares manifest as DaemonManifest. +func DeclareDaemonManifest(m *Manifest) *DaemonManifest { + return (*DaemonManifest)(unsafe.Pointer(m)) +} + +// ID present plugin identity. +type ID string + +// Decode decodes a plugin id into name, version parts. +func (n ID) Decode() (name string, version string, err error) { + splits := strings.Split(string(n), "-") + if len(splits) != 2 { + err = errInvalidPluginID.GenWithStackByArgs(string(n)) + return + } + name = splits[0] + version = splits[1] + return +} diff --git a/plugin/plugin.go b/plugin/plugin.go new file mode 100644 index 0000000000000..a4a06fac15dbb --- /dev/null +++ b/plugin/plugin.go @@ -0,0 +1,373 @@ +// Copyright 2019 PingCAP, Inc. +// +// 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, +// See the License for the specific language governing permissions and +// limitations under the License. + +package plugin + +import ( + "context" + "path/filepath" + gplugin "plugin" + "strconv" + "strings" + "sync/atomic" + "unsafe" + + "github.com/pingcap/errors" + "github.com/pingcap/tidb/sessionctx/variable" +) + +// pluginGlobal holds all global variables for plugin. +var pluginGlobal copyOnWriteContext + +// copyOnWriteContext wraps a context follow COW idiom. +type copyOnWriteContext struct { + tiPlugins unsafe.Pointer // *plugins +} + +// plugins collects loaded plugins info. +type plugins struct { + plugins map[Kind][]Plugin + versions map[string]uint16 + dyingPlugins []Plugin +} + +// clone deep copies plugins info. +func (p *plugins) clone() *plugins { + np := &plugins{ + plugins: make(map[Kind][]Plugin, len(p.plugins)), + versions: make(map[string]uint16, len(p.versions)), + } + for key, value := range p.plugins { + np.plugins[key] = append([]Plugin(nil), value...) + } + for key, value := range p.versions { + np.versions[key] = value + } + for key, value := range p.dyingPlugins { + np.dyingPlugins[key] = value + } + return np +} + +// add adds a plugin to loaded plugin collection. +func (p plugins) add(plugin *Plugin) { + plugins, ok := p.plugins[plugin.Kind] + if !ok { + plugins = make([]Plugin, 0) + } + plugins = append(plugins, *plugin) + p.plugins[plugin.Kind] = plugins + p.versions[plugin.Name] = plugin.Version +} + +// plugins got plugin in COW context. +func (p copyOnWriteContext) plugins() *plugins { + return (*plugins)(atomic.LoadPointer(&p.tiPlugins)) +} + +// Config presents the init configuration for plugin framework. +type Config struct { + Plugins []string + PluginDir string + GlobalSysVar *map[string]*variable.SysVar + PluginVarNames *[]string + SkipWhenFail bool + EnvVersion map[string]uint16 +} + +// Plugin presents a TiDB plugin. +type Plugin struct { + *Manifest + library *gplugin.Plugin + State State + Path string +} + +type validateMode int + +const ( + initMode validateMode = iota + reloadMode +) + +func (p *Plugin) validate(ctx context.Context, tiPlugins *plugins, mode validateMode) error { + if mode == reloadMode { + var oldPlugin *Plugin + for i, item := range tiPlugins.plugins[p.Kind] { + if item.Name == p.Name { + oldPlugin = &tiPlugins.plugins[p.Kind][i] + break + } + } + if oldPlugin == nil { + return errUnsupportedReloadPlugin.GenWithStackByArgs(p.Name) + } + if len(p.SysVars) != len(oldPlugin.SysVars) { + return errUnsupportedReloadPluginVar.GenWithStackByArgs("") + } + for varName, varVal := range p.SysVars { + if oldPlugin.SysVars[varName] == nil || *oldPlugin.SysVars[varName] != *varVal { + return errUnsupportedReloadPluginVar.GenWithStackByArgs(varVal) + } + } + } + if p.RequireVersion != nil { + for component, reqVer := range p.RequireVersion { + if ver, ok := tiPlugins.versions[component]; !ok || ver < reqVer { + return errRequireVersionCheckFail.GenWithStackByArgs(p.Name, component, reqVer, ver) + } + } + } + if p.SysVars != nil { + for varName := range p.SysVars { + if !strings.HasPrefix(varName, p.Name) { + return errInvalidPluginSysVarName.GenWithStackByArgs(p.Name, varName, p.Name) + } + } + } + if p.Manifest.Validate != nil { + if err := p.Manifest.Validate(ctx, p.Manifest); err != nil { + return err + } + } + return nil +} + +// Init initializes the plugin and load plugin by config param. +// This method isn't thread-safe and must be called before any other plugin operation. +func Init(ctx context.Context, cfg Config) (err error) { + tiPlugins := &plugins{ + plugins: make(map[Kind][]Plugin), + versions: make(map[string]uint16), + dyingPlugins: make([]Plugin, 0), + } + + // Setup component version info for plugin running env. + for component, version := range cfg.EnvVersion { + tiPlugins.versions[component] = version + } + + // Load plugin dl & manifest. + for _, pluginID := range cfg.Plugins { + var pName string + pName, _, err = ID(pluginID).Decode() + if err != nil { + err = errors.Trace(err) + return + } + // Check duplicate. + _, dup := tiPlugins.versions[pName] + if dup { + if cfg.SkipWhenFail { + continue + } + err = errDuplicatePlugin.GenWithStackByArgs(pluginID) + return + } + // Load dl. + var plugin Plugin + plugin, err = loadOne(cfg.PluginDir, ID(pluginID)) + if err != nil { + if cfg.SkipWhenFail { + continue + } + return + } + tiPlugins.add(&plugin) + } + + // Cross validate & Load plugins. + for kind := range tiPlugins.plugins { + for i := range tiPlugins.plugins[kind] { + if err = tiPlugins.plugins[kind][i].validate(ctx, tiPlugins, initMode); err != nil { + if cfg.SkipWhenFail { + tiPlugins.plugins[kind][i].State = Disable + err = nil + continue + } + return + } + p := tiPlugins.plugins[kind][i] + if err = p.OnInit(ctx, p.Manifest); err != nil { + if cfg.SkipWhenFail { + tiPlugins.plugins[kind][i].State = Disable + err = nil + continue + } + return + } + if cfg.GlobalSysVar != nil { + for key, value := range tiPlugins.plugins[kind][i].SysVars { + (*cfg.GlobalSysVar)[key] = value + if value.Scope != variable.ScopeSession && cfg.PluginVarNames != nil { + *cfg.PluginVarNames = append(*cfg.PluginVarNames, key) + } + } + } + tiPlugins.plugins[kind][i].State = Ready + } + } + pluginGlobal = copyOnWriteContext{tiPlugins: unsafe.Pointer(tiPlugins)} + err = nil + return +} + +func loadOne(dir string, pluginID ID) (plugin Plugin, err error) { + plugin.Path = filepath.Join(dir, string(pluginID)+LibrarySuffix) + plugin.library, err = gplugin.Open(plugin.Path) + if err != nil { + err = errors.Trace(err) + return + } + manifestSym, err := plugin.library.Lookup(ManifestSymbol) + if err != nil { + err = errors.Trace(err) + return + } + manifest, ok := manifestSym.(func() *Manifest) + if !ok { + err = errInvalidPluginManifest.GenWithStackByArgs(string(pluginID)) + return + } + pName, pVersion, err := pluginID.Decode() + if err != nil { + err = errors.Trace(err) + return + } + plugin.Manifest = manifest() + if plugin.Name != pName { + err = errInvalidPluginName.GenWithStackByArgs(string(pluginID), plugin.Name) + return + } + if strconv.Itoa(int(plugin.Version)) != pVersion { + err = errInvalidPluginVersion.GenWithStackByArgs(string(pluginID)) + return + } + return +} + +// Reload hot swap a old plugin with new version. +// Limit: loaded plugins shouldn't be unload and only be mark dying. +func Reload(ctx context.Context, cfg Config, pluginID ID) (err error) { + newPlugin, err := loadOne(cfg.PluginDir, pluginID) + if err != nil { + return + } + _, err = replace(ctx, cfg, newPlugin.Name, newPlugin) + return +} + +func replace(ctx context.Context, cfg Config, name string, newPlugin Plugin) (replaced bool, err error) { + + oldPlugins := pluginGlobal.plugins() + if oldPlugins.versions[name] == newPlugin.Version { + replaced = false + return + } + err = newPlugin.validate(ctx, oldPlugins, reloadMode) + if err != nil { + return + } + err = newPlugin.OnInit(ctx, newPlugin.Manifest) + if err != nil { + return + } + if cfg.GlobalSysVar != nil { + for key, value := range newPlugin.SysVars { + (*cfg.GlobalSysVar)[key] = value + } + } + + for { + oldPlugins = pluginGlobal.plugins() + newPlugins := oldPlugins.clone() + replaced = true + tiPluginKind := newPlugins.plugins[newPlugin.Kind] + var oldPlugin *Plugin + for i, p := range tiPluginKind { + if p.Name == name { + oldPlugin = &tiPluginKind[i] + tiPluginKind = append(tiPluginKind[:i], tiPluginKind[i+1:]...) + } + } + + if oldPlugin != nil { + oldPlugin.State = Dying + newPlugins.dyingPlugins = append(newPlugins.dyingPlugins, *oldPlugin) + err = oldPlugin.OnShutdown(ctx, oldPlugin.Manifest) + if err != nil { + // When shutdown failure, the plugin is in stranger state, so make it as Dying. + return + } + } + + newPlugin.State = Ready + tiPluginKind = append(tiPluginKind, newPlugin) + newPlugins.plugins[newPlugin.Kind] = tiPluginKind + newPlugins.versions[newPlugin.Name] = newPlugin.Version + + if atomic.CompareAndSwapPointer(&pluginGlobal.tiPlugins, unsafe.Pointer(oldPlugins), unsafe.Pointer(newPlugins)) { + return + } + } +} + +// Shutdown cleanups all plugin resources. +// Notice: it just cleanups the resource of plugin, but cannot unload plugins(limited by go plugin). +func Shutdown(ctx context.Context) { + for { + tiPlugins := pluginGlobal.plugins() + for _, plugins := range tiPlugins.plugins { + for _, p := range plugins { + p.State = Dying + if err := p.OnShutdown(ctx, p.Manifest); err != nil { + } + } + } + if atomic.CompareAndSwapPointer(&pluginGlobal.tiPlugins, unsafe.Pointer(tiPlugins), nil) { + return + } + } +} + +// Get finds and returns plugin by kind and name parameters. +func Get(kind Kind, name string) *Plugin { + plugins := pluginGlobal.plugins() + if plugins == nil { + return nil + } + for _, p := range plugins.plugins[kind] { + if p.Name == name { + return &p + } + } + return nil +} + +// GetByKind finds and returns plugin by kind parameters. +func GetByKind(kind Kind) []Plugin { + plugins := pluginGlobal.plugins() + if plugins == nil { + return nil + } + return plugins.plugins[kind] +} + +// GetAll finds and returns all plugins. +func GetAll() map[Kind][]Plugin { + plugins := pluginGlobal.plugins() + if plugins == nil { + return nil + } + return plugins.plugins +} diff --git a/plugin/spi.go b/plugin/spi.go new file mode 100644 index 0000000000000..15684f7b0e154 --- /dev/null +++ b/plugin/spi.go @@ -0,0 +1,77 @@ +// Copyright 2019 PingCAP, Inc. +// +// 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, +// See the License for the specific language governing permissions and +// limitations under the License. + +package plugin + +import ( + "context" + "reflect" + "unsafe" + + "github.com/pingcap/tidb/sessionctx/variable" +) + +const ( + // LibrarySuffix defines TiDB plugin's file suffix. + LibrarySuffix = ".so" + // ManifestSymbol defines TiDB plugin's entrance symbol. + // Plugin take manifest info from this symbol. + ManifestSymbol = "PluginManifest" +) + +// Manifest describes plugin info and how it can do by plugin itself. +type Manifest struct { + Kind Kind + Name string + Description string + Version uint16 + RequireVersion map[string]uint16 + License string + BuildTime string + SysVars map[string]*variable.SysVar + Validate func(ctx context.Context, manifest *Manifest) error + OnInit func(ctx context.Context, manifest *Manifest) error + OnShutdown func(ctx context.Context, manifest *Manifest) error +} + +// ExportManifest exports a manifest to TiDB as a known format. +// it just casts sub-manifest to manifest. +func ExportManifest(m interface{}) *Manifest { + v := reflect.ValueOf(m) + return (*Manifest)(unsafe.Pointer(v.Pointer())) +} + +// AuditManifest presents a sub-manifest that every audit plugin must provide. +type AuditManifest struct { + Manifest + NotifyEvent func(ctx context.Context, sctx *variable.SessionVars) error +} + +// AuthenticationManifest presents a sub-manifest that every audit plugin must provide. +type AuthenticationManifest struct { + Manifest + AuthenticateUser func() + GenerateAuthenticationString func() + ValidateAuthenticationString func() + SetSalt func() +} + +// SchemaManifest presents a sub-manifest that every schema plugins must provide. +type SchemaManifest struct { + Manifest +} + +// DaemonManifest presents a sub-manifest that every DaemonManifest plugins must provide. +type DaemonManifest struct { + Manifest +} diff --git a/plugin/spi_test.go b/plugin/spi_test.go new file mode 100644 index 0000000000000..98e676acfcc3a --- /dev/null +++ b/plugin/spi_test.go @@ -0,0 +1,51 @@ +// Copyright 2019 PingCAP, Inc. +// +// 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, +// See the License for the specific language governing permissions and +// limitations under the License. + +package plugin_test + +import ( + "context" + "testing" + + "github.com/pingcap/tidb/plugin" + "github.com/pingcap/tidb/sessionctx/variable" +) + +func TestExportManifest(t *testing.T) { + callRecorder := struct { + OnInitCalled bool + NotifyEventCalled bool + }{} + manifest := &plugin.AuditManifest{ + Manifest: plugin.Manifest{ + Kind: plugin.Authentication, + Name: "test audit", + Version: 1, + OnInit: func(ctx context.Context, manifest *plugin.Manifest) error { + callRecorder.OnInitCalled = true + return nil + }, + }, + NotifyEvent: func(ctx context.Context, sctx *variable.SessionVars) error { + callRecorder.NotifyEventCalled = true + return nil + }, + } + exported := plugin.ExportManifest(manifest) + exported.OnInit(context.Background(), exported) + audit := plugin.DeclareAuditManifest(exported) + audit.NotifyEvent(context.Background(), nil) + if !callRecorder.NotifyEventCalled || !callRecorder.OnInitCalled { + t.Fatalf("export test failure") + } +}