etcetera

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Dec 23, 2014 License: MIT Imports: 6 Imported by: 0

README

etcetera

Build Status Coverage Status GoDoc

This is an etcd client that uses a tagged struct to save and load values from the etcd cluster. etcetera is only an abstraction layer over the go-etcd library. It was designed to be simple to use and make transitions from JSON to key-value configuration easier.

The idea was originally from my co-worker Gustavo Henrique Montesião de Sousa (@gustavo-hms).

How to use it

Download the library using the command bellow.

go get -u github.com/rafaeljusto/etcetera

This project has the following dependencies:

  • github.com/coreos/go-etcd/etcd

So you should also download the dependencies.

go get -u github.com/coreos/go-etcd/etcd

Now lets see an example to understand how it works. Imagine that your system today use a structure for configuration everything and it is persisted in a JSON file.

type B struct {
  SubField1 string `json:"subfield1"`
}

type A struct {
  Field1 string            `json:"field1"`
  Field2 int               `json:"field2"`
  Field3 int64             `json:"field3"`
  Field4 bool              `json:"field4"`
  Field5 B                 `json:"field5"`
  Field6 map[string]string `json:"field6"`
  Field7 []string          `json:"field7"`
}

Now you want to start using etcd for configuration management. But the problem is that etcd works with URI and key/value, and you will need to change the way your configuration was developed to fit this style. Here is where this library will help you! It will map each field of the structure into an URI of etcd using tags as it is for JSON. Lets look our example:

type B struct {
  SubField1 string `etcd:"subfield1"`
}

type A struct {
  Field1 string            `etcd:"field1"`
  Field2 int               `etcd:"field2"`
  Field3 int64             `etcd:"field3"`
  Field4 bool              `etcd:"field4"`
  Field5 B                 `etcd:"field5"`
  Field6 map[string]string `etcd:"field6"`
  Field7 []string          `etcd:"field7"`
}

And that's it! You can still work with your structure and now have the flexibility of a centralized configuration system. The best part is that you can also monitor some field for changes, calling a callback when something happens.

What happens is that the library will build the URI of etcd based on the tags, so if we want to look for the "A.FieldB.SubField1" field in etcd we would have to look at the URI "/field5/subfield1". Now, using just this strategy would limit to have only one configuration structure in the etcd cluster, for that reason you can define a namespace in the constructor. For example, when checking the same field "A.FieldB.SubField1" with the namespace "test" the URI to look for would be "/test/field5/subfield1".

For now you can add a tag in the following types:

  • struct
  • map[string]string
  • map[string]struct
  • []string
  • []struct
  • []int
  • []int64
  • []bool
  • string
  • int
  • int64
  • bool

When saving or loading a structure, attributes without the tag 'etcd' or other types from the listed above are going to be ignored.

Performance

To make the magic we use reflection, and this can degrade performance. But the purpouse is to use this library to centralize the configurations of your project into a etcd cluster, and for this the performance isn't the most important issue. Here are some benchmarks (without etcd I/O and latency delays):

BenchmarkSave      2000000         760 ns/op
BenchmarkSaveField 2000000         654 ns/op
BenchmarkLoad      2000000         664 ns/op
BenchmarkWatch      300000        4977 ns/op
BenchmarkVersion  20000000         114 ns/op

Fill free to send pull requests to improve the performance or make the code cleaner (I will thank you a lot!). Just remember to run the tests after every code change.

Examples

type B struct {
  SubField1 string `etcd:"subfield1"`
}

type A struct {
  Field1 string            `etcd:"field1"`
  Field2 int               `etcd:"field2"`
  Field3 int64             `etcd:"field3"`
  Field4 bool              `etcd:"field4"`
  Field5 B                 `etcd:"field5"`
  Field6 map[string]string `etcd:"field6"`
  Field7 []string          `etcd:"field7"`
}

func ExampleSave() {
  a := A{
    Field1: "value1",
    Field2: 10,
    Field3: 999,
    Field4: true,
    Field5: B{"value2"},
    Field6: map[string]string{"key1": "value3"},
    Field7: []string{"value4", "value5", "value6"},
  }

  client, err := NewClient([]string{"http://127.0.0.1:4001"}, "test", &a)
  if err != nil {
    fmt.Println(err.Error())
    return
  }

  if err := client.Save(); err != nil {
    fmt.Println(err.Error())
    return
  }

  fmt.Printf("%+v\n", a)
}

func ExampleSaveField() {
  a := A{
    Field1: "value1 changed",
  }

  client, err := NewClient([]string{"http://127.0.0.1:4001"}, "test", &a)
  if err != nil {
    fmt.Println(err.Error())
    return
  }

  if err := client.SaveField(&a.Field1); err != nil {
    fmt.Println(err.Error())
    return
  }

  fmt.Printf("%+v\n", a)
}

func ExampleLoad() {
  var a A

  client, err := NewClient([]string{"http://127.0.0.1:4001"}, "test", &a)
  if err != nil {
    fmt.Println(err.Error())
    return
  }

  if err := client.Load(); err != nil {
    fmt.Println(err.Error())
    return
  }

  fmt.Printf("%+v\n", a)
}

func ExampleWatch() {
  var a A

  client, err := NewClient([]string{"http://127.0.0.1:4001"}, "test", &a)
  if err != nil {
    fmt.Println(err.Error())
    return
  }

  stop, err := client.Watch(&a.Field1, func() {
    fmt.Printf("%+v\n", a)
  })

  if err != nil {
    fmt.Println(err.Error())
    return
  }

  close(stop)
}

