feat: tutorial on how to write the torsf experiment (#390)
Original tracking issue for Sprint 41: https://github.com/ooni/probe/issues/1507 Follow-up work in Sprint 42 tracked by: https://github.com/ooni/probe/issues/1689
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
|
||||
# Chapter III: starting to simulate a real torsf experiment
|
||||
|
||||
In this chapter we will improve upon what we did in the previous
|
||||
chapter by creating runner code for the `torsf` experiment. We will
|
||||
not, yet, run the real experiment, but we will instead write
|
||||
simple code that pretends to run a `tor` bootstrap using snowflake.
|
||||
|
||||
(This file is auto-generated from the corresponding source file,
|
||||
so make sure you don't edit it manually.)
|
||||
|
||||
### The TestKeys structure
|
||||
|
||||
Let us start by defining the `TestKeys` structure that contains
|
||||
the experiment specific results. As we have already seen in
|
||||
Chapter I, this structure must contain two fields. The bootstrap
|
||||
time for the experiment and the failure.
|
||||
|
||||
```Go
|
||||
type TestKeys struct {
|
||||
BootstrapTime float64 `json:"bootstrap_time"`
|
||||
Failure *string `json:"failure"`
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Rewriting the Run method
|
||||
|
||||
Next we will rewrite the Run method. We will arrange for this
|
||||
method to fill the `measurement`, to setup the timeout, and to
|
||||
print periodic updates via the `callbacks`. We will defer the
|
||||
real work to a private function called `run`.
|
||||
|
||||
```Go
|
||||
func (m *Measurer) Run(
|
||||
ctx context.Context, sess model.ExperimentSession,
|
||||
measurement *model.Measurement, callbacks model.ExperimentCallbacks,
|
||||
) error {
|
||||
```
|
||||
|
||||
Let's create an instance of `TestKeys` and let's modify
|
||||
the `measurement` to refer to such an instance.
|
||||
|
||||
```Go
|
||||
testkeys := &TestKeys{}
|
||||
measurement.TestKeys = testkeys
|
||||
```
|
||||
|
||||
Next, we record the current time and we modify the
|
||||
context to have a timeout after 300 seconds. Because
|
||||
Snowflake *may* take a long time to bootstrap, we
|
||||
need to specify a generous timeout here.
|
||||
|
||||
```Go
|
||||
start := time.Now()
|
||||
const maxRuntime = 300 * time.Second
|
||||
ctx, cancel := context.WithTimeout(ctx, maxRuntime)
|
||||
defer cancel()
|
||||
```
|
||||
|
||||
Okay, now we are ready to defer the real work to
|
||||
the internal `run` function. We first create a
|
||||
channel to receive the result of `run`. Then, we
|
||||
create a ticker to emit periodic updates. We
|
||||
emit an update every 250 milliseconds, which is
|
||||
a reasonably smooth way of increasing a progress
|
||||
bar (progress is indeed used to move progress bars
|
||||
both in OONI Probe Desktop and mobile.)
|
||||
|
||||
```Go
|
||||
errch := make(chan error)
|
||||
ticker := time.NewTicker(250 * time.Millisecond)
|
||||
defer ticker.Stop()
|
||||
```
|
||||
|
||||
Now we defer the real work to `run`, which will
|
||||
run in a background goroutine.
|
||||
|
||||
```Go
|
||||
go m.run(ctx, sess, testkeys, errch)
|
||||
```
|
||||
|
||||
While `run` is running, we loop and check which
|
||||
channel has become ready.
|
||||
|
||||
If the `errch` channel is ready, it means that `run` is
|
||||
terminated, so we return to the caller.
|
||||
|
||||
Instead, if `ticker.C` is ready, we emit a progress
|
||||
update using the `callbacks`.
|
||||
|
||||
```Go
|
||||
for {
|
||||
select {
|
||||
case err := <-errch:
|
||||
callbacks.OnProgress(1.0, "torsf experiment is finished")
|
||||
return err
|
||||
case <-ticker.C:
|
||||
progress := time.Since(start).Seconds() / maxRuntime.Seconds()
|
||||
callbacks.OnProgress(progress, "torsf experiment is running")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### The run function
|
||||
|
||||
We will now implement the `run` function. For now, this function
|
||||
will not do any real work, but it will just pretend to do work.
|
||||
|
||||
Note how we sleep for some time, set the `BootstrapTime` field
|
||||
of the `TestKeys`, and then return using `errch`.
|
||||
|
||||
```Go
|
||||
func (m *Measurer) run(ctx context.Context,
|
||||
sess model.ExperimentSession, testkeys *TestKeys, errch chan<- error) {
|
||||
fakeBootstrapTime := 10 * time.Second
|
||||
time.Sleep(fakeBootstrapTime)
|
||||
testkeys.BootstrapTime = fakeBootstrapTime.Seconds()
|
||||
errch <- nil
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Running the code
|
||||
|
||||
It's now time to run the new code we've written:
|
||||
|
||||
```
|
||||
$ go run ./experiment/torsf/chapter03 | jq
|
||||
2021/06/21 21:21:18 info [ 0.1%] torsf experiment is running
|
||||
2021/06/21 21:21:19 info [ 0.2%] torsf experiment is running
|
||||
[...]
|
||||
2021/06/21 21:21:28 info [ 3.3%] torsf experiment is running
|
||||
2021/06/21 21:21:28 info [100.0%] torsf experiment is finished
|
||||
{
|
||||
"data_format_version": "",
|
||||
"input": null,
|
||||
"measurement_start_time": "",
|
||||
"probe_asn": "",
|
||||
"probe_cc": "",
|
||||
"probe_network_name": "",
|
||||
"report_id": "",
|
||||
"resolver_asn": "",
|
||||
"resolver_ip": "",
|
||||
"resolver_network_name": "",
|
||||
"software_name": "",
|
||||
"software_version": "",
|
||||
"test_keys": {
|
||||
"bootstrap_time": 10,
|
||||
"failure": null
|
||||
},
|
||||
"test_name": "",
|
||||
"test_runtime": 0,
|
||||
"test_start_time": "",
|
||||
"test_version": ""
|
||||
}
|
||||
```
|
||||
|
||||
You see that now we're filling the bootstrap time and we're
|
||||
also printing progress using `callbacks`.
|
||||
|
||||
In the next chapter, we'll replace the stub `run` implementation
|
||||
with a real implementation using Snowflake.
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
|
||||
"github.com/apex/log"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/mockable"
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/model"
|
||||
"golang.org/x/sys/execabs"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if _, err := execabs.LookPath("tor"); err != nil {
|
||||
log.Fatal("cannot find the tor executable in path")
|
||||
}
|
||||
tempdir, err := ioutil.TempDir("", "")
|
||||
if err != nil {
|
||||
log.WithError(err).Fatal("cannot create temporary directory")
|
||||
}
|
||||
m := NewExperimentMeasurer(Config{})
|
||||
ctx := context.Background()
|
||||
measurement := &model.Measurement{}
|
||||
callbacks := model.NewPrinterCallbacks(log.Log)
|
||||
sess := &mockable.Session{
|
||||
MockableLogger: log.Log,
|
||||
MockableTempDir: tempdir,
|
||||
}
|
||||
if err = m.Run(ctx, sess, measurement, callbacks); err != nil {
|
||||
log.WithError(err).Fatal("torsf experiment failed")
|
||||
}
|
||||
data, err := json.Marshal(measurement)
|
||||
if err != nil {
|
||||
log.WithError(err).Fatal("json.Marshal failed")
|
||||
}
|
||||
fmt.Printf("%s\n", data)
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/ooni/probe-cli/v3/internal/engine/model"
|
||||
)
|
||||
|
||||
// Config contains config for the torsf experiment.
|
||||
type Config struct{}
|
||||
|
||||
// Measurer is the torsf measurer.
|
||||
type Measurer struct {
|
||||
config Config
|
||||
}
|
||||
|
||||
// newExperimentMeasurer creates a new ExperimentMeasurer for torsf.
|
||||
func NewExperimentMeasurer(config Config) model.ExperimentMeasurer {
|
||||
return &Measurer{config: config}
|
||||
}
|
||||
|
||||
// ExperimentName implements ExperimentMeasurer.ExperimentName.
|
||||
func (m *Measurer) ExperimentName() string {
|
||||
return "torsf"
|
||||
}
|
||||
|
||||
// ExperimentVersion implements ExperimentMeasurer.ExperimentVersion.
|
||||
func (m *Measurer) ExperimentVersion() string {
|
||||
return "0.1.0"
|
||||
}
|
||||
|
||||
// -=-=- StartHere -=-=-
|
||||
//
|
||||
// # Chapter III: starting to simulate a real torsf experiment
|
||||
//
|
||||
// In this chapter we will improve upon what we did in the previous
|
||||
// chapter by creating runner code for the `torsf` experiment. We will
|
||||
// not, yet, run the real experiment, but we will instead write
|
||||
// simple code that pretends to run a `tor` bootstrap using snowflake.
|
||||
//
|
||||
// (This file is auto-generated from the corresponding source file,
|
||||
// so make sure you don't edit it manually.)
|
||||
//
|
||||
// ### The TestKeys structure
|
||||
//
|
||||
// Let us start by defining the `TestKeys` structure that contains
|
||||
// the experiment specific results. As we have already seen in
|
||||
// Chapter I, this structure must contain two fields. The bootstrap
|
||||
// time for the experiment and the failure.
|
||||
//
|
||||
// ```Go
|
||||
type TestKeys struct {
|
||||
BootstrapTime float64 `json:"bootstrap_time"`
|
||||
Failure *string `json:"failure"`
|
||||
}
|
||||
|
||||
// ```
|
||||
//
|
||||
// ### Rewriting the Run method
|
||||
//
|
||||
// Next we will rewrite the Run method. We will arrange for this
|
||||
// method to fill the `measurement`, to setup the timeout, and to
|
||||
// print periodic updates via the `callbacks`. We will defer the
|
||||
// real work to a private function called `run`.
|
||||
//
|
||||
// ```Go
|
||||
func (m *Measurer) Run(
|
||||
ctx context.Context, sess model.ExperimentSession,
|
||||
measurement *model.Measurement, callbacks model.ExperimentCallbacks,
|
||||
) error {
|
||||
// ```
|
||||
//
|
||||
// Let's create an instance of `TestKeys` and let's modify
|
||||
// the `measurement` to refer to such an instance.
|
||||
//
|
||||
// ```Go
|
||||
testkeys := &TestKeys{}
|
||||
measurement.TestKeys = testkeys
|
||||
// ```
|
||||
//
|
||||
// Next, we record the current time and we modify the
|
||||
// context to have a timeout after 300 seconds. Because
|
||||
// Snowflake *may* take a long time to bootstrap, we
|
||||
// need to specify a generous timeout here.
|
||||
//
|
||||
// ```Go
|
||||
start := time.Now()
|
||||
const maxRuntime = 300 * time.Second
|
||||
ctx, cancel := context.WithTimeout(ctx, maxRuntime)
|
||||
defer cancel()
|
||||
// ```
|
||||
//
|
||||
// Okay, now we are ready to defer the real work to
|
||||
// the internal `run` function. We first create a
|
||||
// channel to receive the result of `run`. Then, we
|
||||
// create a ticker to emit periodic updates. We
|
||||
// emit an update every 250 milliseconds, which is
|
||||
// a reasonably smooth way of increasing a progress
|
||||
// bar (progress is indeed used to move progress bars
|
||||
// both in OONI Probe Desktop and mobile.)
|
||||
//
|
||||
// ```Go
|
||||
errch := make(chan error)
|
||||
ticker := time.NewTicker(250 * time.Millisecond)
|
||||
defer ticker.Stop()
|
||||
// ```
|
||||
//
|
||||
// Now we defer the real work to `run`, which will
|
||||
// run in a background goroutine.
|
||||
//
|
||||
// ```Go
|
||||
go m.run(ctx, sess, testkeys, errch)
|
||||
// ```
|
||||
//
|
||||
// While `run` is running, we loop and check which
|
||||
// channel has become ready.
|
||||
//
|
||||
// If the `errch` channel is ready, it means that `run` is
|
||||
// terminated, so we return to the caller.
|
||||
//
|
||||
// Instead, if `ticker.C` is ready, we emit a progress
|
||||
// update using the `callbacks`.
|
||||
//
|
||||
// ```Go
|
||||
for {
|
||||
select {
|
||||
case err := <-errch:
|
||||
callbacks.OnProgress(1.0, "torsf experiment is finished")
|
||||
return err
|
||||
case <-ticker.C:
|
||||
progress := time.Since(start).Seconds() / maxRuntime.Seconds()
|
||||
callbacks.OnProgress(progress, "torsf experiment is running")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ```
|
||||
//
|
||||
// ### The run function
|
||||
//
|
||||
// We will now implement the `run` function. For now, this function
|
||||
// will not do any real work, but it will just pretend to do work.
|
||||
//
|
||||
// Note how we sleep for some time, set the `BootstrapTime` field
|
||||
// of the `TestKeys`, and then return using `errch`.
|
||||
//
|
||||
// ```Go
|
||||
func (m *Measurer) run(ctx context.Context,
|
||||
sess model.ExperimentSession, testkeys *TestKeys, errch chan<- error) {
|
||||
fakeBootstrapTime := 10 * time.Second
|
||||
time.Sleep(fakeBootstrapTime)
|
||||
testkeys.BootstrapTime = fakeBootstrapTime.Seconds()
|
||||
errch <- nil
|
||||
}
|
||||
|
||||
// ```
|
||||
//
|
||||
// ## Running the code
|
||||
//
|
||||
// It's now time to run the new code we've written:
|
||||
//
|
||||
// ```
|
||||
// $ go run ./experiment/torsf/chapter03 | jq
|
||||
// 2021/06/21 21:21:18 info [ 0.1%] torsf experiment is running
|
||||
// 2021/06/21 21:21:19 info [ 0.2%] torsf experiment is running
|
||||
// [...]
|
||||
// 2021/06/21 21:21:28 info [ 3.3%] torsf experiment is running
|
||||
// 2021/06/21 21:21:28 info [100.0%] torsf experiment is finished
|
||||
// {
|
||||
// "data_format_version": "",
|
||||
// "input": null,
|
||||
// "measurement_start_time": "",
|
||||
// "probe_asn": "",
|
||||
// "probe_cc": "",
|
||||
// "probe_network_name": "",
|
||||
// "report_id": "",
|
||||
// "resolver_asn": "",
|
||||
// "resolver_ip": "",
|
||||
// "resolver_network_name": "",
|
||||
// "software_name": "",
|
||||
// "software_version": "",
|
||||
// "test_keys": {
|
||||
// "bootstrap_time": 10,
|
||||
// "failure": null
|
||||
// },
|
||||
// "test_name": "",
|
||||
// "test_runtime": 0,
|
||||
// "test_start_time": "",
|
||||
// "test_version": ""
|
||||
// }
|
||||
// ```
|
||||
//
|
||||
// You see that now we're filling the bootstrap time and we're
|
||||
// also printing progress using `callbacks`.
|
||||
//
|
||||
// In the next chapter, we'll replace the stub `run` implementation
|
||||
// with a real implementation using Snowflake.
|
||||
//
|
||||
// -=-=- StopHere -=-=-
|
||||
|
||||
// SummaryKeys contains summary keys for this experiment.
|
||||
type SummaryKeys struct {
|
||||
IsAnomaly bool `json:"-"`
|
||||
}
|
||||
|
||||
// GetSummaryKeys implements model.ExperimentMeasurer.GetSummaryKeys.
|
||||
func (m *Measurer) GetSummaryKeys(measurement *model.Measurement) (interface{}, error) {
|
||||
return &SummaryKeys{IsAnomaly: false}, nil
|
||||
}
|
||||
Reference in New Issue
Block a user