gorange

package module
v2.15.3+incompatible Latest Latest
Warning

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

Go to latest
Published: Feb 2, 2018 License: MIT Imports: 18 Imported by: 3

README

gorange

gorange is a small Go library for interacting with range servers.

Usage

Documentation is available via GoDoc.

Description

gorange provides a Querier interface, and a few data structures that implement this interface and allows querying range services on remote hosts.

Querying range is a simple HTTP GET call, and Go already provides a steller http library. So why wrap it? Well, either you write your own wrapper or use one someone else has written, it's all the same to me. But I had to write the wrapper, so I figured I'd at least provide my implementation as a reference piece for others doing the same.

In any eveny, this library

  1. guarantees HTTP connections can be re-used by always reading all body bytes if the Get succeeded.
  2. detects and parses the RangeException header, so you don't have to.
  3. converts response body to slice of strings.

There are four possible error types this library returns:

  1. Raw error that the underlying Get method returned.
  2. ErrStatusNotOK is returned when the response status code is not OK.
  3. ErrRangeException is returned when the response headers includes 'RangeException' header.
  4. ErrParseException is returned by Client.Query when an error occurs while parsing the GET response.
Supported Use Cases

Both the Client and CachingClient data types implement the Querier interface. In fact, CachingClient is implemented as a simple Client with a TTL cache, and the NewCachingClient function merely wraps the provided Client. For a majority of use-cases, you don't need to worry about any of this. I recommend ignoring Client and CachingClient and just think about calling the NewQuerier function and getting back some opaque data structure instance that exposes the Query method. See the Simple code example below.

Simple

The easiest way to use gorange is to use a Configurator instance to create an object that implements the Querier interface, and use that to query range.

    package main
    
    import (
    	"bufio"
    	"fmt"
    	"os"
    	"time"
    
    	"github.com/karrick/gorange"
    )
    
    func main() {
    	// create a range querier; could list additional servers or include other options as well
    	querier, err := gorange.NewQuerier(&gorange.Configurator{
    		Servers:                 []string{"range.example.com"},
    	})
    	if err != nil {
    		fmt.Fprintf(os.Stderr, "%s\n", err)
    		os.Exit(1)
    	}
    
    	// main loop
    	fmt.Printf("> ")
    	scanner := bufio.NewScanner(os.Stdin)
    	for scanner.Scan() {
    		text := scanner.Text()
    		hosts, err := querier.Query(text)
    		if err != nil {
    			fmt.Fprintf(os.Stderr, "%s\n", err)
    			fmt.Printf("> ")
    			continue
    		}
    		fmt.Printf("%s\n> ", hosts)
    	}
    	if err := scanner.Err(); err != nil {
    		fmt.Fprintf(os.Stderr, "ERROR: %s\n", err)
    	}
    }
Customized

As described above, the NewQuerier function allows creating objects that implement Querier and supports most use-cases: optional round-robin query of multiple servers, optional retry of specific errors, and optional TTL memoization of query responses. The only requirement is to specify one or more servers to query. Leaving any other config option at its zero value creates a viable Querier without those optional features.

See the examples/customized/main.go for complete example of this code, including constants and functions not shown here.

    package main
    
    import (
        "github.com/karrick/gorange"
    )

    func main() {
    	servers := []string{"range1.example.com", "range2.example.com", "range3.example.com"}
    
    	config := &gorange.Configurator{
    		Addr2Getter:             addr2Getter,
    		RetryCount:              len(servers),
    		Servers:                 servers,
    		CheckVersionPeriodicity: 15 * time.Second,
    	}
    
    	// create a range querier
    	querier, err := gorange.NewQuerier(config)
    	if err != nil {
    		fmt.Fprintf(os.Stderr, "%s", err)
    		os.Exit(1)
    	}
    
    	// main loop
    	fmt.Printf("> ")
    	scanner := bufio.NewScanner(os.Stdin)
    	for scanner.Scan() {
    		text := scanner.Text()
    		hosts, err := querier.Query(text)
    		if err != nil {
    			fmt.Fprintf(os.Stderr, "%s\n", err)
    			fmt.Printf("> ")
    			continue
    		}
    		fmt.Printf("%s\n> ", hosts)
    	}
    	if err := scanner.Err(); err != nil {
    		fmt.Fprintf(os.Stderr, "ERROR: %s\n", err)
    	}
    }
