[Plugin system] Use MESG service to develop MESG core

Proposal

We have a system that is oriented with services but we don’t really use services and event driven inside MESG. The idea is to embrace this and develop all or some features that we use using services that will have their own docker container and their own features. By doing so we will have a real experience on how to create services with MESG and we will see directly the frictions and optimizations that needs to be done.

Examples

Docker service

We could have a service for docker, with different tasks like

  • build image
  • start service
  • stop service
  • list services

    with events like:
  • onNewImage
  • onServiceStarted
  • onServiceStoped

MESG service

A service for all the services features


Like that the way we create MESG will be by creating MESG application and MESG services and after we just need to create more or less complex service, it doesn’t mean that we need to do really micro services it can be bigger services but the good point of that is that we would have a clear interface for all our services and the core will just when boot deploy all those services and use them, this would be really easy to soft update the core, you will just need to fetch and re-deploy the new version of the service and also people will be able to create their services based on ours if they want to.

3 Likes

This open to some new possibilities like even run mesg-core in a decentralized way because it’s all based on services and we will have decentralized services it means that it could even be possible to have those parts of mesg-core decentralized. Maybe a bit too much but maybe not because an iot device would just use the service to connect to its peers for example and other services will not be necessary. Anyway this leads to some nice possibilities I think

2 Likes

We need to think about a good system before start the development. This feature is really important for the future.

List of requirements:

  • Identify a plugin. Eg: resolver plugin != X plugin
  • How to manage (deploy, start, etc…) plugins
  • How to install them (bundle with core? separate download?)
  • Where/how to store plugin interface? Go interface in the core?

Plugin system

The goal is to find all the scenarios for the plugin system in order to define a good API to use it. This sequence diagram is just to make sure we all see the same way for the plugin system.

@core let me know already what you think about that, if we all agree on this workflow or you see something different.

Executing

sequenceDiagram Program->>+Config: GetPluginKey Config-->>-Program: key Program->>+PluginResolver: GetPlugin(key) alt plugin exists? PluginResolver->>Plugin: Create Plugin-->>-Program: plugin else Plugin-->>Program: null end alt plugin? Program->>+Plugin: pluginExecute Plugin->>Service: Start if needed Plugin->>Service: Listen for result if needed Plugin->>Service: Execute task Service-->>-Program: Result end

Reacting from an event of a plugin

We could have an event driven approach in the core and just listen for the different results or event that the plugin may send, for example plugin started, node added, block received…

1 Like

Can you clarify the names in diagram please? For example what is a Program, Config, PluginResolver, Plugin, Service?

What is the difference between a plugin and a service? How do you actually Create a plugin that showed in the diagram? How a program actually executes a task on a plugin?

I think, we need a long technical article about this plugin system that explains every small aspect of the plugin feature with the assumption of community doesn’t know anything about this new feature and only have some basic knowledge about how core works.

Right now, I’m struggling to understand the implementation details of plugin feature.

Actors

The different actors in the diagram with their associated roles

Program

The entity that need to execute a plugin, this part is the part that actually implement the business logic and that rely to the plugins to delegate some process

Config

This is the general configurations for the program. These configurations have default values but can be override.

PluginResolver (name to define)

The entity responsible for the creation of the plugin instance. This entity is just a kind of manager.

Plugin

This is the part that actually contains all the “API” and the delegates to the service. This is the interface that exposes all the informations that the plugin provides.

Service

This is a standard MESG Service than in this case is not used for an application but as a plugin of the core

Creation part

The creation of the plugin is a new allocation of an object that represent the API of the MESG Service that acts as a plugin. The plugin doesn’t do anything except delegate the call of the function to the MESG Service. Basically it will listen the events of the service and execute tasks to the service. The plugin might even use the grpc API that we have right now or directly the API package.

package test

import (
    "github.com/mesg-foundation/core/api"
    "github.com/satori/go.uuid"
)

// Test ...
type Test struct {
    serviceID string
    api       *api.API
}

// TaskXSuccess ...
type TaskXSuccess struct {
    Message string
}

