diff --git a/api/swagger.yaml b/api/swagger.yaml index 2cda377..c08cbee 100644 --- a/api/swagger.yaml +++ b/api/swagger.yaml @@ -120,6 +120,51 @@ paths: "500": $ref: "#/components/responses/InternalServerError" + /statistics/execution-environments: + get: + summary: Retrieve the statistics about the execution environments of Poseidon + description: Return Return the current availability and usage of runners. + tags: + - miscellaneous + responses: + "200": + description: Success. Returns all execution environments + content: + application/json: + schema: + type: object + additionalProperties: + type: object + properties: + id: + description: The id of the execution environment. + type: integer + prewarmingPoolSize: + description: Number of runners with this configuration to prewarm. + type: integer + example: 50 + idleRunners: + description: Number of runners currently prewarmed. + type: number + example: 45 + usedRunners: + description: Number of runners currently in use. + type: number + example: 20 + example: + 21: + id: 21 + prewarmingPoolSize: 50 + idleRunners: 45 + usedRunners: 20 + 42: + id: 42 + prewarmingPoolSize: 50 + idleRunners: 45 + usedRunners: 20 + "500": + $ref: "#/components/responses/InternalServerError" + /runners: post: summary: Provide a runner @@ -144,7 +189,7 @@ paths: type: integer example: 6 required: - - executionEnvironment + - executionEnvironmentId additionalProperties: false responses: "200": @@ -167,7 +212,9 @@ paths: properties: exposedPort: description: The port inside the container. - type: uint + type: integer + minimum: 0 + maximum: 65535 example: 80 hostAddress: description: The address which can be contacted to reach the mapped port. diff --git a/internal/api/api.go b/internal/api/api.go index f0a8926..9339b28 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -6,6 +6,7 @@ import ( "github.com/openHPI/poseidon/internal/config" "github.com/openHPI/poseidon/internal/environment" "github.com/openHPI/poseidon/internal/runner" + "github.com/openHPI/poseidon/pkg/dto" "github.com/openHPI/poseidon/pkg/logging" "net/http" ) @@ -18,6 +19,7 @@ const ( VersionPath = "/version" RunnersPath = "/runners" EnvironmentsPath = "/execution-environments" + StatisticsPath = "/statistics" ) // NewRouter returns a *mux.Router which can be @@ -46,10 +48,14 @@ func configureV1Router(router *mux.Router, runnerManager runner.Manager, environ runnerController := &RunnerController{manager: runnerManager} environmentController := &EnvironmentController{manager: environmentManager} - configureRoutes := func(router *mux.Router) { runnerController.ConfigureRoutes(router) environmentController.ConfigureRoutes(router) + + // May add a statistics controller if another route joins + statisticsRouter := router.PathPrefix(StatisticsPath).Subrouter() + statisticsRouter. + HandleFunc(EnvironmentsPath, StatisticsExecutionEnvironments(environmentManager)).Methods(http.MethodGet) } if auth.InitializeAuthentication() { @@ -73,3 +79,16 @@ func Version(writer http.ResponseWriter, _ *http.Request) { writer.WriteHeader(http.StatusNotFound) } } + +// StatisticsExecutionEnvironments handles the route for statistics about execution environments. +// It responds the prewarming pool size and the number of idle runners and used runners. +func StatisticsExecutionEnvironments(manager environment.Manager) http.HandlerFunc { + return func(writer http.ResponseWriter, _ *http.Request) { + result := make(map[string]*dto.StatisticalExecutionEnvironmentData) + environmentsData := manager.Statistics() + for id, data := range environmentsData { + result[id.ToString()] = data + } + sendJSON(writer, result, http.StatusOK) + } +} diff --git a/internal/environment/environment.go b/internal/environment/environment.go index 96fedc1..0e88bd4 100644 --- a/internal/environment/environment.go +++ b/internal/environment/environment.go @@ -254,6 +254,10 @@ func (n *NomadEnvironment) DeleteRunner(id string) { n.idleRunners.Delete(id) } +func (n *NomadEnvironment) IdleRunnerCount() int { + return n.idleRunners.Length() +} + // MarshalJSON implements the json.Marshaler interface. // This converts the NomadEnvironment into the expected schema for dto.ExecutionEnvironmentData. func (n *NomadEnvironment) MarshalJSON() (res []byte, err error) { diff --git a/internal/environment/manager.go b/internal/environment/manager.go index 7281a4e..017bb9f 100644 --- a/internal/environment/manager.go +++ b/internal/environment/manager.go @@ -44,6 +44,9 @@ type Manager interface { // Delete removes the specified execution environment. // Iff the specified environment could not be found Delete returns false. Delete(id dto.EnvironmentID) (bool, error) + + // Statistics returns statistical data for each execution environment. + Statistics() map[dto.EnvironmentID]*dto.StatisticalExecutionEnvironmentData } type NomadEnvironmentManager struct { @@ -156,6 +159,10 @@ func (m *NomadEnvironmentManager) Delete(id dto.EnvironmentID) (bool, error) { return true, nil } +func (m *NomadEnvironmentManager) Statistics() map[dto.EnvironmentID]*dto.StatisticalExecutionEnvironmentData { + return m.runnerManager.EnvironmentStatistics() +} + func (m *NomadEnvironmentManager) Load() error { templateJobs, err := m.api.LoadEnvironmentJobs() if err != nil { diff --git a/internal/environment/manager_mock.go b/internal/environment/manager_mock.go index 32b0bdc..6f4e0c4 100644 --- a/internal/environment/manager_mock.go +++ b/internal/environment/manager_mock.go @@ -115,3 +115,19 @@ func (_m *ManagerMock) Load() error { return r0 } + +// Statistics provides a mock function with given fields: +func (_m *ManagerMock) Statistics() map[dto.EnvironmentID]*dto.StatisticalExecutionEnvironmentData { + ret := _m.Called() + + var r0 map[dto.EnvironmentID]*dto.StatisticalExecutionEnvironmentData + if rf, ok := ret.Get(0).(func() map[dto.EnvironmentID]*dto.StatisticalExecutionEnvironmentData); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(map[dto.EnvironmentID]*dto.StatisticalExecutionEnvironmentData) + } + } + + return r0 +} diff --git a/internal/runner/execution_environment_mock.go b/internal/runner/execution_environment_mock.go index 69276c4..21c7f35 100644 --- a/internal/runner/execution_environment_mock.go +++ b/internal/runner/execution_environment_mock.go @@ -66,6 +66,20 @@ func (_m *ExecutionEnvironmentMock) ID() dto.EnvironmentID { return r0 } +// IdleRunnerCount provides a mock function with given fields: +func (_m *ExecutionEnvironmentMock) IdleRunnerCount() int { + ret := _m.Called() + + var r0 int + if rf, ok := ret.Get(0).(func() int); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(int) + } + + return r0 +} + // Image provides a mock function with given fields: func (_m *ExecutionEnvironmentMock) Image() string { ret := _m.Called() diff --git a/internal/runner/manager.go b/internal/runner/manager.go index ce18299..c558769 100644 --- a/internal/runner/manager.go +++ b/internal/runner/manager.go @@ -62,6 +62,8 @@ type ExecutionEnvironment interface { AddRunner(r Runner) // DeleteRunner removes an idle runner from the environment. DeleteRunner(id string) + // IdleRunnerCount returns the number of idle runners of the environment. + IdleRunnerCount() int } // Manager keeps track of the used and unused runners of all execution environments in order to provide unused @@ -82,6 +84,9 @@ type Manager interface { // It does nothing if the specified environment can not be found. DeleteEnvironment(id dto.EnvironmentID) + // EnvironmentStatistics returns statistical data for each execution environment. + EnvironmentStatistics() map[dto.EnvironmentID]*dto.StatisticalExecutionEnvironmentData + // Claim returns a new runner. The runner is deleted after duration seconds if duration is not 0. // It makes sure that the runner is not in use yet and returns an error if no runner could be provided. Claim(id dto.EnvironmentID, duration int) (Runner, error) @@ -136,6 +141,27 @@ func (m *NomadRunnerManager) DeleteEnvironment(id dto.EnvironmentID) { m.environments.Delete(id) } +func (m *NomadRunnerManager) EnvironmentStatistics() map[dto.EnvironmentID]*dto.StatisticalExecutionEnvironmentData { + environments := make(map[dto.EnvironmentID]*dto.StatisticalExecutionEnvironmentData) + for _, e := range m.environments.List() { + environments[e.ID()] = &dto.StatisticalExecutionEnvironmentData{ + ID: int(e.ID()), + PrewarmingPoolSize: e.PrewarmingPoolSize(), + IdleRunners: uint(e.IdleRunnerCount()), + UsedRunners: 0, + } + } + + for _, r := range m.usedRunners.List() { + id, err := nomad.EnvironmentIDFromRunnerID(r.ID()) + if err != nil { + log.WithError(err).Error("Stored runners must have correct IDs") + } + environments[id].UsedRunners++ + } + return environments +} + func (m *NomadRunnerManager) Claim(environmentID dto.EnvironmentID, duration int) (Runner, error) { environment, ok := m.environments.Get(environmentID) if !ok { diff --git a/internal/runner/manager_mock.go b/internal/runner/manager_mock.go index 1edd092..8a93cd4 100644 --- a/internal/runner/manager_mock.go +++ b/internal/runner/manager_mock.go @@ -40,6 +40,22 @@ func (_m *ManagerMock) DeleteEnvironment(id dto.EnvironmentID) { _m.Called(id) } +// EnvironmentStatistics provides a mock function with given fields: +func (_m *ManagerMock) EnvironmentStatistics() map[dto.EnvironmentID]*dto.StatisticalExecutionEnvironmentData { + ret := _m.Called() + + var r0 map[dto.EnvironmentID]*dto.StatisticalExecutionEnvironmentData + if rf, ok := ret.Get(0).(func() map[dto.EnvironmentID]*dto.StatisticalExecutionEnvironmentData); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(map[dto.EnvironmentID]*dto.StatisticalExecutionEnvironmentData) + } + } + + return r0 +} + // Get provides a mock function with given fields: runnerID func (_m *ManagerMock) Get(runnerID string) (Runner, error) { ret := _m.Called(runnerID) diff --git a/internal/runner/storage.go b/internal/runner/storage.go index 74452b4..795d9d9 100644 --- a/internal/runner/storage.go +++ b/internal/runner/storage.go @@ -6,6 +6,9 @@ import ( // Storage is an interface for storing runners. type Storage interface { + // List returns all runners from the storage. + List() []Runner + // Add adds a runner to the storage. // It overwrites the old runner if one with the same id was already stored. Add(Runner) @@ -41,6 +44,15 @@ func NewLocalRunnerStorage() *localRunnerStorage { } } +func (s *localRunnerStorage) List() (r []Runner) { + s.RLock() + defer s.RUnlock() + for _, value := range s.runners { + r = append(r, value) + } + return r +} + func (s *localRunnerStorage) Add(r Runner) { s.Lock() defer s.Unlock() diff --git a/pkg/dto/dto.go b/pkg/dto/dto.go index 889e5d3..35d85ba 100644 --- a/pkg/dto/dto.go +++ b/pkg/dto/dto.go @@ -53,6 +53,15 @@ type ExecutionEnvironmentData struct { ID int `json:"id"` } +// StatisticalExecutionEnvironmentData is the expected json structure of the response body +// for routes returning statistics about execution environments. +type StatisticalExecutionEnvironmentData struct { + ID int `json:"id"` + PrewarmingPoolSize uint `json:"prewarmingPoolSize"` + IdleRunners uint `json:"idleRunners"` + UsedRunners uint `json:"usedRunners"` +} + // ExecutionEnvironmentRequest is the expected json structure of the request body // for the create execution environment function. type ExecutionEnvironmentRequest struct {