Documentation ¶
Overview ¶
Llama client to pull metrics from Llama collectors
LLAMA Collector sends UDP probes to a set of target reflectors and provides statistics about their latency and reachability via an API.
Mock Llama client used in `client_test.go`
Functionality for sending and receiving UDP probes on a socket.
portgroup defines PortGroup, which is used to multiplex UDPAddr structs to multiple ports via parallel channels.
LLAMA Scraper pulls stats from Collectors and then writes them to the indicated database.
Tags is a helper description for a structure that stores a map of attributes and values for a given key.
Example: Tags["1.2.3.4"]["dst_hostname"] = "localhost" Tags["1.2.3.4"]["dst_cluster"] = "mycluster"
Index ¶
- Constants
- func CalcCounts(results []*Result, summary *Summary)
- func CalcLoss(summary *Summary)
- func CalcRTT(results []*Result, summary *Summary)
- func EnableTimestamps(conn *net.UDPConn)
- func FileCloseHandler(f *os.File)
- func GetTos(conn *net.UDPConn) byte
- func HandleError(err error)
- func HandleFatalError(err error)
- func HandleMinorError(err error)
- func IDToBytes(id string) [10]byte
- func LocalUDPAddr(conn *net.UDPConn) (*net.UDPAddr, string, error)
- func NewClient(hostname string, port string) *client
- func NewID() string
- func NowUint64() uint64
- func NsToMs(ns float64) float64
- func PackUdpData(data *UdpData) ([]byte, error)
- func RTT(probe *Probe, result *Result) error
- func Receive(data []byte, oob []byte, conn *net.UDPConn) ([]byte, []byte, *net.UDPAddr)
- func Reflect(conn *net.UDPConn, rl *rate.Limiter)
- func Send(data []byte, conn *net.UDPConn, addr *net.UDPAddr)
- func SetRecvBufferSize(conn *net.UDPConn, size int)
- func SetTos(conn *net.UDPConn, tos byte)
- type API
- type APIConfig
- type Client
- type Collector
- func (c *Collector) LoadConfig()
- func (c *Collector) Reload()
- func (c *Collector) Run()
- func (c *Collector) Setup()
- func (c *Collector) SetupAPI()
- func (c *Collector) SetupSummarizer()
- func (c *Collector) SetupTagSet()
- func (c *Collector) SetupTestRunner(test TestConfig)
- func (c *Collector) SetupTestRunners()
- func (c *Collector) Stop()
- type CollectorConfig
- type DataPoint
- func (dp *DataPoint) FromPD(pd *PathDist)
- func (dp *DataPoint) FromSummary(s *Summary)
- func (dp *DataPoint) SetFieldFloat64(k string, v float64)
- func (dp *DataPoint) SetFieldInt(k string, v int)
- func (dp *DataPoint) SetMeasurement(s string)
- func (dp *DataPoint) SetTime(t time.Time)
- func (dp *DataPoint) UpdateTags(t Tags)
- type Getter
- type IDBFloat64
- type InfluxDbWriter
- type LegacyCollectorConfig
- type MockClient
- type PathDist
- type Points
- type Port
- type PortConfig
- type PortGroup
- type PortGroupConfig
- type PortGroupsConfig
- type PortsConfig
- type Probe
- type RateLimitConfig
- type RateLimitsConfig
- type Result
- type ResultHandler
- type Scraper
- type SummarizationConfig
- type Summarizer
- type Summary
- type TagSet
- type Tags
- type TargetConfig
- type TargetSet
- type TargetsConfig
- type TestConfig
- type TestRunner
- func (tr *TestRunner) Add(addrs ...*net.UDPAddr)
- func (tr *TestRunner) AddNewPort(portStr string, tos byte, cTimeout time.Duration, cCleanRate time.Duration, ...)
- func (tr *TestRunner) Del(addr *net.UDPAddr)
- func (tr *TestRunner) Run()
- func (tr *TestRunner) Set(targets []*net.UDPAddr)
- func (tr *TestRunner) Stop()
- type TestsConfig
- type UdpData
Constants ¶
const ( // Listens on any addr to an automatically assigned port number DefaultAddrStr = "0.0.0.0:0" DefaultTos = byte(0) DefaultRcvBuff = 2097600 // 2MiB DefaultReadTimeout = 200 * time.Millisecond DefaultCacheTimeout = 2 * time.Second DefaultCacheCleanRate = 5 * time.Second ExpireNow = time.Nanosecond )
const DEFAULT_CHANNEL_SIZE int64 = 100 // Default size used for buffered channels.
const DefaultTimeout = time.Second * 5
Set default timeout for writes to 5 seconds This may be worth adding as a parameter in the future
Variables ¶
This section is empty.
Functions ¶
func CalcCounts ¶
CalcCounts will calculate the Sent and Lost counts on the provided summary, based on the provided results.
func CalcLoss ¶
func CalcLoss(summary *Summary)
CalcLoss will calculate the Loss percentage (out of 1) based on the Sent and Lost vaules of the provided summary.
func CalcRTT ¶
CalcRT will calculate the RTT values for the provided summary, based on the provided results.
func EnableTimestamps ¶
EnableTimestamps enables kernel receive timestamping of packets on the provided conn.
The timestamp values can later be extracted in the oob data from Receive.
func FileCloseHandler ¶
FileCloseHandler will close an open File and handle the resulting error.
func HandleError ¶
func HandleError(err error)
func HandleFatalError ¶
func HandleFatalError(err error)
HandleError receives an error, then logs and exits if not nil. TODO(dmar): Create additional simple handlers for non-fatal issues
func HandleMinorError ¶
func HandleMinorError(err error)
func LocalUDPAddr ¶
LocalUDPAddr returns the UDPAddr and net for the provided UDPConn.
For UDPConn instances, net is generaly 'udp'.
func NewClient ¶
NewClient creates a new collector client with hostname and port TODO(dmar): This is likely overkill and should be simplified.
func NewID ¶
func NewID() string
NewID returns 10 bytes of a new UUID4 as a string.
This should be unique enough for short-lived cases, but as it's only a partial UUID4.
func NowUint64 ¶
func NowUint64() uint64
NowUint64 returns the current time in nanoseconds as a uint64.
func PackUdpData ¶
TODO(dmar): These should be functions attached to `UdpData` PackUdpData takes a UdpData instances and converts it to a byte array.
func Receive ¶
Receive accepts UDP packets on the provided conn and returns the data and and control message slices, as well as the UDPAddr it was received from.
func Reflect ¶
Reflect will listen on the provided UDPConn and will send back any UdpData compliant packets that it receives, in compliance with the RateLimiter.
func SetRecvBufferSize ¶
SetRecvBufferSize sets the size of the receive buffer for the conn to the provided size in bytes. TODO(dmar): Validate and replace this with a simple call to conn.SetReadBuffer
Types ¶
type API ¶
type API struct {
// contains filtered or unexported fields
}
API represnts the HTTP server answering queries for collected data.
func NewAPI ¶
func NewAPI(s *Summarizer, t TagSet, addr string) *API
New returns an initialized API struct.
func (*API) InfluxHandler ¶
func (api *API) InfluxHandler(rw http.ResponseWriter, request *http.Request)
InfluxHandler handles requests for InfluxDB formatted summaries.
func (*API) MergeUpdateTagSet ¶
MergeUpdateTagSet combines a provided TagSet with the existing one
func (*API) Run ¶
func (api *API) Run()
Run calls RunForever in a separate goroutine for non-blocking behavior.
func (*API) RunForever ¶
func (api *API) RunForever()
RunForever sets up the handlers above and then listens for requests until stopped or a fatal error occurs.
Calling this will block until stopped/crashed.
func (*API) StatusHandler ¶
func (api *API) StatusHandler(rw http.ResponseWriter, request *http.Request)
StatusHandler acts as a back healthcheck and simply returns 200 OK.
type APIConfig ¶
type APIConfig struct {
Bind string `yaml:"bind"`
}
APIConfig describes the parameters for the JSON HTTP API.
type Collector ¶
type Collector struct {
// contains filtered or unexported fields
}
Collector reads a YAML configuration, performs UDP probe tests against targets, and provides summaries of the results via a JSON HTTP API.
func (*Collector) LoadConfig ¶
func (c *Collector) LoadConfig()
LoadConfig loads the collector's configuration from CLI flag if provided, otherwise the default.
func (*Collector) Reload ¶
func (c *Collector) Reload()
Reload causes the config to be reread, and test runners recreated
func (*Collector) Run ¶
func (c *Collector) Run()
Run starts all of the components of the collector and begins testing.
func (*Collector) Setup ¶
func (c *Collector) Setup()
Setup is a generally wrapper around all of the other Setup* functions.
func (*Collector) SetupAPI ¶
func (c *Collector) SetupAPI()
SetupAPI creates and performs initial setup of the API based on the config.
func (*Collector) SetupSummarizer ¶
func (c *Collector) SetupSummarizer()
SetupSummarizer creates the Summarizer and ResultHandlers that will summarize and save the test results, based on the config.
func (*Collector) SetupTagSet ¶
func (c *Collector) SetupTagSet()
SetupTagSet loads the tags for targets, based on the config, that will be applied to summarized results.
func (*Collector) SetupTestRunner ¶
func (c *Collector) SetupTestRunner(test TestConfig)
SetupTestRunner takes parameters from the loaded config, and creates the specified TestConfig.
func (*Collector) SetupTestRunners ¶
func (c *Collector) SetupTestRunners()
SetupTestRunners creates all the `tests` that are defined in the config.
type CollectorConfig ¶
type CollectorConfig struct { Summarization SummarizationConfig `yaml:"summarization"` API APIConfig `yaml:"api"` Ports PortsConfig `yaml:"ports"` PortGroups PortGroupsConfig `yaml:"port_groups"` RateLimits RateLimitsConfig `yaml:"rate_limits"` Tests TestsConfig `yaml:"tests"` Targets TargetsConfig `yaml:"targets"` }
CollectorConfig wraps all of the above structs/maps/slices and defines the overall configuration for a collector.
func NewCollectorConfig ¶
func NewCollectorConfig(data []byte) (*CollectorConfig, error)
NewCollectorConfig provides a parsed CollectorConfig based on the provided data.
`data` is expected to be a byte slice version of a YAML CollectorConfig.
func NewDefaultCollectorConfig ¶
func NewDefaultCollectorConfig() (*CollectorConfig, error)
NewDefaultCollectorConfig provides a sensible default collector config.
type DataPoint ¶
type DataPoint struct { Fields map[string]IDBFloat64 `json:"fields"` Tags Tags `json:"tags"` Time time.Time `json:"time"` Measurement string `json:"measurement"` }
DataPoint represents a single "point" of data for InfluxDB.
func NewDataPoint ¶
func NewDataPoint() *DataPoint
NewDataPoint provides an empty and usable DataPoint.
func NewDataPointFromSummary ¶
NewDataPointFromSummary provides a new DataPoint populated with values in s and t.
func NewDataPointsFromSummaries ¶
NewFromSummaries allows bulk operations against New by providing a slice of summaries and map of Tags (t).
func (*DataPoint) FromSummary ¶
FromSummary updates the values of dp to reflect what is available in s.
func (*DataPoint) SetFieldFloat64 ¶
SetFieldFloat64 sets the value of "field" k to the value v.
func (*DataPoint) SetFieldInt ¶
SetFieldINt sets the value of "field" k to the value v.
func (*DataPoint) SetMeasurement ¶
SetMeasurements set the measurement of the dp to the value of s.
func (*DataPoint) UpdateTags ¶
UpdateTags populates the tags of the dp based on the provided Tags map.
type IDBFloat64 ¶
type IDBFloat64 float64
IDBFloat64 is to allow custom JSON marshalling in the API, so it actually formats like a float consistently
func (IDBFloat64) MarshalJSON ¶
func (n IDBFloat64) MarshalJSON() ([]byte, error)
TODO(dmar): This should be handled in the scraper by always writing numbers
as floats. But for now, ensure that float64 values without decimal precision are still written in decimal format. Otherwise, it turns into an int along the way and makes InfluxDB angry. Another alternative, GRPC and Protobufs instead of a JSON HTTP API.
type InfluxDbWriter ¶
type InfluxDbWriter struct {
// contains filtered or unexported fields
}
InfluxDbWriter is used for writing datapoints to an InfluxDB instance
func NewInfluxDbWriter ¶
func NewInfluxDbWriter(host string, port string, user string, pass string, db string) (*InfluxDbWriter, error)
NewInfluxDbWriter provides a client for writing LLAMA datapoints to InfluxDB
func (*InfluxDbWriter) Batch ¶
func (w *InfluxDbWriter) Batch(points Points) (influxdb_client.BatchPoints, error)
Batch will group the points into a batch for writing to the database
func (*InfluxDbWriter) BatchWrite ¶
func (w *InfluxDbWriter) BatchWrite(points Points) error
BatchWrite will group and write the indicates points to the associated InfluxDB host
func (*InfluxDbWriter) Close ¶
func (w *InfluxDbWriter) Close() error
Close will close the InfluxDB client connection and release any associated resources
func (*InfluxDbWriter) Write ¶
func (w *InfluxDbWriter) Write(batch influxdb_client.BatchPoints) error
Write will commit the batched points to the database
type LegacyCollectorConfig ¶
LegacyCollectorConfig is for backward compatibility with the existing LLAMA config and represents only a map of targets to tags.
func NewLegacyCollectorConfig ¶
func NewLegacyCollectorConfig(data []byte) (*LegacyCollectorConfig, error)
NewLegacyCollectorConfig creates a new LegacyCollectorConfig struct based on the provided data, which is expected to be a YAML representation of the config.
func (*LegacyCollectorConfig) ToDefaultCollectorConfig ¶
func (legacy *LegacyCollectorConfig) ToDefaultCollectorConfig(port int64) (*CollectorConfig, error)
ToDefaultCollectorConfig converts a LegacyCollectorConfig to CollectorConfig by merging with the defaults and applying the provided port on targets.
type MockClient ¶
type MockClient struct { NextPoints Points NextErr error // contains filtered or unexported fields }
func NewMock ¶
func NewMock(serverHost string) (*MockClient, error)
deadcode: NewMock is grandfathered in as legacy code
func (*MockClient) GetPoints ¶
func (m *MockClient) GetPoints() (Points, error)
func (*MockClient) Hostname ¶
func (m *MockClient) Hostname() string
func (*MockClient) Port ¶
func (m *MockClient) Port() string
func (*MockClient) Run ¶
func (m *MockClient) Run()
type PathDist ¶
type PathDist struct { SrcIP net.IP SrcPort int DstIP net.IP DstPort int Proto string // 'udp' generally }
PathDist -> Path Distinguisher, uniquely IDs the components that determine path selection.
type Port ¶
type Port struct {
// contains filtered or unexported fields
}
Port represents a socket and its associated caching, inputs, and outputs.
func NewDefault ¶
NewDefault creates a new Port using default settings.
func NewPort ¶
func NewPort(conn *net.UDPConn, tosend chan *net.UDPAddr, stop chan bool, cbc chan *Probe, cTimeout time.Duration, cCleanRate time.Duration, readTimeout time.Duration) *Port
New creates and returns a new Port with associated inputs, outputs, and caching mechanisms.
func (*Port) Recv ¶
func (p *Port) Recv()
Recv listens on the Port for returning probes and updates them in the cache.
Once probes are received, they are located in the cache, updated, and then set for immediate expiration. If a probe is received but has no entry in the cache, it most likely exceeded the timeout.
func (*Port) Send ¶
func (p *Port) Send()
Send waits to get UDPAddr targets and sends probes to them using the associated Port.
After sending the probe, it is added to a cache with a unique ID, which is used for retrieving later. The cache will also utilize a timeout to expire probes that haven't returned in time.
type PortConfig ¶
type PortConfig struct { IP string `yaml:"ip"` Port int64 `yaml:"port"` Tos int64 `yaml:"tos"` Timeout int64 `yaml:"timeout"` }
PortConfig describes the configuration for a single Port.
type PortGroup ¶
type PortGroup struct {
// contains filtered or unexported fields
}
func NewPortGroup ¶
New creates a new PortGroup utilizing a set of input, output, and signalling channels.
stop is used to signal stopping of the PortGroup and all ports. cbc is used as a callback for completed or timedout probes from all ports. tosend is used to receive UDPAddr targets for sending to probes, and is muxed across all Ports in the PortGroup.
func (*PortGroup) Add ¶
Add will add a Port and channel to the PortGroup.
This must NOT be used after running, as it is currently not threadsafe. TODO(dmar): In the future, if doing this is desired, add a mutex and
appropriate locking.
func (*PortGroup) AddNew ¶
func (pg *PortGroup) AddNew(portStr string, tos byte, cTimeout time.Duration, cCleanRate time.Duration, readTimeout time.Duration) ( *Port, chan *net.UDPAddr)
AddNew will create a new Port and add it to the PortGroup via Add.
func (*PortGroup) Del ¶
Del removes a Port from the PortGroup.
This must NOT be done after running. TODO(dmar): If this is desirable, similar to Add, a mutex and locking
will be needed and adds overhead.
func (*PortGroup) Run ¶
func (pg *PortGroup) Run()
Run will start sending/receiving on all Ports in the PortGroup, and then then loop muxing inbound UDPAddrs to all ports until stopped.
TODO(dmar): Add something here to prevent ports from being added after
it has started running. Otherwise, a mutex is needed to to sync things, though that may be a fine option as long as there aren't too many goroutines or ports.
TODO(dmar): Allow an arg for starting multiple goroutines? Otherwise
leave that to higher level stuff.
type PortGroupConfig ¶
type PortGroupConfig struct { Port string `yaml:"port"` // Should correspond with a PortsConfig key Count int64 `yaml:"count"` }
PortGroupConfig describes a set of identical Ports in a PortGroup.
type PortGroupsConfig ¶
type PortGroupsConfig map[string][]PortGroupConfig
PortGroupsConfig is a mapping of port group "name" to PortGroupConfigs.
type PortsConfig ¶
type PortsConfig map[string]PortConfig
PortsConfig is a mapping of port "name" to a PortConfig.
type Probe ¶
Probe represents a single UDP probe that was sent from, and (hopefully) received back, a Port.
func IfaceToProbe ¶
IfaceToProbe attempts to convert an anonymous object to a Probe, and returns and error if the operation failed.
type RateLimitConfig ¶
type RateLimitConfig struct {
CPS float64 `yaml:"cps"` // Cycles per second
}
RateLimitConfig describes the configuration for a rate limiter.
type RateLimitsConfig ¶
type RateLimitsConfig map[string]RateLimitConfig
RateLimitsConfig is a mapping of "name" to RateLimitConfig.
type Result ¶
type Result struct { Pd *PathDist // Characteristics that make this path unique RTT uint64 // Round trip time in nanoseconds Done uint64 // When the test completed (was received by Port) in ns Lost bool // If the Probe was lost and never actually completed }
Result defines characteristics of a single completed Probe.
type ResultHandler ¶
type ResultHandler struct {
// contains filtered or unexported fields
}
ResultHandler is a post-processor for Probes and converts them to Results.
func NewResultHandler ¶
func NewResultHandler(in chan *Probe, out chan *Result) *ResultHandler
New creates a new ResultHandler that utilizes the provided in and out channels.
func (*ResultHandler) Run ¶
func (rh *ResultHandler) Run()
Run will start the ResultHandler in a new goroutine, and cause it to forever receive Probes, process them and pass their results out.
type Scraper ¶
type Scraper struct {
// contains filtered or unexported fields
}
Scraper pulls stats from collectors and writes them to a backend
type SummarizationConfig ¶
type SummarizationConfig struct { Interval int64 `yaml:"interval"` Handlers int64 `yaml:"handlers"` }
SummarizationConfig describes the parameters for setting up a Summarizer and related ResultHandlers.
type Summarizer ¶
type Summarizer struct { // NOTE(dmar): For posterity, use value references for mutexes, not pointers CMutex sync.RWMutex Cache []*Summary // contains filtered or unexported fields }
Summarizer stores results and summarizes them at intervals.
func NewSummarizer ¶
func NewSummarizer(in chan *Result, interval time.Duration) *Summarizer
New returns a new Summarizer, based on the provided parameters.
func (*Summarizer) Run ¶
func (s *Summarizer) Run()
Run causes the summarizer to infinitely wait for new results, store them, and then summarize at an interval.
When results are summarized, they are removed and won't be summarized again.
func (*Summarizer) Stop ¶
func (s *Summarizer) Stop()
Stop will stop the summarizer from receiving results or summarizing them.
type Summary ¶
type Summary struct { Pd *PathDist RTTAvg float64 RTTMin float64 RTTMax float64 Sent int Lost int Loss float64 TS time.Time // No longer used, but keeping for posterity }
Summary represents summaried results and statistics about them.
type TargetConfig ¶
TargetConfig describes a single target for testing, including tags that are applied to the resulting summaries.
TODO(dmar): Restructure this to be more Dropbox specific, and reduce the
data being included in this config. Most of this can come from a base, and then be populated by MDB queries.
func (*TargetConfig) AddrString ¶
func (tc *TargetConfig) AddrString() string
AddrString converts the tc into a string formated "IP:port" combo.
func (*TargetConfig) ResolveUDPAddr ¶
func (tc *TargetConfig) ResolveUDPAddr() (*net.UDPAddr, error)
ResolveUDPAddr converts the tc into a net.UDPAddr pointer.
type TargetSet ¶
type TargetSet []TargetConfig
TargetSet is a slice of TargetConfig structs.
func (TargetSet) IntoTagSet ¶
IntoTagSet is similar to TagSet but updates the provided tagset instead of creating a new one.
func (TargetSet) ListResolvedTargets ¶
ListResolvedTargets provides a slice of net.UDPAddr pointers for all of the targets in the ts, and will return with an error as soon as one is hit.
func (TargetSet) ListTargets ¶
ListTargets provides a slice of "IP:port" string representations for all of the targets in the ts.
type TargetsConfig ¶
TargetsConfig is a mapping of "name" to TargetSet slice.
func (TargetsConfig) IntoTagSet ¶
func (tc TargetsConfig) IntoTagSet(ts TagSet)
IntoTagSet is a wrapper about the same function for each contained TargetSet and merges them into an existing ts.
func (TargetsConfig) TagSet ¶
func (tc TargetsConfig) TagSet() TagSet
TagSet is a wrapper, and merges the TagSet output for all TargetSet slices within the tc.
type TestConfig ¶
type TestConfig struct { Targets string `yaml:"targets"` // Should correspond with a TargetsConfig key PortGroup string `yaml:"port_group"` // Should correspond with a PortGroupsConfig key RateLimit string `yaml:"rate_limit"` // Should correspond with a RateLimitsConfig key }
TestConfig describes the elements of a test, for use by TestRunner, which correspond to their respective named elements in the config.
Ex. A `targets` value of "default" in the config would correspond to a TargetsConfig key of "default" which contains the definitions of targets.
type TestRunner ¶
type TestRunner struct {
// contains filtered or unexported fields
}
TestRunner repeatedly runs through a list of targets and passes them down to a PortGroup for processing.
func NewTestRunner ¶
func NewTestRunner(cbc chan *Probe, rl *rate.Limiter) *TestRunner
New creates and returns a new TestRunner instance.
`cbc` is a channel for accepting completed Probes. `rl` is a rate limiter which is used to throttle the number of cycles that may be completed per second.
func (*TestRunner) Add ¶
func (tr *TestRunner) Add(addrs ...*net.UDPAddr)
Add will add a variable number of addrs to the slice of targets for processing.
NOTE: This will block during cycles. So it should be avoided when possible.
It's better to just use `Set` to replace the whole thing. Either way, this change will only go into effect between cycles.
func (*TestRunner) AddNewPort ¶
func (tr *TestRunner) AddNewPort(portStr string, tos byte, cTimeout time.Duration, cCleanRate time.Duration, readTimeout time.Duration)
AddNewPort will add a new Port to the TestRunner's PortGroup.
See PortGroup.AddNew for more details on these arguments.
NOTE: This is basically just a passthrough for PortGroup.AddNew until
the pattern is better understood and this can be cleaned up.
func (*TestRunner) Del ¶
func (tr *TestRunner) Del(addr *net.UDPAddr)
Del will remove all occurrences of a target addr from the slice of targets.
NOTE: This will block during cycles. It will also take longer as the
number of targets increases. So it should be avoided when possible. It's better to just use `Set` to replace the whole thing. Either way, this change will only go into effect between cycles.
func (*TestRunner) Run ¶
func (tr *TestRunner) Run()
Run starts the TestRunner and begins cycling through targets.
func (*TestRunner) Set ¶
func (tr *TestRunner) Set(targets []*net.UDPAddr)
Set will replace the current slice of targets with the provided one.
NOTE: This will block during cycles. It is generally advised to use `Set`
over `Add` and `Del` in making larger changes or operating on multiple targets. It's just more atomic.
func (*TestRunner) Stop ¶
func (tr *TestRunner) Stop()
Stop will stop the TestRunner after the current cycle and any underlying PortGroup and Port(s).