// TaskXError ...
type TaskXError struct {
    Error string
}

// New ...
func New(api *api.API, serviceID string) *Test {
    return &Test{
        api:       api,
        serviceID: serviceID,
    }
}

// TaskX ...
func (t *Test) TaskX(foo string, bar string) (chan TaskXSuccess, chan TaskXError, error) {
    tag := uuid.NewV4().String()
    successOutput := make(chan TaskXSuccess)
    errorOutput := make(chan TaskXError)
    _, err := t.api.ExecuteTask(t.serviceID, "taskX", map[string]interface{}{
        "foo": foo,
        "bar": bar,
    }, []string{tag})
    if err != nil {
        return successOutput, errorOutput, err
    }
    result, err := t.api.ListenResult(t.serviceID, api.ListenResultTagFilters([]string{tag}))
    if err != nil {
        return successOutput, errorOutput, err
    }
    defer result.Close()
    go func(r *api.ResultListener) {
        e := <-result.Executions
        switch e.Output {
        case "success":
            successOutput <- TaskXSuccess{
                Message: e.OutputData["message"].(string),
            }
        case "error":
            errorOutput <- TaskXError{
                Error: e.OutputData["error"].(string),
            }
        }
    }(result)
    return successOutput, errorOutput, err
}

Config

  • Add a plugin section containing system services’ id

SystemServices

  • Reference all system service at any time
  • Start all System Services when start core
  • Keep System Services alive at all time in Docker
  • Call api package

SystemService instance

  • Specific interface/struct per System Service definition
  • Call api package to interact with the service in docker

In order to deploy a System Service

  • share a System Service folder on the local machine (~/.mesg/system-services/)
  • if no folder, download the default ones from hardcoded url or from an embedded tar in the bin

-> will put service definition in database
-> get service id
-> will put service id in config package

In order to start a System Service

  • service definition in database
  • service id

TODO

  • Need to update the schema
1 Like

Here is a first draft of the specification of the system service package:
https://github.com/mesg-foundation/core/tree/feature/system-services/systemservices

I didn’t create an interface because I thought it was over engineering and we will not need mock (i guess?). Are you ok with this?

Please take a look at the different files and comments in the package and tell me your feedback.

The goal is to write all necessary indications as comments so the implementation is easier and anyone could read its godoc to use it.

2 Likes

It’s nicely structured and easy to follow. We can also provide config pkg instance as an arg to systemservices.New() to prevent ambiguous access to configs and multiple initialization for config instance.

1 Like

We have the config.Global that is a singleton so we can have this in the New function directly in the body or we can pass it as parameters whatever solutions is fine

I wonder why we have chans returned here? I think this func should block untill there is something to return, it seems we don’t need chans here.

To not create new branch on github I’m pasting code here. The most important changes:

  1. There are no channels - we don’t need it here.
  2. The api of resolver (and any other system service) should be simplified. This is why we create this wrappers, otherwise anywan can execute such task and don’t need resolver service for that. As for the caller he wants simple API that brings knowleage/functionalites to him.
package systemservices

import (
	"fmt"

	"github.com/mesg-foundation/core/api"
	"github.com/mesg-foundation/core/systemservices/resolver"
)

type SystemService interface {
	Name() string
	SetServiceID(id string)
}

type SystemServices struct {
	api *api.API

	services map[string]SystemService
}

// New don't read config as it knows nothing about config - it should be passed
// it can accpet serviceIDS, or it coudl be done in AddService (my choice)
func New(api *api.API) *SystemServices {
	return &SystemServices{api: api}
}

func (s *SystemServices) AddService(ss SystemService) error {
	if _, ok := s.services[ss.Name()]; ok {
		return fmt.Errorf("system service %s already exists", ss.Name())
	}
	s.services[ss.Name()] = ss

	// deploy && start
	id := "1234"

	ss.SetServiceID(id)
	return nil
}


// Seccond approach (it will be too much for now, but to give overview)
type Strategy int