Bit-Twiddling Tweaking Capabilities

While using the NewQuerier method is fine for most use-cases, there are times when you might need to create your own querying pipeline. If you have a Get method that matches the http.Client Get method's signature that you'd like to inject in the pipeline, you can build your own Querier by composing the functionality you need for your application.

How to do this is illustrated by the NewQuerier function in this library, but a simple example is shown below. Note that the example code simply wraps gogetter.Getter instances around other gogetter.Getter instances.

NOTE: A gogetter.Prefixer is needed to prepend the server name and URL route to each URL before the URL is sent to the underlying http.Client's Get method.

WARNING: Using http.Client instance without a Timeout will cause resource leaks and may render your program inoperative if the client connects to a buggy range server, or over a poor network connection.

    func main() {
        // create a range client
    	server := "range.example.com"
    	querier := &gorange.Client{
    		&gogetter.Prefixer{
    			Prefix: fmt.Sprintf("http://%s/range/list?", server),

    			Getter: &http.Client{Timeout: 5 * time.Second}, // don't forget the Timeout...
    		},
    	}
    
        // use the range querier
    	lines, err := querier.Query("%someQuery")
    	if err != nil {
    		fmt.Fprintf(os.Stderr, "%s", err)
    		os.Exit(1)
    	}
    	for _, line := range lines {
    		fmt.Println(line)
    	}
    }

Documentation

Index

Constants

View Source
const DefaultDialKeepAlive = 30 * time.Second

DefaultDialKeepAlive is used when no Addr2Getter function is provided to control the keep-alive duration for an active connection.

View Source
const DefaultDialTimeout = 5 * time.Second

DefaultDialTimeout is used when no Addr2Getter function is provided to control the timeout for establishing a new connection.

View Source
const DefaultMaxIdleConnsPerHost = 1

DefaultMaxIdleConnsPerHost is used when no Addr2Getter function is provided to control how many idle connections to keep alive per host.

View Source
const DefaultQueryTimeout = 30 * time.Second

DefaultQueryTimeout is used when no Addr2Getter function is provided to control the duration a query will remain in flight prior to automatic cancellation.

Variables

This section is empty.

Functions

func Proxy

func Proxy(config ProxyConfig) error

Proxy creates a proxy http server on the port that proxies range queries to the specified range servers.

Types

type CachingClient

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

CachingClient memoizes responses from a Querier.

func (*CachingClient) Close

func (c *CachingClient) Close() error

Close releases all memory and go-routines used by the Simple swarm. If during instantiation, checkVersionPeriodicty was greater than the zero-value for time.Duration, this method may block while completing any in progress updates due to `%version` changes.

func (*CachingClient) Expand

func (c *CachingClient) Expand(query string) (string, error)

Expand returns the response of the query, first checking in the TTL cache, then by actually invoking the Expand method on the underlying Querier.

func (*CachingClient) List

func (c *CachingClient) List(query string) ([]string, error)

List returns the response of the query, first checking in the TTL cache, then by actually invoking the List method on the underlying Querier.

func (*CachingClient) Query

func (c *CachingClient) Query(query string) ([]string, error)

Query returns the response of the query, first checking in the TTL cache, then by actually invoking the Query method on the underlying Querier.

func (*CachingClient) Raw

func (c *CachingClient) Raw(query string) (io.ReadCloser, error)

Raw sends the range request and checks for invalid responses from downstream. If the response is valid, this returns the response body as an io.ReadCloser for the client to use. It is the client's responsibility to invoke the Close method on the returned io.ReadCloser.

type Client

type Client struct {
	Getter gogetter.Getter
}

Client attempts to resolve range queries to a list of strings or an error.

func (*Client) Close

func (c *Client) Close() error

Close returns nil error.

func (*Client) Expand

