go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/deploy/service/model/roundtrip_test.go (about)

     1  // Copyright 2022 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package model
    16  
    17  import (
    18  	"context"
    19  	"testing"
    20  	"time"
    21  
    22  	"go.chromium.org/luci/common/clock/testclock"
    23  	"go.chromium.org/luci/gae/impl/memory"
    24  	"go.chromium.org/luci/gae/service/datastore"
    25  
    26  	"go.chromium.org/luci/deploy/api/modelpb"
    27  	"go.chromium.org/luci/deploy/api/rpcpb"
    28  
    29  	. "github.com/smartystreets/goconvey/convey"
    30  )
    31  
    32  func TestRoundTrip(t *testing.T) {
    33  	t.Parallel()
    34  
    35  	Convey("With datastore", t, func() {
    36  		now := testclock.TestRecentTimeUTC.Round(time.Millisecond)
    37  		ctx, _ := testclock.UseTime(context.Background(), now)
    38  		ctx = memory.Use(ctx)
    39  
    40  		store := func(a *modelpb.Asset) {
    41  			So(datastore.Put(ctx, &Asset{
    42  				ID:    a.Id,
    43  				Asset: a,
    44  			}), ShouldBeNil)
    45  		}
    46  
    47  		fetch := func(assetID string) *modelpb.Asset {
    48  			ent := &Asset{ID: assetID}
    49  			So(datastore.Get(ctx, ent), ShouldBeNil)
    50  			return ent.Asset
    51  		}
    52  
    53  		history := func(assetID string, historyID int64) *modelpb.AssetHistory {
    54  			ent := &AssetHistory{
    55  				ID:     historyID,
    56  				Parent: datastore.NewKey(ctx, "Asset", assetID, 0, nil),
    57  			}
    58  			if datastore.Get(ctx, ent) == datastore.ErrNoSuchEntity {
    59  				return nil
    60  			}
    61  			return ent.Entry
    62  		}
    63  
    64  		intendedState := func(payload string, traffic int32) *modelpb.AssetState {
    65  			return &modelpb.AssetState{
    66  				State: &modelpb.AssetState_Appengine{
    67  					Appengine: mockedIntendedState(payload, traffic),
    68  				},
    69  			}
    70  		}
    71  
    72  		reportedState := func(payload string, traffic int32) *modelpb.AssetState {
    73  			return &modelpb.AssetState{
    74  				State: &modelpb.AssetState_Appengine{
    75  					Appengine: mockedReportedState(payload, traffic),
    76  				},
    77  			}
    78  		}
    79  
    80  		Convey("Two assets, one up-to-date", func() {
    81  			store(&modelpb.Asset{
    82  				Id:           "apps/app-1",
    83  				AppliedState: intendedState("app-1", 0),
    84  				LastActuateActuation: &modelpb.Actuation{
    85  					Id: "old-actuation",
    86  				},
    87  			})
    88  			store(&modelpb.Asset{
    89  				Id:           "apps/app-2",
    90  				AppliedState: intendedState("app-2", 0),
    91  				LastActuateActuation: &modelpb.Actuation{
    92  					Id: "old-actuation",
    93  				},
    94  			})
    95  
    96  			beginOp, err := NewActuationBeginOp(ctx, []string{"apps/app-1", "apps/app-2"}, &modelpb.Actuation{
    97  				Id: "new-actuation",
    98  			})
    99  			So(err, ShouldBeNil)
   100  
   101  			// SKIP_UPTODATE decision.
   102  			beginOp.MakeDecision(ctx, "apps/app-1", &rpcpb.AssetToActuate{
   103  				Config:        &modelpb.AssetConfig{EnableAutomation: true},
   104  				IntendedState: intendedState("app-1", 0),
   105  				ReportedState: reportedState("app-1", 0),
   106  			})
   107  			// ACTUATE_STALE decision.
   108  			beginOp.MakeDecision(ctx, "apps/app-2", &rpcpb.AssetToActuate{
   109  				Config:        &modelpb.AssetConfig{EnableAutomation: true},
   110  				IntendedState: intendedState("app-2", 500),
   111  				ReportedState: reportedState("app-2", 0),
   112  			})
   113  
   114  			_, err = beginOp.Apply(ctx)
   115  			So(err, ShouldBeNil)
   116  
   117  			asset1 := fetch("apps/app-1")
   118  			asset2 := fetch("apps/app-2")
   119  
   120  			// Both assets are associated with the actuation in EXECUTING state.
   121  			So(asset1.LastActuation.State, ShouldEqual, modelpb.Actuation_EXECUTING)
   122  			So(asset2.LastActuation.State, ShouldEqual, modelpb.Actuation_EXECUTING)
   123  
   124  			// LastActuateActuation changes only for the stale asset.
   125  			So(asset1.LastActuateActuation.Id, ShouldEqual, "old-actuation")
   126  			So(asset2.LastActuateActuation.Id, ShouldEqual, "new-actuation")
   127  			So(asset2.LastActuateActuation.State, ShouldEqual, modelpb.Actuation_EXECUTING)
   128  
   129  			// Finish the executing actuation.
   130  			actuation := &Actuation{ID: "new-actuation"}
   131  			So(datastore.Get(ctx, actuation), ShouldBeNil)
   132  			endOp, err := NewActuationEndOp(ctx, actuation)
   133  			So(err, ShouldBeNil)
   134  			endOp.UpdateActuationStatus(ctx, nil, "")
   135  			endOp.HandleActuatedState(ctx, "apps/app-2", &rpcpb.ActuatedAsset{
   136  				State: reportedState("app-2", 500),
   137  			})
   138  			So(endOp.Apply(ctx), ShouldBeNil)
   139  
   140  			asset1 = fetch("apps/app-1")
   141  			asset2 = fetch("apps/app-2")
   142  
   143  			// Both assets are associated with the actuation in SUCCEEDED state.
   144  			So(asset1.LastActuation.State, ShouldEqual, modelpb.Actuation_SUCCEEDED)
   145  			So(asset2.LastActuation.State, ShouldEqual, modelpb.Actuation_SUCCEEDED)
   146  
   147  			// LastActuateActuation changes only for the stale asset.
   148  			So(asset1.LastActuateActuation.Id, ShouldEqual, "old-actuation")
   149  			So(asset2.LastActuateActuation.Id, ShouldEqual, "new-actuation")
   150  			So(asset2.LastActuateActuation.State, ShouldEqual, modelpb.Actuation_SUCCEEDED)
   151  		})
   152  
   153  		Convey("Crashing actuation", func() {
   154  			store(&modelpb.Asset{
   155  				Id:           "apps/app",
   156  				AppliedState: intendedState("app", 0),
   157  			})
   158  
   159  			beginOp, err := NewActuationBeginOp(ctx, []string{"apps/app"}, &modelpb.Actuation{
   160  				Id: "actuation-0",
   161  			})
   162  			So(err, ShouldBeNil)
   163  
   164  			// ACTUATE_STALE decision.
   165  			beginOp.MakeDecision(ctx, "apps/app", &rpcpb.AssetToActuate{
   166  				Config:        &modelpb.AssetConfig{EnableAutomation: true},
   167  				IntendedState: intendedState("app", 500),
   168  				ReportedState: reportedState("app", 0),
   169  			})
   170  			_, err = beginOp.Apply(ctx)
   171  			So(err, ShouldBeNil)
   172  
   173  			// The asset points to this actuation.
   174  			asset := fetch("apps/app")
   175  			So(asset.LastActuation.Id, ShouldEqual, "actuation-0")
   176  			So(asset.LastActuateActuation.Id, ShouldEqual, "actuation-0")
   177  
   178  			// Another actuation starts before the previous one finishes.
   179  			beginOp, err = NewActuationBeginOp(ctx, []string{"apps/app"}, &modelpb.Actuation{
   180  				Id: "actuation-1",
   181  			})
   182  			So(err, ShouldBeNil)
   183  
   184  			// ACTUATE_STALE decision.
   185  			beginOp.MakeDecision(ctx, "apps/app", &rpcpb.AssetToActuate{
   186  				Config:        &modelpb.AssetConfig{EnableAutomation: true},
   187  				IntendedState: intendedState("app", 500),
   188  				ReportedState: reportedState("app", 0),
   189  			})
   190  			_, err = beginOp.Apply(ctx)
   191  			So(err, ShouldBeNil)
   192  
   193  			// The asset points to the new actuation.
   194  			asset = fetch("apps/app")
   195  			So(asset.LastActuation.Id, ShouldEqual, "actuation-1")
   196  			So(asset.LastActuateActuation.Id, ShouldEqual, "actuation-1")
   197  
   198  			// There's a history log that points to the crashed actuation.
   199  			hist := history("apps/app", 1)
   200  			So(hist.Actuation.Id, ShouldEqual, "actuation-0")
   201  			So(hist.Actuation.State, ShouldEqual, modelpb.Actuation_EXPIRED)
   202  
   203  			// Finish the stale actuation, it should be a noop.
   204  			actuation := &Actuation{ID: "actuation-0"}
   205  			So(datastore.Get(ctx, actuation), ShouldBeNil)
   206  			endOp, err := NewActuationEndOp(ctx, actuation)
   207  			So(err, ShouldBeNil)
   208  			endOp.UpdateActuationStatus(ctx, nil, "")
   209  			endOp.HandleActuatedState(ctx, "apps/app", &rpcpb.ActuatedAsset{
   210  				State: reportedState("app", 777),
   211  			})
   212  			So(endOp.Apply(ctx), ShouldBeNil)
   213  
   214  			// No new history entries.
   215  			So(history("apps/app", 2), ShouldBeNil)
   216  
   217  			// Finish the active actuation.
   218  			actuation = &Actuation{ID: "actuation-1"}
   219  			So(datastore.Get(ctx, actuation), ShouldBeNil)
   220  			endOp, err = NewActuationEndOp(ctx, actuation)
   221  			So(err, ShouldBeNil)
   222  			endOp.UpdateActuationStatus(ctx, nil, "")
   223  			endOp.HandleActuatedState(ctx, "apps/app", &rpcpb.ActuatedAsset{
   224  				State: reportedState("app", 500),
   225  			})
   226  			So(endOp.Apply(ctx), ShouldBeNil)
   227  
   228  			// Have the new history log entry.
   229  			hist = history("apps/app", 2)
   230  			So(hist.Actuation.Id, ShouldEqual, "actuation-1")
   231  			So(hist.Actuation.State, ShouldEqual, modelpb.Actuation_SUCCEEDED)
   232  		})
   233  	})
   234  }