Documentation ¶
Overview ¶
Package service implements service-like goroutine lifecycle management.
It is intended for use when you need to co-ordinate the state of one or more long-running goroutines and control startup and shutdown.
Package service is complemented by the 'github.com/shabbyrobe/go-service/services' package, which provides a global version of a service.Runner for use in simpler applications.
Quick Example
type MyRunnable struct {} func (m *MyRunnable) Run(ctx service.Context) error { // Set up your stuff: tick := time.NewTicker() defer tick.Stop() // Notify the Runner that we are 'ready', which will unblock the call // Runner.Start(). // // If you omit this, Start() will never unblock; failing to call Ready() // in a Runnable is an error. if err := ctx.Ready(); err != nil { return err } // Run the service, awaiting an instruction from the runner to Halt: select { case <-ctx.Done(): case t := <-tick.C: fmt.Println(t) } return nil } func run() error { runner := service.NewRunner() // Ensure that every service is shut down within 10 seconds, or panic // if the deadline is exceeded: defer service.MustShutdownTimeout(10*time.Second, runner) rn := &MyRunnable{} // If you want to be notified if the service ends prematurely, attach // an EndListener. failer := service.NewFailureListener(1) svc := service.New("my-service", rn).WithEndListener(failer) // Start a service in the background. The call to Start will unblock when // MyRunnable.Run() calls ctx.Ready(): if err := runner.Start(context.TODO(), svc); err != nil { return err } after := time.After(10*time.Second) select { case <-after: // Halt a service and wait for it to signal it finished: if err := runner.Halt(context.TODO(), svc); err != nil { return err } case err := <-failer.Failures(): // If something goes wrong and MyRunnable ends prematurely, // the error returned by MyRunnable.Run() will be sent to the // FailureListener.Failures() channel. return err } return nil }
Performance ¶
Services are by nature heavier than a regular goroutine; they're up to 10x slower and use more memory. You should probably only use Services when you need to fully control the management of a long-lived goroutine, otherwise they're likely not worth it:
BenchmarkRunnerStart1-4 500000 2951 ns/op 352 B/op 6 allocs/op BenchmarkGoroutineStart1-4 5000000 368 ns/op 0 B/op 0 allocs/op BenchmarkRunnerStart10-4 100000 20429 ns/op 3521 B/op 60 allocs/op BenchmarkGoroutineStart10-4 500000 2933 ns/op 0 B/op 0 allocs/op
There are plenty of opportunities for memory savings in the library, but the chief priority has been to get a working, stable and complete API first. I don't plan to start 50,000 services a second in any app I am currently working on, but this is not to say that optimising the library isn't important, it's just not a priority yet. YMMV.
Runnables ¶
Runnables can be created by implementing the Runnable interface. This interface only contains one method (Run), but there are some very important caveats in order to correctly implement it:
type MyRunnable struct {} func (m *MyRunnable) Run(ctx service.Context) error { // This MUST be present in every implementation of service.Runnable: if err := ctx.Ready(); err != nil { return err } // You must wait for the signal to Halt. You can also poll // ctx.ShouldHalt(). <-ctx.Done() return nil }
The Run() method will be run in the background by a Runner. The Run() method MUST do the following to be considered valid. Violating any of these rules will result in Undefined Behaviour (uh-oh!):
ctx.Ready() MUST be called and error checked properly
<-ctx.Done() MUST be included in any select {} block
OR... ctx.ShouldHalt() MUST be checked frequently enough that your calls to Halt() won't time out if <-ctx.Done() is not used.
If Run() ends before it is halted by a Runner, an error MUST be returned. If there is no obvious application specific error to return in this case, service.ErrServiceEnded MUST be returned.
The Run() method SHOULD do the following:
- service.Sleep(ctx) should be used instead of time.Sleep(); service.Sleep() is haltable.
Here is an example of a Run() method which uses a select{} loop:
func (m *MyRunnable) Run(ctx service.Context) error { if err := ctx.Ready(); err != nil { return err } for { select { case stuff := <-m.channelOfStuff: m.doThingsWithTheStuff(stuff) case <-ctx.Done(): return nil } } }
Here is an example of a Run() method which sleeps:
func (m *MyService) Run(ctx service.Context) error { if err := ctx.Ready(); err != nil { return err } for !ctx.ShouldHalt() { m.doThingsWithTheStuff(stuff) service.Sleep(ctx, 1 * time.Second) } return nil }
service.RunnableFunc allows you to use a bare function as a Runnable instead of implementing the Service interface:
service.RunnableFunc("My service", func(ctx service.Context) error { // valid Run implementation })
Runners ¶
To start or halt a Runnable, a Runner is required and the Runnable must be wrapped in a service.Service:
runner := service.NewRunner(nil) rn1, rn2 := &MyRunnable{}, &MyRunnable{} svc1, svc2 := service.New("s1", rn1), service.New("s2", rn2) // start svc1 and wait until it is ready: err := runner.Start(context.TODO(), svc1) // start svc1 and svc2 simultaneously and wait until both of them are ready: err := runner.Start(context.TODO(), svc1, svc2) // start both services, but wait no more than 1 second for them both to be ready: err := service.StartTimeout(1 * time.Second, runner, svc1, svc2) if err != nil { // You MUST attempt to halt the services if StartTimeout does not succeed: service.MustHaltTimeout(1 * time.Second, runner, svc1, svc2) } // the above StartTimeout call is equivalent to the following (error handling // skipped for brevity): ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) defer cancel() err := runner.Start(ctx, svc1, svc2) // now halt the services we just started, unblocking when both services have // ended or failed to end: if err := runner.Halt(context.TODO(), svc1, svc2); err != nil { // If Halt() fails, we have probably leaked a resource. If you have no // mechanism to recover it, it could be time to crash: panic(err) } // halt every service currently started in the runner: err := runner.Shutdown(context.TODO()) // halt every service in the runner, waiting no more than 1 second for them // to finish halting: err := service.ShutdownTimeout(1*time.Second, runner)
Contexts ¶
Service.Run receives a service.Context as its first parameter. service.Context implements context.Context (https://golang.org/pkg/context/).
You should use the ctx as the basis for any child contexts you wish to create in your service, as you will then gain access to cancellation propagation from Runner.Halt():
func (s *MyService) Run(ctx service.Context) error { if err := ctx.Ready(); err != nil { return err } dctx, cancel := context.WithDeadline(ctx, time.Now().Add(2 * time.Second)) defer cancel() // This service will be "Done" either when the service is halted, // or the deadline arrives (though in the latter case, the service // will be considered to have ended prematurely) <-dctx.Done() // If the service wasn't halted (i.e. if the deadline elapsed), we must // return an error to satisfy the service.Run contract outlined in the // docs: return dctx.Err() }
The rules around when the ctx passed to Run() is considered Done() are different depending on whether ctx.Ready() has been called. If ctx.Ready() has not yet been called, the ctx passed to Run() is Done() if:
- The service is halted using Runner.Halt()
- The context passed to Runner.Start() is either cancelled or its deadline is exceeded.
If ctx.Ready() has been called, the ctx passed to Run() is Done() if:
- The service is halted using Runner.Halt()
- That's it.
The context passed to Runner.Halt() is not bound to the ctx passed to Run(). The rationale for this decision is that if you need to do things in your service that require a context.Context after Run's ctx is Done() (i.e. when your Runnable is Halting), you are responsible for creating your own context:
func (s *MyService) Run(ctx service.Context) error { if err := ctx.Ready(); err != nil { return err } <-ctx.Done() return nil }
The guideline for this may change, but at the moment the best recommendation is to use context.Background() in this case: if you are calling Runner.Halt() and your context deadline runs out, you have lost resources no matter what.
Listeners ¶
Runner takes a list of functional RunnerOptions that can be used to listen to events.
The RunnerOnEnd option allows you to supply a callback which is executed when a service ends:
endFn := func(stage Stage, service *Service, err error) { // behaviour you want when a service ends before it is halted } r := service.NewRunner(service.RunnerOnEnd(endFn))
OnEnd will always be called if a Runnable's Run() function returns, whether that is because the service failed prematurely or because it was halted. If the service has ended because it was halted, err will be nil. If the service has ended for any other reason, err MUST contain an error.
The RunnerOnError option allows you to supply a callback which is executed when a service calls service.Context.OnError(err). Context.OnError(err) is used when an error occurs that cannot be handled and does not terminate the service. It's most useful for logging:
errorFn := func(stage Stage, service *Service, err error) { log.Println(service.Name, err) } r := service.NewRunner(service.RunnerOnError(endFn))
You MUST NOT attempt to call your Runner from inside your OnEnd or OnError function. This will cause a deadlock. If you wish to call the runner, wrap your function body in an anonymous goroutine:
endFn := func(stage Stage, service *Service, err error) { // Safe: go func() { err := runner.Start(...) }() // NOT SAFE: err := runner.Start(...) }
Restarting ¶
All Runnable implementations are restartable by default. If written carefully, it's also possible to start the same Runnable in multiple Runners. Maybe that's not a good idea, but who am I to judge? You might have a great reason.
Some Runnable implementations may wish to explicitly block restart, such as tings that wrap a net.Conn (which will not be available if the service fails). An atomic can be a good tool for this job:
type MyService struct { used int32 } func (m *MyService) Run(ctx service.Context) error { if !atomic.CompareAndSwapInt32(&m.used, 0, 1) { return errors.New("cannot reuse MyService") } if err := ctx.Ready(); err != nil { return err } <-ctx.Done() return nil }
Example ¶
msg := make(chan string) // Runnables must implement service.Runnable: runnable := RunnableFunc(func(ctx Context) error { defer fmt.Println("halted") // Set up your stuff: after := time.After(10 * time.Millisecond) // Notify the Runner that we are 'ready', which will unblock the call // Runner.Start(). // // If you omit this, Start() will never unblock; failing to call Ready() // in a Runnable is an error. if err := ctx.Ready(); err != nil { return err } // Run the service, awaiting an instruction from the runner to Halt: for { select { case <-ctx.Done(): return nil case <-after: msg <- "stop me!" } } return nil }) runner := NewRunner() // Ensure that every service is shut down within 10 seconds, or panic // if the deadline is exceeded: defer MustShutdownTimeout(10*time.Second, runner) // If you want to be notified if the service ends prematurely, attach // an EndListener. failer := NewFailureListener(1) svc := New("my-service", runnable).WithEndListener(failer) // Start a service in the background. The call to Start will unblock when // the Runnable calls ctx.Ready(): if err := runner.Start(nil, svc); err != nil { panic(err) } select { case s := <-msg: fmt.Println(s) // Halt a service and wait for it to signal it finished: if err := runner.Halt(context.TODO(), svc); err != nil { panic(err) } case err := <-failer.Failures(): // If something goes wrong and MyRunnable ends prematurely, // the error returned by MyRunnable.Run() will be sent to the // FailureListener.Failures() channel. panic(err) }
Output: stop me! halted
Index ¶
- Constants
- Variables
- func Errors(err error) []error
- func HaltTimeout(timeout time.Duration, runner Runner, services ...*Service) error
- func IsAlreadyRunning(err error) bool
- func IsEnded(err error) bool
- func IsRunnerNotEnabled(err error) bool
- func MustHalt(ctx context.Context, r Runner, services ...*Service)
- func MustHaltTimeout(timeout time.Duration, r Runner, services ...*Service)
- func MustShutdown(ctx context.Context, r Runner)
- func MustShutdownTimeout(timeout time.Duration, r Runner)
- func MustStart(ctx context.Context, runner Runner, services ...*Service)
- func MustStartTimeout(timeout time.Duration, runner Runner, services ...*Service)
- func ShutdownTimeout(timeout time.Duration, runner Runner) error
- func Sleep(ctx context.Context, d time.Duration) (halted bool)
- func StartTimeout(timeout time.Duration, runner Runner, services ...*Service) error
- type Context
- type EndListener
- type Endable
- type Error
- type FailureListener
- func (f *FailureListener) AttachEnd(svc *Service)
- func (f *FailureListener) Failures() <-chan error
- func (f *FailureListener) ForRunner() RunnerOption
- func (f *FailureListener) OnEnd(stage Stage, service *Service, err error)
- func (f *FailureListener) Send(err error)
- func (f *FailureListener) SendNonNil(err error)
- type Name
- type OnEnd
- type OnError
- type Runnable
- type RunnableFunc
- type Runner
- type RunnerOption
- type RunnerState
- type Service
- type ServiceInfo
- type Stage
- type State
- type StateChange
Examples ¶
Constants ¶
const MinHaltableSleep = 50 * time.Millisecond
MinHaltableSleep specifies the minimum amount of time that you must pass to service.Sleep() if you want the Sleep() to be cancellable from a context. Calls to service.Sleep() with a duration smaller than this will simply call time.Sleep().
Variables ¶
var ErrServiceEnded = errors.New("service ended")
ErrServiceEnded is a sentinel error used to indicate that a service ended prematurely but no obvious error that could be returned.
It is a part of the public API so that consumers of this package can return it from their own services.
Functions ¶
func HaltTimeout ¶
func IsAlreadyRunning ¶
func IsRunnerNotEnabled ¶
func MustHalt ¶
MustHalt calls Runner.Halt() and panics if it does not complete successfully.
This is a convenience to allow you to halt in a defer if it is acceptable to crash the server if the service does not Halt.
func MustHaltTimeout ¶
MustHaltTimeout calls MustHalt using context.WithTimeout()
func MustShutdown ¶
MustShutdown calls Runner.Shutdown() and panics if it does not complete successfully.
This is a convenience to allow you to halt in a defer if it is acceptable to crash the server if the runner does not Shutdown.
func MustShutdownTimeout ¶
MustShutdownTimeout calls MustShutdown using context.WithTimeout()
func MustStartTimeout ¶
Types ¶
type Context ¶
type Context interface { context.Context // Ready MUST be called by all services when they have finished // their setup routines and are considered "Ready" to run. If // Ready() returns an error, it MUST be immediately returned. Ready() error // ShouldHalt is used in situations where you can not block listening // to <-Done(), but instead must poll to check if Run() should halt. // Checking <-Done() in a select{} is preferred, but not always practical. ShouldHalt() bool // OnError is used to pass all non-fatal errors that do not cause the // service to halt prematurely up to the runner's listener. OnError(err error) // Runner allows you to access the Runner from which the invocation of // Run() originated. This lets you start child services in the same runner. // It is safe to call any method of this from inside a Runnable. Runner() Runner }
Context is passed to a Service's Run() method. It is used to signal that the service is ready, to receive the signal to halt, or to relay non-fatal errors to the Runner's listener.
All services must either include Context.Done() in their select loop, or regularly poll ctx.ShouldHalt() if they don't make use of one.
service.Context is a context.Context, so you can use it anywhere you would expect to be able to use a context.Context and the thing you are using will be signalled when your service is halted:
func (s *MyService) Run(ctx service.Context) error { if err := ctx.Ready(); err != nil { return err } rqCtx, cancel := context.WithDeadline(ctx, time.Now().Add(2 * time.Second)) defer cancel() client := &http.Client{} rq, err := http.NewRequest("GET", "http://example.com", nil) // ... // The child context passed to the request will be "Done" either when // the service is halted, or the deadline arrives, so this request will // be aborted by the service being Halted: rq = rq.WithContext(rqCtx) rs, err := client.Do(rq) // ... <-ctx.Done() return nil }
type EndListener ¶
type EndListener struct {
// contains filtered or unexported fields
}
EndListener provides a channel that emits errors when services end unexpectedly, and nil when they end expectedly.
WARNING: this API is not stable.
If the ends channel would block, items are discarded.
If you expect a certain number of ends, you should pass at least that number to cap. If not, you may find that any ends that occur between a service stopping unexpectedly and the channel being read from get dropped.
func NewEndListener ¶
func NewEndListener(cap int) *EndListener
func (*EndListener) AttachEnd ¶
func (e *EndListener) AttachEnd(svc *Service)
func (*EndListener) Ends ¶
func (e *EndListener) Ends() <-chan error
func (*EndListener) ForRunner ¶
func (e *EndListener) ForRunner() RunnerOption
func (*EndListener) Send ¶
func (e *EndListener) Send(err error)
Send sends an arbitrary error through the failure channel. It can send nil. err is discarded if Send woudl block.
func (*EndListener) SendNonNil ¶
func (e *EndListener) SendNonNil(err error)
SendNonNil sends an arbitrary error through the failure channel if it is not nil. Use it if you want to mix arbitrary goroutine error handling with service failure.
type FailureListener ¶
type FailureListener struct {
// contains filtered or unexported fields
}
FailureListener provides a channel that emits errors when services end unexpectedly.
WARNING: this API is not stable.
If the failure channel would block, errors are discarded.
If you expect a certain number of errors, you should pass at least that number to cap. If not, you may find that any errors that occur between a service stopping unexpectedly and the channel being read from get dropped.
Example ¶
var wg sync.WaitGroup wg.Add(3) // there are 3 services we should receive ends for onEnd := func(stage Stage, service *Service, err error) { fmt.Println(service.Name, err) wg.Done() } runner := NewRunner(RunnerOnEnd(onEnd)) // This Runnable will run until it is halted: runningChild := RunnableFunc(func(ctx Context) error { if err := ctx.Ready(); err != nil { return err } <-ctx.Done() return nil }) // This Runnable will fail as soon as it is Ready(): failingChild := RunnableFunc(func(ctx Context) error { if err := ctx.Ready(); err != nil { return err } return fmt.Errorf("BOOM!") }) // This service starts two child services and ensures they are halted // before it returns. It will fail when the first child service fails: parent := New("parent", RunnableFunc(func(ctx Context) error { failer := NewFailureListener(1) s1 := New("s1", runningChild).WithEndListener(failer) s2 := New("s2", failingChild).WithEndListener(failer) if err := ctx.Runner().Start(nil, s1, s2); err != nil { return err } defer MustHalt(context.Background(), ctx.Runner(), s1, s2) if err := ctx.Ready(); err != nil { return err } select { case <-ctx.Done(): return nil case err := <-failer.Failures(): return err } })) MustStartTimeout(1*time.Second, runner, parent) wg.Wait()
Output: s2 BOOM! s1 <nil> parent BOOM!
func NewFailureListener ¶
func NewFailureListener(cap int) *FailureListener
func (*FailureListener) AttachEnd ¶
func (f *FailureListener) AttachEnd(svc *Service)
func (*FailureListener) Failures ¶
func (f *FailureListener) Failures() <-chan error
func (*FailureListener) ForRunner ¶
func (f *FailureListener) ForRunner() RunnerOption
func (*FailureListener) OnEnd ¶
func (f *FailureListener) OnEnd(stage Stage, service *Service, err error)
func (*FailureListener) Send ¶
func (f *FailureListener) Send(err error)
Send sends an arbitrary error through the failure channel. It can send nil. err is discarded if Send woudl block.
func (*FailureListener) SendNonNil ¶
func (f *FailureListener) SendNonNil(err error)
SendNonNil sends an arbitrary error through the failure channel if it is not nil. Use it if you want to mix arbitrary goroutine error handling with service failure.
type Runnable ¶
type Runnable interface { // Run the service, blocking the caller until the service is complete. // ready MUST not be nil. ctx.Ready() MUST be called. // // If Run() ends because <-ctx.Done() has yielded, you MUST return nil. // If Run() ends for any other reason, you MUST return an error. Run(ctx Context) error }
type RunnableFunc ¶
RunnableFunc allows you to create a Runnable from a Closure, similar to http.HandlerFunc:
service.New("my-runnable-func", func(ctx service.Context) error { if err := ctx.Ready() { return err } <-ctx.Done() return nil })
func (RunnableFunc) Run ¶
func (r RunnableFunc) Run(ctx Context) error
type Runner ¶
type Runner interface { // Start one or more services in this runner and block until they are Ready. // // Start will unblock when all services have either signalled Ready or have // returned an error indicating they have failed to start, or when the context // provided to Start() is Done(). // // An optional context can be provided via ctx; this allows cancellation to // be declared outside the Runner. You may provide a nil Context. // // If ctx signals Done(), you should try to Halt() the service as you may // leak a goroutine - the service may have become Ready() after you stopped // waiting for it. // Start(ctx context.Context, services ...*Service) error // Halt one or more services that have been started in this runner and block // until they have halted. // // Halt will unblock when all services have finished halting. If Halt returns // an error, one or more of the services may have failed to halt. If a service // fails to Halt(), you may have leaked a goroutine and you should probably // panic(). // // You may Halt() a service in any state. If the service is already Halted // or has already Ended, Halt will immediately succeed for that service. // // An optional context can be provided via ctx; this allows cancellation to // be declared outside the Runner. You may provide a nil Context. // // If the context is cancelled before the service halts, you may have leaked // a goroutine; there is no way for a service lost in this manner to be // recovered using go-service, you will need to build in your own recovery // mechanisms if you want to handle this condition. In practice, a // cancelled 'halt' is probably a good time to panic(), but your specific // application may be able to tolerate some goroutine leaks until you can // fix the issue. Halt(ctx context.Context, services ...*Service) error // Shutdown halts all services started in this runner and prevents new ones // from being started. It will block until all services have Halted. // // If any service fails to halt, err will contain an error for each service // that failed, accessible by calling service.Errors(err). n will contain // the number of services successfully halted. // // An optional context can be provided via ctx; this allows cancellation to // be declared outside the Runner. You may provide a nil Context, but this is // not recommended as your application may block indefinitely. // // It is safe to call Shutdown multiple times. Shutdown(ctx context.Context) (err error) // Enable resumes a Shutdown runner. Enable() error // Suspend prevents new services from being started in this Runner, but // does not shut down existing services. Suspend() error RunnerState() RunnerState State(svc *Service) State // Services returns the list of services running at the time of the call. // time of the call. If StateQuery is provided, only the matching services // are returned. // // Pass limit to restrict the number of returned results. If limit is <= 0, // all matching services are returned. // // You can instruct States to allocate into an existing slice by passing // it in. You should replace it with the return value in case it needs // to grow. Services(state State, limit int, into []ServiceInfo) []ServiceInfo }
Runner Starts, Halts and manages Services.
func NewRunner ¶
func NewRunner(opts ...RunnerOption) Runner
type RunnerOption ¶
type RunnerOption func(rn *runner)
func RunnerOnEnd ¶
func RunnerOnEnd(cb OnEnd) RunnerOption
RunnerOnEnd supplies a default OnEnd callback for use in a Runner.
Your OnEnd function will always be called if a Runnable's Run() function returns, whether that is because the service failed prematurely or because it was halted. If the service has ended because it was halted, err will be nil. If the service has ended for any other reason, err MUST contain an error.
The Runner will be locked while your callback is in progress. You should return as quickly as possible. It is not safe to call methods on a Runner from the OnEnd callback; doing so may cause your application to deadlock. If you need to call the Runner, wrap the body of your OnEnd in an anonymous goroutine:
RunnerOnEnd(func(stage Stage, service *Service, err error) { // Safe: go func() { runner.Start(...) }() // NOT SAFE: runner.Start(...) })
func RunnerOnError ¶
func RunnerOnError(cb OnError) RunnerOption
func RunnerOnState ¶
func RunnerOnState(ch chan<- StateChange) RunnerOption
type RunnerState ¶
type RunnerState int
const ( RunnerEnabled RunnerState = 0 RunnerSuspended RunnerState = 1 RunnerShutdown RunnerState = 2 )
type Service ¶
type Service struct { Name Name Runnable Runnable // OnEnd allows you to supply a callback which will be executed whenever a // Runnable's Run() function returns. // // If the Runnable has returned because it was halted, err will be nil. If // the service has ended for any other reason, err MUST contain an error. // // If the Runner has a default OnEnd function (see RunnerOnEnd), both // callbacks will be called. // // It is not safe to call methods on a Runner from the OnEnd callback; doing so // may cause your application to deadlock. If you need to call the Runner, wrap // the body of your OnEnd in an anonymous goroutine: // // svc.OnEnd = func(stage Stage, service *Service, err error) { // // Safe: // go func() { // runner.Start(...) // }() // // // NOT SAFE: // runner.Start(...) // } // OnEnd OnEnd // OnStateChange allows you to receive notifications when the state // of a service changes. The Runner will drop state changes if this // channel is not able to receive them, so supply a big buffer if // that concerns you. OnState chan StateChange }
Service wraps a Runnable with common properties.
func (*Service) WithEndListener ¶
type ServiceInfo ¶
type State ¶
type State int
type StateChange ¶
Source Files ¶
Directories ¶
Path | Synopsis |
---|---|
internal
|
|
Package services contains a set of convenience functions for managing a global service.Runner.
|
Package services contains a set of convenience functions for managing a global service.Runner. |
Package servicetest contains tools and tests for the service package.
|
Package servicetest contains tools and tests for the service package. |
Package serviceutil contains useful experiments and utilities that have not yet reached the level of robustness required to be part of the service package.
|
Package serviceutil contains useful experiments and utilities that have not yet reached the level of robustness required to be part of the service package. |