func (c *Client) Expand(query string) (string, error)

Expand sends the specified query string to the Client's Getter, and converts a non-error result into a slice of bytes.

If the response includes a RangeException header, it returns ErrRangeException. If the status code is not okay, it returns ErrStatusNotOK. Finally, if it cannot parse the lines in the response body, it returns ErrParseException.

// use the range querier
result, err := querier.Expand("%someQuery")
if err != nil {
	fmt.Fprintf(os.Stderr, "%s", err)
	os.Exit(1)
}
fmt.Printf("%s\n", result)

func (*Client) List

func (c *Client) List(query string) ([]string, error)

List sends the specified query string to the Client's Getter, and converts a non-error result into a list of strings.

If the response includes a RangeException header, it returns ErrRangeException. If the status code is not okay, it returns ErrStatusNotOK. Finally, if it cannot parse the lines in the response body, it returns ErrParseException.

// use the range querier
list, err := querier.List("%someQuery")
if err != nil {
	fmt.Fprintf(os.Stderr, "%s", err)
	os.Exit(1)
}
for _, line := range list {
	fmt.Println(line)
}

func (*Client) Query

func (c *Client) Query(query string) ([]string, error)

Query sends the specified query string to the Client's Getter, and converts a non-error result into a list of strings.

If the response includes a RangeException header, it returns ErrRangeException. If the status code is not okay, it returns ErrStatusNotOK. Finally, if it cannot parse the lines in the response body, it returns ErrParseException.

// use the range querier
lines, err := querier.Query("%someQuery")
if err != nil {
	fmt.Fprintf(os.Stderr, "%s", err)
	os.Exit(1)
}
for _, line := range lines {
	fmt.Println(line)
}

func (*Client) Raw

func (c *Client) Raw(query string) (io.ReadCloser, error)

Raw sends the range request and checks for invalid responses from downstream. If the response is valid, this returns the response body as an io.ReadCloser for the client to use. It is the client's responsibility to invoke the Close method on the returned io.ReadCloser.

type Configurator

type Configurator struct {
	// Addr2Getter converts a range server address to a Getter, ideally a customized http.Client
	// object with a Timeout set. Leave nil to create default gogetter.Getter with
	// DefaultQueryTimeout.
	Addr2Getter func(string) gogetter.Getter

	// RetryCallback is predicate function that tests whether query should be retried for a
	// given error. Leave nil to retry all errors.
	RetryCallback func(error) bool

	// RetryCount is number of query retries to be issued if query returns error. Leave 0 to
	// never retry query errors.
	RetryCount int

	// RetryPause is the amount of time to wait before retrying the query with the underlying
	// Getter.
	RetryPause time.Duration

	// Servers is slice of range server address strings. Must contain at least one string.
	Servers []string

	// TTL is duration of time to cache query responses. Leave 0 to not cache responses.  When a
	// value is older than its TTL, it becomes stale.  When a key is queried for a value that is
	// stale, an asynchronous routine attempts to lookup the new value, while the existing value
	// is immediately returned to the user.  TTL, TTE, and CheckVersionPeriodicity work together
	// to prevent frequently needlessly asking servers for information that is still current
	// while preventing heap build-up on clients.
	TTL time.Duration

	// TTE is duration of time before cached response is no longer able to be served, even if
	// attempts to fetch new value repeatedly fail.  This value should be large if your
	// application needs to still operate even when range servers are down.  A zero-value for
	// this implies that values never expire and can continue to be served.  TTL, TTE, and
	// CheckVersionPeriodicity work together to prevent frequently needlessly asking servers for
	// information that is still current while preventing heap build-up on clients.
	TTE time.Duration

	// CheckVersionPeriodicity is the amount of time between checking the range `%version`
	// key. If your range server returns the epoch seconds of the time the data set became
	// active when given the `%version` query, using this option is much better than using just
	// TTL and TTE.  After the specified period of time the CachingClient will query the range
	// server's `%version` key, and if greater than the value discovered during the previous
	// check, schedules an asynchronous refresh of all keys last requested by the client less
	// than the amount of time specified by the TTL from the new version epoch. In other words,
	// say key A was last requested at time 300, and key B was last requested at time 360. If
	// the version comes back as 400, and the TTL is 60, then key A will be deleted and B will
	// be refreshed.  It makes no sense for CheckVersionPeriodicity to be a non-zero value when
	// TTL and TTE are both zero-values.
	CheckVersionPeriodicity time.Duration
}

