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:
Simone Basso
2021-06-22 00:12:03 +02:00
committed by GitHub
parent a9990ac9da
commit 520398dd8e
41 changed files with 2113 additions and 26 deletions
@@ -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
}