@@ -33,6 +33,7 @@ import (
33
33
"github.com/coder/coder/v2/codersdk"
34
34
"github.com/coder/coder/v2/codersdk/workspacesdk"
35
35
"github.com/coder/coder/v2/scaletest/agentconn"
36
+ "github.com/coder/coder/v2/scaletest/autostart"
36
37
"github.com/coder/coder/v2/scaletest/createusers"
37
38
"github.com/coder/coder/v2/scaletest/createworkspaces"
38
39
"github.com/coder/coder/v2/scaletest/dashboard"
@@ -60,6 +61,7 @@ func (r *RootCmd) scaletestCmd() *serpent.Command {
60
61
r .scaletestCreateWorkspaces (),
61
62
r .scaletestWorkspaceUpdates (),
62
63
r .scaletestWorkspaceTraffic (),
64
+ r .scaletestAutostart (),
63
65
},
64
66
}
65
67
@@ -1682,6 +1684,239 @@ func (r *RootCmd) scaletestDashboard() *serpent.Command {
1682
1684
return cmd
1683
1685
}
1684
1686
1687
+ const (
1688
+ autostartTestName = "autostart"
1689
+ )
1690
+
1691
+ func (r * RootCmd )scaletestAutostart ()* serpent.Command {
1692
+ var (
1693
+ workspaceCount int64
1694
+ workspaceJobTimeout time.Duration
1695
+ autostartDelay time.Duration
1696
+ autostartTimeout time.Duration
1697
+ template string
1698
+ noCleanup bool
1699
+
1700
+ parameterFlags workspaceParameterFlags
1701
+ tracingFlags = & scaletestTracingFlags {}
1702
+ timeoutStrategy = & timeoutFlags {}
1703
+ cleanupStrategy = newScaletestCleanupStrategy ()
1704
+ output = & scaletestOutputFlags {}
1705
+ prometheusFlags = & scaletestPrometheusFlags {}
1706
+ )
1707
+
1708
+ cmd := & serpent.Command {
1709
+ Use :"autostart" ,
1710
+ Short :"Replicate a thundering herd of autostarting workspaces" ,
1711
+ Handler :func (inv * serpent.Invocation )error {
1712
+ ctx := inv .Context ()
1713
+ client ,err := r .InitClient (inv )
1714
+ if err != nil {
1715
+ return err
1716
+ }
1717
+
1718
+ notifyCtx ,stop := signal .NotifyContext (ctx ,StopSignals ... )// Checked later.
1719
+ defer stop ()
1720
+ ctx = notifyCtx
1721
+
1722
+ me ,err := requireAdmin (ctx ,client )
1723
+ if err != nil {
1724
+ return err
1725
+ }
1726
+
1727
+ client .HTTPClient = & http.Client {
1728
+ Transport :& codersdk.HeaderTransport {
1729
+ Transport :http .DefaultTransport ,
1730
+ Header :map [string ][]string {
1731
+ codersdk .BypassRatelimitHeader : {"true" },
1732
+ },
1733
+ },
1734
+ }
1735
+
1736
+ if workspaceCount <= 0 {
1737
+ return xerrors .Errorf ("--workspace-count must be greater than zero" )
1738
+ }
1739
+
1740
+ outputs ,err := output .parse ()
1741
+ if err != nil {
1742
+ return xerrors .Errorf ("could not parse --output flags" )
1743
+ }
1744
+
1745
+ tpl ,err := parseTemplate (ctx ,client ,me .OrganizationIDs ,template )
1746
+ if err != nil {
1747
+ return xerrors .Errorf ("parse template: %w" ,err )
1748
+ }
1749
+
1750
+ cliRichParameters ,err := asWorkspaceBuildParameters (parameterFlags .richParameters )
1751
+ if err != nil {
1752
+ return xerrors .Errorf ("can't parse given parameter values: %w" ,err )
1753
+ }
1754
+
1755
+ richParameters ,err := prepWorkspaceBuild (inv ,client ,prepWorkspaceBuildArgs {
1756
+ Action :WorkspaceCreate ,
1757
+ TemplateVersionID :tpl .ActiveVersionID ,
1758
+
1759
+ RichParameterFile :parameterFlags .richParameterFile ,
1760
+ RichParameters :cliRichParameters ,
1761
+ })
1762
+ if err != nil {
1763
+ return xerrors .Errorf ("prepare build: %w" ,err )
1764
+ }
1765
+
1766
+ tracerProvider ,closeTracing ,tracingEnabled ,err := tracingFlags .provider (ctx )
1767
+ if err != nil {
1768
+ return xerrors .Errorf ("create tracer provider: %w" ,err )
1769
+ }
1770
+ tracer := tracerProvider .Tracer (scaletestTracerName )
1771
+
1772
+ reg := prometheus .NewRegistry ()
1773
+ metrics := autostart .NewMetrics (reg )
1774
+
1775
+ setupBarrier := new (sync.WaitGroup )
1776
+ setupBarrier .Add (int (workspaceCount ))
1777
+
1778
+ th := harness .NewTestHarness (timeoutStrategy .wrapStrategy (harness.ConcurrentExecutionStrategy {}),cleanupStrategy .toStrategy ())
1779
+ for i := range workspaceCount {
1780
+ id := strconv .Itoa (int (i ))
1781
+ config := autostart.Config {
1782
+ User : createusers.Config {
1783
+ OrganizationID :me .OrganizationIDs [0 ],
1784
+ },
1785
+ Workspace : workspacebuild.Config {
1786
+ OrganizationID :me .OrganizationIDs [0 ],
1787
+ Request : codersdk.CreateWorkspaceRequest {
1788
+ TemplateID :tpl .ID ,
1789
+ RichParameterValues :richParameters ,
1790
+ },
1791
+ },
1792
+ WorkspaceJobTimeout :workspaceJobTimeout ,
1793
+ AutostartDelay :autostartDelay ,
1794
+ AutostartTimeout :autostartTimeout ,
1795
+ Metrics :metrics ,
1796
+ SetupBarrier :setupBarrier ,
1797
+ }
1798
+ if err := config .Validate ();err != nil {
1799
+ return xerrors .Errorf ("validate config: %w" ,err )
1800
+ }
1801
+ var runner harness.Runnable = autostart .NewRunner (client ,config )
1802
+ if tracingEnabled {
1803
+ runner = & runnableTraceWrapper {
1804
+ tracer :tracer ,
1805
+ spanName :fmt .Sprintf ("%s/%s" ,autostartTestName ,id ),
1806
+ runner :runner ,
1807
+ }
1808
+ }
1809
+ th .AddRun (autostartTestName ,id ,runner )
1810
+ }
1811
+
1812
+ logger := inv .Logger
1813
+ prometheusSrvClose := ServeHandler (ctx ,logger ,promhttp .HandlerFor (reg , promhttp.HandlerOpts {}),prometheusFlags .Address ,"prometheus" )
1814
+ defer prometheusSrvClose ()
1815
+
1816
+ defer func () {
1817
+ _ ,_ = fmt .Fprintln (inv .Stderr ,"\n Uploading traces..." )
1818
+ if err := closeTracing (ctx );err != nil {
1819
+ _ ,_ = fmt .Fprintf (inv .Stderr ,"\n Error uploading traces: %+v\n " ,err )
1820
+ }
1821
+ // Wait for prometheus metrics to be scraped
1822
+ _ ,_ = fmt .Fprintf (inv .Stderr ,"Waiting %s for prometheus metrics to be scraped\n " ,prometheusFlags .Wait )
1823
+ <- time .After (prometheusFlags .Wait )
1824
+ }()
1825
+
1826
+ _ ,_ = fmt .Fprintln (inv .Stderr ,"Running autostart load test..." )
1827
+ testCtx ,testCancel := timeoutStrategy .toContext (ctx )
1828
+ defer testCancel ()
1829
+ err = th .Run (testCtx )
1830
+ if err != nil {
1831
+ return xerrors .Errorf ("run test harness (harness failure, not a test failure): %w" ,err )
1832
+ }
1833
+
1834
+ // If the command was interrupted, skip stats.
1835
+ if notifyCtx .Err ()!= nil {
1836
+ return notifyCtx .Err ()
1837
+ }
1838
+
1839
+ res := th .Results ()
1840
+ for _ ,o := range outputs {
1841
+ err = o .write (res ,inv .Stdout )
1842
+ if err != nil {
1843
+ return xerrors .Errorf ("write output %q to %q: %w" ,o .format ,o .path ,err )
1844
+ }
1845
+ }
1846
+
1847
+ if ! noCleanup {
1848
+ _ ,_ = fmt .Fprintln (inv .Stderr ,"\n Cleaning up..." )
1849
+ cleanupCtx ,cleanupCancel := cleanupStrategy .toContext (ctx )
1850
+ defer cleanupCancel ()
1851
+ err = th .Cleanup (cleanupCtx )
1852
+ if err != nil {
1853
+ return xerrors .Errorf ("cleanup tests: %w" ,err )
1854
+ }
1855
+ }
1856
+
1857
+ if res .TotalFail > 0 {
1858
+ return xerrors .New ("load test failed, see above for more details" )
1859
+ }
1860
+
1861
+ return nil
1862
+ },
1863
+ }
1864
+
1865
+ cmd .Options = serpent.OptionSet {
1866
+ {
1867
+ Flag :"workspace-count" ,
1868
+ FlagShorthand :"c" ,
1869
+ Env :"CODER_SCALETEST_WORKSPACE_COUNT" ,
1870
+ Description :"Required: Total number of workspaces to create." ,
1871
+ Value :serpent .Int64Of (& workspaceCount ),
1872
+ Required :true ,
1873
+ },
1874
+ {
1875
+ Flag :"workspace-job-timeout" ,
1876
+ Env :"CODER_SCALETEST_WORKSPACE_JOB_TIMEOUT" ,
1877
+ Default :"5m" ,
1878
+ Description :"Timeout for workspace jobs (e.g. build, start)." ,
1879
+ Value :serpent .DurationOf (& workspaceJobTimeout ),
1880
+ },
1881
+ {
1882
+ Flag :"autostart-delay" ,
1883
+ Env :"CODER_SCALETEST_AUTOSTART_DELAY" ,
1884
+ Default :"2m" ,
1885
+ Description :"How long after all the workspaces have been stopped to schedule them to be started again." ,
1886
+ Value :serpent .DurationOf (& autostartDelay ),
1887
+ },
1888
+ {
1889
+ Flag :"autostart-timeout" ,
1890
+ Env :"CODER_SCALETEST_AUTOSTART_TIMEOUT" ,
1891
+ Default :"5m" ,
1892
+ Description :"Timeout for the autostart build to be initiated after the scheduled start time." ,
1893
+ Value :serpent .DurationOf (& autostartTimeout ),
1894
+ },
1895
+ {
1896
+ Flag :"template" ,
1897
+ FlagShorthand :"t" ,
1898
+ Env :"CODER_SCALETEST_TEMPLATE" ,
1899
+ Description :"Required: Name or ID of the template to use for workspaces." ,
1900
+ Value :serpent .StringOf (& template ),
1901
+ Required :true ,
1902
+ },
1903
+ {
1904
+ Flag :"no-cleanup" ,
1905
+ Env :"CODER_SCALETEST_NO_CLEANUP" ,
1906
+ Description :"Do not clean up resources after the test completes." ,
1907
+ Value :serpent .BoolOf (& noCleanup ),
1908
+ },
1909
+ }
1910
+
1911
+ cmd .Options = append (cmd .Options ,parameterFlags .cliParameters ()... )
1912
+ tracingFlags .attach (& cmd .Options )
1913
+ timeoutStrategy .attach (& cmd .Options )
1914
+ cleanupStrategy .attach (& cmd .Options )
1915
+ output .attach (& cmd .Options )
1916
+ prometheusFlags .attach (& cmd .Options )
1917
+ return cmd
1918
+ }
1919
+
1685
1920
type runnableTraceWrapper struct {
1686
1921
tracer trace.Tracer
1687
1922
spanName string