Configurator provides a way to list the range server addresses, and a way to override defaults when creating new http.Client instances.

type ErrParseException

type ErrParseException struct {
	Err error
}

ErrParseException is returned by Client.Query method when an error occurs while parsing the Get response.

func (ErrParseException) Error

func (err ErrParseException) Error() string

type ErrRangeException

type ErrRangeException struct {
	Message string
}

ErrRangeException is returned when the response headers includes 'RangeException'.

func (ErrRangeException) Error

func (err ErrRangeException) Error() string

type ErrStatusNotOK

type ErrStatusNotOK struct {
	Status     string
	StatusCode int
}

ErrStatusNotOK is returned when the response status code is not Ok.

func (ErrStatusNotOK) Error

func (err ErrStatusNotOK) Error() string

type ProxyConfig

type ProxyConfig struct {
	// CheckVersionPeriodicity directs the range proxy to periodically send the '%version' query
	// the proxied range servers, and if the value is greater than the previous value, to
	// asynchronously update all the values for recently used range keys.
	CheckVersionPeriodicity time.Duration

	// Log directs the proxy to emit common log formatted log lines to the specified io.Writer.
	Log io.Writer

	// LogFormat specifies the log format to use when Log is not nil.  See `gohm` package for
	// LogFormat specifications.  If left blank, uses `gohm.DefaultLogFormat`.
	LogFormat string

	// Port specifies which network port the proxy should bind to.
	Port uint

	// Servers specifies which addresses ought to be consulted as the source of truth for range
	// queries.
	Servers []string

	// Timeout specifies how long to wait for the source of truth to respond. If the zero-value,
	// no timeout will be used. Not having a timeout value may cause resource exhaustion where
	// any of the proxied servers take too long to return a response.
	Timeout time.Duration

	// TTE is duration of time before cached response is no longer able to be served, even if
	// attempts to fetch new value repeatedly fail.  This value should be large if your application
	// needs to still operate even when range servers are down.  A zero-value for this implies that
	// values never expire and can continue to be served.  TTE and CheckVersionPeriodicity work
	// together to prevent frequently needlessly asking servers for information that is still
	// current while preventing heap build-up on clients.
	TTE time.Duration
}

ProxyConfig specifies the configuration for a gorange proxy HTTP server.

type Querier

type Querier interface {
	Close() error
	Expand(string) (string, error)
	List(string) ([]string, error)
	Query(string) ([]string, error)
	Raw(string) (io.ReadCloser, error)
}

Querier is the interface implemented by an object that allows key-value lookups, where keys are strings and values are slices of strings.

func NewQuerier

func NewQuerier(config *Configurator) (Querier, error)

NewQuerier returns a new instance that sends queries to one or more range servers. The provided Configurator not only provides a way of listing one or more range servers, but also allows specification of optional retry-on-failure feature and optional TTL cache that memoizes range query responses.

func main() {
	servers := []string{"range1.example.com", "range2.example.com", "range3.example.com"}

	config := &gorange.Configurator{
		RetryCount:              len(servers),
		RetryPause:              5 * time.Second,
		Servers:                 servers,
		CheckVersionPeriodicity: 15 * time.Second,
		TTL:                     30 * time.Second,
		TTE:                     15 * time.Minute,
	}

	// create a range querier; could list additional servers or include other options as well
	querier, err := gorange.NewQuerier(config)
	if err != nil {
		fmt.Fprintf(os.Stderr, "%s", err)
		os.Exit(1)
	}
	// must invoke Close method when finished using to prevent resource leakage
	defer func() { _ = querier.Close() }()
}

Directories

Path Synopsis
examples

Jump to

Keyboard shortcuts

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