Abordagem idiomática para um sistema baseado em plugin Go

9

Eu tenho um projeto Go que gostaria de abrir o código-fonte, mas há certos elementos que não são adequados para o OSS, por exemplo, lógica específica da empresa, etc.

Eu concebi a seguinte abordagem:

  • interface s estão definidos no repositório principal.
  • Os plug-ins podem então ser repositórios autônomos, cujo type s implementa o interface s definido no núcleo. Isso permite que os plug-ins sejam armazenados em módulos completamente separados e, portanto, tenham seus próprios trabalhos de CI, etc.
  • Os plug-ins são compilados no binário final por meio de links simbólicos.

Isso resultaria em uma estrutura de diretórios algo como o seguinte:

|- $GOPATH
  |- src
    |- github.com
      |- jabclab
        |- core-system
          |- plugins <-----|
      |- xxx               | 
        |- plugin-a ------>| ln -s
      |- yyy               |  
        |- plugin-b ------>|

Com um exemplo de fluxo de trabalho de:

$ go get git@github.com:jabclab/core-system.git
$ go get git@github.com:xxx/plugin-a.git
$ go get git@github.com:yyy/plugin-b.git
$ cd $GOPATH/src/github.com
$ ln -s ./xxx/plugin-a/*.go ./jabclab/core-system/plugins
$ ln -s ./yyy/plugin-b/*.go ./jabclab/core-system/plugins
$ cd jabclab/core-system
$ go build

O único problema sobre o qual não tenho certeza é como tornar os tipos definidos em plug-ins disponíveis no tempo de execução no core. Prefiro não usar reflect , mas não consigo pensar em uma maneira melhor no momento. Se eu estivesse fazendo o código em um repo, usaria algo como:

package plugins

type Plugin interface {
  Exec(chan<- string) error
}

var Registry map[string]Plugin

// plugin_a.go
func init() { Registry["plugin_a"] = PluginA{} }

// plugin_b.go
func init() { Registry["plugin_b"] = PluginB{} }

Além da pergunta acima, essa abordagem geral seria considerada idiomática?

    
por jabclab 29.02.2016 в 21:05
fonte

1 resposta

2

Este é um dos meus problemas favoritos no Go. Eu tenho um projeto de código aberto que tem que lidar com isso também ( link ), ele tem DB e Runtime conectáveis (Docker, k8s , Mesos, etc) clientes. Antes do pacote de plugins que está no branch master do Go (então ele deve estar chegando a uma versão estável em breve) eu apenas compilei todos os plugins no binário e a configuração permitida decidiu qual usar.

A partir do pacote de plugins, o link , você pode usar links dinâmicos para plug-ins, de modo semelhante a C's dlopen() em seu carregamento, e o comportamento do pacote de plug-ins do go está bem descrito na documentação.

Além disso, recomendo dar uma olhada em como a Hashicorp resolve isso fazendo RPC em um soquete unix local. link

O benefício adicional de executar um plugin como um processo separado, como o modelo da Hashicorp, é que você obtém grande estabilidade no caso de o plug-in falhar, mas o processo principal é capaz de lidar com essa falha.

Eu também devo mencionar que o Docker faz seus plugins no Go da mesma forma, exceto que o Docker usa HTTP em vez de RPC. Além disso, um engenheiro do Docker postou a inclusão de um intérprete de JavaScript para lógica dinâmica no link anterior.

O problema que eu queria ressaltar com o padrão do pacote sql mencionado nos comentários é que, na verdade, isso não é uma arquitetura de plug-in, você ainda está limitado a tudo o que está em suas importações, portanto você pode ter vários main.go mas isso não é um plugin, o ponto de um plugin é tal que o mesmo programa pode executar um pedaço de código ou outro. O que você tem com coisas como o pacote sql é a flexibilidade, onde um pacote separado determina qual driver de banco de dados usar. No entanto, você acaba modificando o código para alterar o driver que está usando.

Eu quero adicionar, com todos esses padrões de plugins, além de compilar no mesmo binário e usar a configuração para escolher, cada pode ter sua própria compilação, teste e implementação (isto é, sua própria CI / CD), mas não necessariamente.

    
por Christian Grabowski 03.11.2016 / 22:43
fonte