const (
	WaitStrategy Strategy= iota
	ParalleStrategy
	ExitOnFailStrategy
	RevertOnFailStrategy
)
func (s *SystemServices) AddServices(sss []SystemService, strategy Strategy) error {
	var (
	wg sync.WaitGroup
	mx sync.Mutex
	deployedServices []string
)

	if strategy == ParalleStrategy {
		wg.Add(len(sss))
	}

	deployFn := func (ss SystemService) {
	if _, ok := s.services[ss.Name()]; ok {
			return fmt.Errorf("system service %s already exists", ss.Name())
		}
		s.services[ss.Name()] = ss
	
		// deploy && start
		mx.Lock()
		id := "1234"
		s.deployedServices = append(s.deployedServices, id)
		mx.Unlock()
	
		ss.SetServiceID(id)
	}

	for _, ss := range sss {
		switch strategy {
		case WaitStrategy:
			deployFn()
		case ParallelStrategy:
			go func () {
				deployFn()
				wg.Done()
			}()
		case ExitOnFailStrategy:
			deployFn()
			/* if err != nil {return err} */
		case RevertOnFailStrategy:
			for _, id := range s.deployedServices {
				/* remove from service list */
			}
				s.services = nil
		}
	}

	wg.Wait()
	return nil
}

// Here we can have 4 diffrent appraoches to get service:

// by name
func (s *SystemServices) GetSystemServiceByName(name string) SystemService {
	return s.services[name]
}

// by service fg:
// systemService.GetSystemServiceByService(&resolver.Resolver{})
func (s *SystemServices) GetSystemServiceByService(ss SystemService) SystemService {
	return s.services[ss.Name()]
}

// assuem we know the name and it won't change
func (s *SystemServices) Resolver() *resolver.Resolver {
	r, _ := s.services["resolver"].(*resolver.Resolver)
	return r
}

// by for loop
func (s *SystemServices) Resolver2() *resolver.Resolver {
	for _, service := range s.services {
		if r, ok := service.(*resolver.Resolver); ok {
			return r
		}
	}
	return nil
}


// example of system service package
package resolver

import (
	"errors"
	"fmt"

	"github.com/mesg-foundation/core/api"
	"github.com/mesg-foundation/core/execution"
	uuid "github.com/satori/go.uuid"
)

// Tasks
const (
	addPeersTask string = "AddPeers"
	resolveTask         = "Resolve"
)

type Resolver struct {
	serviceID string
	api       *api.API
}

func New(api *api.API) *Resolver {
	return &Resolver{api: api}
}

func (r *Resolver) Name() string {
	return "resolver"
}

func (r *Resolver) SetServiceID(serviceID string) {
	r.serviceID = serviceID
}

// wrapper will go to api as method
func ExecuteAndListen(a *api.API, serviceID, task string, inputs map[string]interface{}) (*execution.Execution, error) {
	tag := uuid.NewV4().String()
	_, err := a.ExecuteTask(serviceID, task, inputs, []string{tag})
	if err != nil {
		return nil, err
	}
	result, err := a.ListenResult(serviceID, api.ListenResultTagFilters([]string{tag}))
	if err != nil {
		return nil, err
	}
	defer result.Close()
	return <-result.Executions, nil
}

func (r *Resolver) AddPeers(address string) error {
	e, err := ExecuteAndListen(r.api, r.serviceID, addPeersTask, map[string]interface{}{
		"address": address,
	})
	if err != nil {
		return err
	}

	switch e.Output {
	case "success":
		return nil
	case "error":
		return errors.New(e.OutputData["error"].(string))
	default:
		return errors.New("unknown output")
	}
}

type ResolveFoundOutput struct {
	Address string
}

func (r *Resolver) Resolve(serviceID string) (address string, err error) {
	e, err := ExecuteAndListen(r.api, r.serviceID, resolveTask, map[string]interface{}{
		"serviceID": serviceID,
	})

	switch e.Output {
	case "found":
		return e.OutputData["found"].(*ResolveFoundOutput).Address, nil
	case "notFound":
		return "", fmt.Errorf("address for service id %q not found", serviceID)
	case "error":
		return "", errors.New(e.OutputData["error"].(string))
	default:
		return "", errors.New("unknown output")
	}
}

