From 49c59726e7a797c01f3ae3648d2eea25fc22f9fa Mon Sep 17 00:00:00 2001 From: lysu Date: Mon, 25 Mar 2019 16:37:00 +0800 Subject: [PATCH] plugin: introduce plugin framework (#8788) (#9880) --- cmd/pluginpkg/pluginpkg.go | 159 ++++++++ go.mod | 2 + go.sum | 4 +- plugin/conn_ip_example/conn_ip_example.go | 49 +++ .../conn_ip_example/conn_ip_example_test.go | 61 +++ plugin/conn_ip_example/manifest.toml | 15 + plugin/const.go | 70 ++++ plugin/errors.go | 50 +++ plugin/helper.go | 54 +++ plugin/plugin.go | 373 ++++++++++++++++++ plugin/spi.go | 77 ++++ plugin/spi_test.go | 51 +++ 12 files changed, 963 insertions(+), 2 deletions(-) create mode 100644 cmd/pluginpkg/pluginpkg.go create mode 100644 plugin/conn_ip_example/conn_ip_example.go create mode 100644 plugin/conn_ip_example/conn_ip_example_test.go create mode 100644 plugin/conn_ip_example/manifest.toml create mode 100644 plugin/const.go create mode 100644 plugin/errors.go create mode 100644 plugin/helper.go create mode 100644 plugin/plugin.go create mode 100644 plugin/spi.go create mode 100644 plugin/spi_test.go diff --git a/cmd/pluginpkg/pluginpkg.go b/cmd/pluginpkg/pluginpkg.go new file mode 100644 index 0000000000000..e1b1db5a3dba3 --- /dev/null +++ b/cmd/pluginpkg/pluginpkg.go @@ -0,0 +1,159 @@ +// Copyright 2018 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" + "encoding/json" + "flag" + "fmt" + "log" + "os" + "os/exec" + "path" + "path/filepath" + "strings" + "text/template" + "time" + + "github.com/BurntSushi/toml" +) + +var ( + pkgDir string + outDir string +) + +const codeTemplate = ` +package main + +import ( + "github.com/pingcap/tidb/plugin" + "github.com/pingcap/tidb/sessionctx/variable" +) + +func PluginManifest() *plugin.Manifest { + return plugin.ExportManifest(&plugin.{{.kind}}Manifest{ + Manifest: plugin.Manifest{ + Kind: plugin.{{.kind}}, + Name: "{{.name}}", + Description: "{{.description}}", + Version: {{.version}}, + RequireVersion: map[string]uint16{}, + License: "{{.license}}", + BuildTime: "{{.buildTime}}", + SysVars: map[string]*variable.SysVar{ + {{range .sysVars}} + "{{.name}}": { + Scope: variable.Scope{{.scope}}, + Name: "{{.name}}", + Value: "{{.value}}", + }, + {{end}} + }, + Validate: {{.validate}}, + OnInit: {{.onInit}}, + OnShutdown: {{.onShutdown}}, + }, + {{range .export}} + {{.extPoint}}: {{.impl}}, + {{end}} + }) +} +` + +func init() { + flag.StringVar(&pkgDir, "pkg-dir", "", "plugin package folder path") + flag.StringVar(&outDir, "out-dir", "", "plugin packaged folder path") + flag.Usage = usage +} + +func usage() { + log.Printf("Usage: %s --pkg-dir [plugin source pkg folder] --outDir-dir [outDir-dir]\n", path.Base(os.Args[0])) + flag.PrintDefaults() + os.Exit(1) +} + +func main() { + flag.Parse() + if pkgDir == "" || outDir == "" { + flag.Usage() + } + var manifest map[string]interface{} + _, err := toml.DecodeFile(filepath.Join(pkgDir, "manifest.toml"), &manifest) + if err != nil { + log.Printf("read pkg %s's manifest failure, %+v\n", pkgDir, err) + os.Exit(1) + } + 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) + } + + version := manifest["version"].(string) + tmpl, err := template.New("gen-plugin").Parse(codeTemplate) + if err != nil { + log.Printf("generate code failure during parse template, %+v\n", err) + os.Exit(1) + } + + genFileName := filepath.Join(pkgDir, filepath.Base(pkgDir)+".gen.go") + genFile, err := os.OpenFile(genFileName, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755) + if err != nil { + log.Printf("generate code failure during prepare output file, %+v\n", err) + os.Exit(1) + } + defer func() { + err1 := os.Remove(genFileName) + if err1 != nil { + log.Printf("remove tmp file %s failure, please clean up manually at %v", genFileName, err1) + } + }() + + err = tmpl.Execute(genFile, manifest) + if err != nil { + log.Printf("generate code failure during generating code, %+v\n", err) + os.Exit(1) + } + + outputFile := filepath.Join(outDir, pluginName+"-"+version+".so") + pluginPath := `-pluginpath=` + pluginName + "-" + version + ctx := context.Background() + buildCmd := exec.CommandContext(ctx, "go", "build", + "-ldflags", pluginPath, + "-buildmode=plugin", + "-o", outputFile, pkgDir) + buildCmd.Stderr = os.Stderr + buildCmd.Stdout = os.Stdout + buildCmd.Env = append(os.Environ(), "GO111MODULE=on") + err = buildCmd.Run() + if err != nil { + log.Printf("compile plugin source code failure, %+v\n", err) + os.Exit(1) + } + fmt.Printf(`Package "%s" as plugin "%s" success.`+"\nManifest:\n", pkgDir, outputFile) + encoder := json.NewEncoder(os.Stdout) + encoder.SetIndent(" ", "\t") + err = encoder.Encode(manifest) + if err != nil { + log.Printf("print manifest detail failure, err: %v", err) + } +} diff --git a/go.mod b/go.mod index fbe0a10ca28cd..cdf05c3016f22 100644 --- a/go.mod +++ b/go.mod @@ -81,3 +81,5 @@ require ( gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect gopkg.in/yaml.v2 v2.2.1 // indirect ) + +replace github.com/pingcap/parser => github.com/lysu/parser v0.0.0-20190325074808-d880cf39390b diff --git a/go.sum b/go.sum index efdd9c0179d8b..236bb1ebe2a46 100644 --- a/go.sum +++ b/go.sum @@ -66,6 +66,8 @@ github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/jonboulle/clockwork v0.1.0 h1:VKV+ZcuP6l3yW9doeqz6ziZGgcynBVQO+obU0+0hcPo= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/lysu/parser v0.0.0-20190325074808-d880cf39390b h1:jWTIO8rWK9pg3RO6eu5/5M386uY07wE+lYq0WKVkaVA= +github.com/lysu/parser v0.0.0-20190325074808-d880cf39390b/go.mod h1:CJk6LPzPxAcwHIcTugQaKxzvTR10NDJ5ln8XR7uYTJk= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/montanaflynn/stats v0.0.0-20151014174947-eeaced052adb h1:bsjNADsjHq0gjU7KO7zwoX5k3HtFdf6TDzB3ncl5iUs= @@ -92,8 +94,6 @@ github.com/pingcap/goleveldb v0.0.0-20171020084629-8d44bfdf1030 h1:XJLuW0lsP7vAt github.com/pingcap/goleveldb v0.0.0-20171020084629-8d44bfdf1030/go.mod h1:O17XtbryoCJhkKGbT62+L2OlrniwqiGLSqrmdHCMzZw= github.com/pingcap/kvproto v0.0.0-20190226063853-f6c0b7ffff11 h1:iGNfAHgK0VHJobW4bPTlFmdnt3YWsEHdSTIcjut6ffk= github.com/pingcap/kvproto v0.0.0-20190226063853-f6c0b7ffff11/go.mod h1:0gwbe1F2iBIjuQ9AH0DbQhL+Dpr5GofU8fgYyXk+ykk= -github.com/pingcap/parser v0.0.0-20190305073013-4f60445a0550 h1:zs2q6jzN04S63J0bvBBp+zK5cXHtiWscGDEjhTkmMbE= -github.com/pingcap/parser v0.0.0-20190305073013-4f60445a0550/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.20190116051332-34c808eef588+incompatible h1:e9Gi/LP9181HT3gBfSOeSBA+5JfemuE4aEAhqNgoE4k= 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") + } +}