func ExampleVersion() {
  var a A

  client, err := NewClient([]string{"http://127.0.0.1:4001"}, "test", &a)
  if err != nil {
    fmt.Println(err.Error())
    return
  }

  if err := client.Load(); err != nil {
    fmt.Println(err.Error())
    return
  }

  version, err := client.Version(&a.Field1)
  if err != nil {
    fmt.Println(err.Error())
    return
  }

  fmt.Printf("%d\n", version)
}

Documentation

Overview

Package etcetera is etcd client that uses a tagged struct to save and load values

Behavior

We took some decisions when creating this library taking into account that less is more. The decisions are all listed bellow.

Always retrieving the last index: For the use case that we thought, there's no reason (for now) to retrieve an intermediate state of a field. We are always looking for the current value in etcd. But we store the index of all attributes retrieved from etcd so that the user wants to know it (in the library we use "version" instead of "index" because it appears to have a better context).

Setting unlimited TTL: The type of data that we store in etcd (configuration values) don't need a TTL. Or at least we did not imagine any case when it does need a TTL.

Ignoring errors occurred in watch: When something goes wrong while retrieving or parsing the data from etcd, we prefer to silent drop the update instead of setting a strange value to the configuration field. Another problem is to create a good API to notify about errors occurred in watch, the first idea is to use a channel for errors, but it doesn't appears to be a elegant approach, and more than that, what the user can do with this error? Well, we are still thinking about it.

Ignoring "directory already exist" errors: If the directory already exists, great! We go on and create the structure under this directory. There's no reason to stop everything because of this error.

Not allowing URI in structure's field tag: This was a change made on 2014-12-17. I thought that leaving the decision to the user to create an URI in a structure's field tag could cause strange behaviors when structuring the configuration in etcd. So all the slashes in the field's tag will be replaced by hyphens, except if the slash is the first or last character

Improve

There are some issues that we still need to improve in the project. First, the code readability is terrible with all the reflection used, and with a good re-factory the repeated code could be reused. The full test coverage will ensure that the re-factory does not break anything.

Second, when watching a field, you will receive a channel to notify when you want to stop watching. Now if you send a boolean false into the channel instead of closing it, we could have a strange behavior since there are two go routines listening on this channel (go-etcd and etcetera watch functions).

And finally, we could have concurrency issues while updating configuration fields caused by the watch service. We still need to test the possible cases, but adding a read/write lock don't appears to be an elegant solution.

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrInvalidConfig alert whenever you try to use something that is not a structure in the Save
	// function, or something that is not a pointer to a structure in the Load funciton
	ErrInvalidConfig = errors.New("etcetera: configuration must be a structure or a pointer to a structure")

	// ErrNotInitialized alert when you pass a structure to the Load function that has a map attribute
	// that is not initialized
	ErrNotInitialized = errors.New("etcetera: configuration has fields that are not initialized (map)")

	// ErrFieldNotMapped alert whenever you try to access a field that wasn't loaded in the client
	// structure. If we don't load the field before we cannot determinate the path or version
	ErrFieldNotMapped = errors.New("etcetera: trying to retrieve information of a field that wasn't previously loaded")

	// ErrFieldNotAddr is throw when a field that cannot be addressable is used in a place that we
	// need the pointer to identify the path related to the field
	ErrFieldNotAddr = errors.New("etcetera: field must be a pointer or an addressable value")
)

Functions

This section is empty.

Types

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client stores the etcd connection, the configuration instance that we are managing and some extra informations that are useful for controlling path versions and making the API simpler

func NewClient

func NewClient(machines []string, namespace string, config interface{}) (*Client, error)

NewClient internally build a etcd client object (go-etcd library). The machines attribute defines the etcd cluster that this client will be connect to. Now the namespace defines a special root directory to build the configuration URIs, and is recommended when you want to use more than one configuration structure in the same etcd. And finally the config attribute is the configuration struct that you want to send or retrieve of etcd

func NewTLSClient

func NewTLSClient(machines []string, cert, key, caCert, namespace string, config interface{}) (*Client, error)

NewTLSClient internally build a etcd client object with TLS (go-etcd library). The machines attribute defines the etcd cluster that this client will be connect to. The cert, key, caCert attributes are the same from the go-etcd library to ensure the TLS connection. Now the namespace defines a special root directory to build the configuration URIs, and is recommended when you want to use more than one configuration structure in the same etcd. And finally the config attribute is the configuration struct that you want to send or retrieve of etcd

func (*Client) Load

func (c *Client) Load() error

Load retrieves the data from the etcd into the given structure. Only attributes with the tag 'etcd' will be filled. Supported types are 'struct', 'slice', 'map', 'string', 'int', 'int64' and 'bool'

func (*Client) Save

func (c *Client) Save() error

Save stores a structure in etcd. Only attributes with the tag 'etcd' are going to be saved. Supported types are 'struct', 'slice', 'map', 'string', 'int', 'int64' and 'bool'

func (*Client) SaveField

func (c *Client) SaveField(field interface{}) error

SaveField saves a specific field from the configuration structure. Works in the same way of Save, but it can be used to save specific parts of the configuration, avoiding excessive requests to etcd cluster

func (*Client) Version

func (c *Client) Version(field interface{}) (uint64, error)

Version returns the current version of a field retrieved from etcd. It does not query etcd for the latest version. When the field was not retrieved from etcd yet, the version 0 is returned

func (*Client) Watch

func (c *Client) Watch(field interface{}, callback func()) (chan<- bool, error)

Watch keeps track of a specific field in etcd using a long polling strategy. When a change is detected the callback function will run. When you want to stop watching the field, just close the returning channel

BUG(rafaeljusto): If the user sends a boolean false instead of closing the returning channel, we could have a strange behavior since there are two go routines listening on it (go-etcd and etcetera watch functions)

Notes

Bugs

  • If the user sends a boolean false instead of closing the returning channel, we could have a strange behavior since there are two go routines listening on it (go-etcd and etcetera watch functions)

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL