Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RSDK-8701 Show configuring state through MachineStatus endpoint #4359

Merged
2 changes: 0 additions & 2 deletions robot/impl/local_robot.go
Original file line number Diff line number Diff line change
Expand Up @@ -1422,9 +1422,7 @@ func (r *localRobot) Shutdown(ctx context.Context) error {
func (r *localRobot) MachineStatus(ctx context.Context) (robot.MachineStatus, error) {
var result robot.MachineStatus

r.manager.resourceGraphLock.Lock()
result.Resources = append(result.Resources, r.manager.resources.Status()...)
r.manager.resourceGraphLock.Unlock()

r.configRevisionMu.RLock()
result.Config = r.configRevision
Expand Down
259 changes: 159 additions & 100 deletions robot/impl/local_robot_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ import (
"crypto/x509"
"errors"
"fmt"
"log"
"math"
"os"
"path"
"path/filepath"
"strings"
"sync"
"testing"
"time"

Expand Down Expand Up @@ -3329,8 +3331,23 @@ type mockResource struct {
}

type mockConfig struct {
Value int `json:"value"`
Fail bool `json:"fail"`
Value int `json:"value"`
Fail bool `json:"fail"`
Sleep string `json:"sleep"`
}

//nolint:unparam // the resource name is currently always "m" but this could easily change
func newMockConfig(name string, val int, fail bool, sleep string) resource.Config {
return resource.Config{
Name: name,
Model: mockModel,
API: mockAPI,
// We need to specify both `Attributes` and `ConvertedAttributes`.
// The former triggers a reconfiguration and the former is actually
// used to reconfigure the component.
Attributes: rutils.AttributeMap{"value": val, "fail": fail, "sleep": sleep},
ConvertedAttributes: &mockConfig{Value: val, Fail: fail, Sleep: sleep},
}
}

var errMockValidation = errors.New("whoops")
Expand Down Expand Up @@ -3368,10 +3385,67 @@ func (m *mockResource) Reconfigure(
if err != nil {
return err
}
if mConf.Sleep != "" {
if d, err := time.ParseDuration(mConf.Sleep); err == nil {
log.Printf("sleeping for %s\n", d)
time.Sleep(d)
}
}
m.value = mConf.Value
return nil
}

// getExpectedDefaultStatuses returns a slice of default [resource.Status] with a given
// revision set for motion and sensor services.
func getExpectedDefaultStatuses(revision string) []resource.Status {
return []resource.Status{
{
Name: resource.Name{
API: resource.APINamespaceRDKInternal.WithServiceType("framesystem"),
Name: "builtin",
},
State: resource.NodeStateReady,
},
{
Name: resource.Name{
API: resource.APINamespaceRDKInternal.WithServiceType("cloud_connection"),
Name: "builtin",
},
State: resource.NodeStateReady,
},
{
Name: resource.Name{
API: resource.APINamespaceRDKInternal.WithServiceType("packagemanager"),
Name: "builtin",
},
State: resource.NodeStateReady,
},
{
Name: resource.Name{
API: resource.APINamespaceRDKInternal.WithServiceType("web"),
Name: "builtin",
},
State: resource.NodeStateReady,
},
{
Name: resource.Name{
API: resource.APINamespaceRDK.WithServiceType("motion"),
Name: "builtin",
},
State: resource.NodeStateReady,
Revision: revision,
},
{
Name: resource.Name{
API: resource.APINamespaceRDK.WithServiceType("sensors"),
Name: "builtin",
},
State: resource.NodeStateReady,
Revision: revision,
},
}
}

func TestMachineStatus(t *testing.T) {
logger := logging.NewTestLogger(t)
ctx := context.Background()
Expand All @@ -3383,93 +3457,32 @@ func TestMachineStatus(t *testing.T) {
)
defer resource.Deregister(mockAPI, mockModel)

rev1 := "rev1"
builtinRev := rev1

getExpectedDefaultStatuses := func() []resource.Status {
return []resource.Status{
{
Name: resource.Name{
API: resource.APINamespaceRDKInternal.WithServiceType("framesystem"),
Name: "builtin",
},
State: resource.NodeStateReady,
},
{
Name: resource.Name{
API: resource.APINamespaceRDKInternal.WithServiceType("cloud_connection"),
Name: "builtin",
},
State: resource.NodeStateReady,
},
{
Name: resource.Name{
API: resource.APINamespaceRDKInternal.WithServiceType("packagemanager"),
Name: "builtin",
},
State: resource.NodeStateReady,
},
{
Name: resource.Name{
API: resource.APINamespaceRDKInternal.WithServiceType("web"),
Name: "builtin",
},
State: resource.NodeStateReady,
},
{
Name: resource.Name{
API: resource.APINamespaceRDK.WithServiceType("motion"),
Name: "builtin",
},
State: resource.NodeStateReady,
Revision: builtinRev,
},
{
Name: resource.Name{
API: resource.APINamespaceRDK.WithServiceType("sensors"),
Name: "builtin",
},
State: resource.NodeStateReady,
Revision: builtinRev,
},
}
}

t.Run("default resources", func(t *testing.T) {
rev1 := "rev1"
lr := setupLocalRobot(t, ctx, &config.Config{Revision: rev1}, logger)

mStatus, err := lr.MachineStatus(ctx)
test.That(t, err, test.ShouldBeNil)
test.That(t, mStatus.Config.Revision, test.ShouldEqual, rev1)

expectedStatuses := getExpectedDefaultStatuses()
expectedStatuses := getExpectedDefaultStatuses(rev1)
rtestutils.VerifySameResourceStatuses(t, mStatus.Resources, expectedStatuses)
})

t.Run("reconfigure", func(t *testing.T) {
lr := setupLocalRobot(t, ctx, &config.Config{Revision: rev1}, logger)

expectedConfigError := fmt.Errorf("resource config validation error: %w", errMockValidation)
t.Run("poll after working and failing reconfigures", func(t *testing.T) {
lr := setupLocalRobot(t, ctx, &config.Config{Revision: "rev1"}, logger)

// Add a fake resource to the robot.
rev2 := "rev2"
builtinRev = rev2
lr.Reconfigure(ctx, &config.Config{
Revision: rev2,
Components: []resource.Config{
{
Name: "m",
Model: mockModel,
API: mockAPI,
ConvertedAttributes: &mockConfig{},
},
},
Revision: rev2,
Components: []resource.Config{newMockConfig("m", 0, false, "")},
})
mStatus, err := lr.MachineStatus(ctx)
test.That(t, err, test.ShouldBeNil)
test.That(t, mStatus.Config.Revision, test.ShouldEqual, rev2)
expectedStatuses := rtestutils.ConcatResourceStatuses(
getExpectedDefaultStatuses(),
getExpectedDefaultStatuses(rev2),
[]resource.Status{
{
Name: mockNamed("m"),
Expand All @@ -3482,27 +3495,17 @@ func TestMachineStatus(t *testing.T) {

// Update resource config to cause reconfiguration to fail.
rev3 := "rev3"
builtinRev = rev3
lr.Reconfigure(ctx, &config.Config{
Revision: rev3,
Components: []resource.Config{
{
Name: "m",
Model: mockModel,
API: mockAPI,
// We need to specify both `Attributes` and `ConvertedAttributes`.
// The former triggers a reconfiguration and the former is actually
// used to reconfigure the component.
Attributes: rutils.AttributeMap{"fail": true},
ConvertedAttributes: &mockConfig{Fail: true},
},
},
Revision: rev3,
Components: []resource.Config{newMockConfig("m", 0, true, "")},
})
mStatus, err = lr.MachineStatus(ctx)
test.That(t, err, test.ShouldBeNil)
test.That(t, mStatus.Config.Revision, test.ShouldEqual, rev3)

expectedConfigError := fmt.Errorf("resource config validation error: %w", errMockValidation)
expectedStatuses = rtestutils.ConcatResourceStatuses(
getExpectedDefaultStatuses(),
getExpectedDefaultStatuses(rev3),
[]resource.Status{
{
Name: mockNamed("m"),
Expand All @@ -3516,27 +3519,15 @@ func TestMachineStatus(t *testing.T) {

// Update resource with a working config.
rev4 := "rev4"
builtinRev = rev4
lr.Reconfigure(ctx, &config.Config{
Revision: rev4,
Components: []resource.Config{
{
Name: "m",
Model: mockModel,
API: mockAPI,
// We need to specify both `Attributes` and `ConvertedAttributes`.
// The former triggers a reconfiguration and the former is actually
// used to reconfigure the component.
Attributes: rutils.AttributeMap{"value": 200},
ConvertedAttributes: &mockConfig{Value: 200},
},
},
Revision: rev4,
Components: []resource.Config{newMockConfig("m", 200, false, "")},
})
mStatus, err = lr.MachineStatus(ctx)
test.That(t, err, test.ShouldBeNil)
test.That(t, mStatus.Config.Revision, test.ShouldEqual, rev4)
expectedStatuses = rtestutils.ConcatResourceStatuses(
getExpectedDefaultStatuses(),
getExpectedDefaultStatuses(rev4),
[]resource.Status{
{
Name: mockNamed("m"),
Expand All @@ -3547,4 +3538,72 @@ func TestMachineStatus(t *testing.T) {
)
rtestutils.VerifySameResourceStatuses(t, mStatus.Resources, expectedStatuses)
})

t.Run("poll during reconfiguration", func(t *testing.T) {
rev1 := "rev1"
lr := setupLocalRobot(t, ctx, &config.Config{
Revision: rev1,
Components: []resource.Config{newMockConfig("m", 200, false, "")},
}, logger)

// update resource with a working config that is slow to reconfigure.
rev2 := "rev2"
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
lr.Reconfigure(ctx, &config.Config{
Revision: rev2,
Components: []resource.Config{newMockConfig("m", 300, false, "1s")},
})
}()
// sleep for a short amount of time to allow the machine to receive a new
// revision. this sleep should be shorter than the resource update duration
// defined above so that updated resource is still in a "configuring" state.
time.Sleep(time.Millisecond * 100)
Comment on lines +3560 to +3563
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

relying on a sleep is a little janky - alternatively we could poll machine status until the revision on the machine is updated and fail after some attempts. any other suggestions are welcome here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can do a WaitForAssertion


// get status while reconfiguring
mStatus, err := lr.MachineStatus(ctx)
test.That(t, err, test.ShouldBeNil)
test.That(t, mStatus.Config.Revision, test.ShouldEqual, rev2)

// the component whose config changed should be the only component in a
// "configuring" state and associated with the original revision.
filterConfiguring := rtestutils.FilterByStatus(t, mStatus.Resources, resource.NodeStateConfiguring)
expectedConfiguring := []resource.Status{
{
Name: mockNamed("m"),
State: resource.NodeStateConfiguring,
Revision: rev1,
},
}
rtestutils.VerifySameResourceStatuses(t, filterConfiguring, expectedConfiguring)

// all other components should be in the "ready" state and associated with the
// new revision.
filterReady := rtestutils.FilterByStatus(t, mStatus.Resources, resource.NodeStateReady)
expectedReady := getExpectedDefaultStatuses(rev2)
rtestutils.VerifySameResourceStatuses(t, filterReady, expectedReady)

wg.Wait()

// get status after reconfigure finishes
mStatus, err = lr.MachineStatus(ctx)
test.That(t, err, test.ShouldBeNil)
test.That(t, mStatus.Config.Revision, test.ShouldEqual, rev2)

// now all components, including the one whose config changed, should all be in
// the "ready" state and associated with the new revision.
expectedStatuses := rtestutils.ConcatResourceStatuses(
getExpectedDefaultStatuses(rev2),
[]resource.Status{
{
Name: mockNamed("m"),
State: resource.NodeStateReady,
Revision: rev2,
},
},
)
rtestutils.VerifySameResourceStatuses(t, mStatus.Resources, expectedStatuses)
})
Comment on lines +3542 to +3608
Copy link
Contributor Author

@maximpertsov maximpertsov Sep 9, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pardon the noisy diff - the test highlighted with this comment is the only new test. the remaining changes are refactors.

this test correctly fails if the locked removed in commit 3556889 is reinstated.

}
14 changes: 14 additions & 0 deletions testutils/resource_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,20 @@ func VerifySameResourceStatuses(tb testing.TB, actual, expected []resource.Statu
test.That(tb, sortedActual, test.ShouldResemble, sortedExpected)
}

// FilterByStatus takes a slice of [resource.Status] and a [resource.NodeState] and
// returns a slice of [resource.Status] that are in the given [resource.NodeState].
func FilterByStatus(tb testing.TB, resourceStatuses []resource.Status, state resource.NodeState) []resource.Status {
tb.Helper()

var result []resource.Status
for _, rs := range resourceStatuses {
if rs.State == state {
result = append(result, rs)
}
}
return result
}

func newSortedResourceStatuses(resourceStatuses []resource.Status) []resource.Status {
sorted := make([]resource.Status, len(resourceStatuses))
copy(sorted, resourceStatuses)
Expand Down
Loading