1 Like

@krhubert Your generic solution with SystemService seems nice.

If we really want SystemServices to be not aware of system service’s types, we cannot have something like this: func (s *SystemServices) Resolver() *resolver.Resolver , instead it should return a SystemService and that can be type asserted by the caller.

I also would like using constants while registering and getting services to strengthen type safety like this:

// in pkg config
const (
  ResolverService ServiceType = iota + 1
)

config.SystemServices = map[ServiceType]string{
  ResolverService: "4f7891f77a6333787075e95b6d3d73ad50b5d1e9",
}

// in pkg systemservices
func (s *SystemServices) AddService(ResolverService, ss SystemService)
func (s *SystemServices) GetService(ResolverService) SystemService

But I’m not sure if we need this much generic use in the systemservices pkg because we already know what system services core requires to run. It is not determineted in the runtime, so having a systemservices pkg like the one proposed by @Nicolas should be well enough and it’s kinda easier to use because we don’t need to make type assertions for services.

To conclude this discussion we must define the idea behing having a systemservices pkg and it’s roles in the first place.

from @krhubert post,

  • I agree with SystemService interface
  • I agree with SystemServices interface
  • func (s *SystemServices) AddService(ss SystemService) error. I actually like the fact that the servicesystems package is not aware of the configuration and directly receive a SystemService (like you did). In this case, we could put in the core/main.go the logic to read the config, deploy the services from folder if needed, and init the system services.
  • For the strategy approach, I will go with the parallel approach. But I not sure if it better to put this strategy in servicesystems or directly in core/main.go
  • For the getter I will go with func (s *SystemServices) Resolver() *resolver.Resolver. But I will add an error as well: func (s *SystemServices) Resolver() (*resolver.Resolver, error)
  • For func ExecuteAndListen(a *api.API, serviceID, task string, inputs map[string]interface{}) (*execution.Execution, error) it’s good. Maybe it could be put directly in the API package? So we could even offer an gRPC API like this in the future.
  • For func (r *Resolver) Resolve(serviceID string) (address string, err error) I also prefer to accept and return simple type rather than struct for simple case. We could still return struct in the case of multiple success output or more complex case.
  • One Suggestion: Create types for errors. If we want to react to error, we need to return dedicated type. For instance, the notFound output of the resolve task, should return a specific error type so the core can react to it.
1 Like

:+1: it’s my first choice

Yep, we can put deploy strategy in seperate package. As you saw in comment this is just proposal to show a nice design patter and ofc we will start with parallel (without passing any strategy as it’s too much for now)

Why you need error here? For not found? Nil is perfect here to represent not found

:+1:

:+1:

I like that this SystemServices doesn’t know about the SystemService but this have for me some serious drawbacks.

I agree that if the calling method add services to this manager then this manager should not be aware of the different types but why the calling method should be the one creating this ?

We need to have access to the types for these SystemServices and not having to cast all the time. We can always have a kind of facade that deal with this casting but I think for now that could be directly implemented into the SystemServices.

I definitely prefer a typed error than have to guess that a nil value is representing a not found error.

Except than that I’m ok to go with the rest

I don’t understand that part can you explain?

If we allow making type assertions inside SystemServices we lose the flexibility of having SystemService interface in the first place and having it will not make sense other than cosmetics which doesn’t provide an actual benefit.

My point was why not having all the system services responsibility inside this SystemServices like we agreed on the meeting monday ?

The SystemServices will have knowledge of all the services and like that can also manage the casting and everytime we use it we just use the different system services without having to care about how to cast them.

Again following back to this implementation

I just don’t want to make things over complicated where we might not need this complexity. I don’t see why we need such abstraction now because we will always have to know what kind of system service we have to implement and use

This is what I also agree on. In addition to this, as suggested before, lets remove the chans from the return types and instead, directly return output datas (as structs) and an error for executing tasks. Or as @krhubert suggested, we can also create a high level wrapper around the output datas of task executions and only return expected datas and a custom error type. This is something that we should experiment and later give a decision about it because we currently don’t know how the implementation will look like for various plugins.