Add statistics route for execution environments

* Add statistics route for execution environments

* Add maximum to port api definition

Co-authored-by: Sebastian Serth <MrSerth@users.noreply.github.com>
This commit is contained in:
Maximilian Paß
2021-12-08 12:08:22 +01:00
committed by GitHub
parent c23a534f86
commit 1de559cebc
10 changed files with 173 additions and 3 deletions

View File

@ -120,6 +120,51 @@ paths:
"500": "500":
$ref: "#/components/responses/InternalServerError" $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: /runners:
post: post:
summary: Provide a runner summary: Provide a runner
@ -144,7 +189,7 @@ paths:
type: integer type: integer
example: 6 example: 6
required: required:
- executionEnvironment - executionEnvironmentId
additionalProperties: false additionalProperties: false
responses: responses:
"200": "200":
@ -167,7 +212,9 @@ paths:
properties: properties:
exposedPort: exposedPort:
description: The port inside the container. description: The port inside the container.
type: uint type: integer
minimum: 0
maximum: 65535
example: 80 example: 80
hostAddress: hostAddress:
description: The address which can be contacted to reach the mapped port. description: The address which can be contacted to reach the mapped port.

View File

@ -6,6 +6,7 @@ import (
"github.com/openHPI/poseidon/internal/config" "github.com/openHPI/poseidon/internal/config"
"github.com/openHPI/poseidon/internal/environment" "github.com/openHPI/poseidon/internal/environment"
"github.com/openHPI/poseidon/internal/runner" "github.com/openHPI/poseidon/internal/runner"
"github.com/openHPI/poseidon/pkg/dto"
"github.com/openHPI/poseidon/pkg/logging" "github.com/openHPI/poseidon/pkg/logging"
"net/http" "net/http"
) )
@ -18,6 +19,7 @@ const (
VersionPath = "/version" VersionPath = "/version"
RunnersPath = "/runners" RunnersPath = "/runners"
EnvironmentsPath = "/execution-environments" EnvironmentsPath = "/execution-environments"
StatisticsPath = "/statistics"
) )
// NewRouter returns a *mux.Router which can be // 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} runnerController := &RunnerController{manager: runnerManager}
environmentController := &EnvironmentController{manager: environmentManager} environmentController := &EnvironmentController{manager: environmentManager}
configureRoutes := func(router *mux.Router) { configureRoutes := func(router *mux.Router) {
runnerController.ConfigureRoutes(router) runnerController.ConfigureRoutes(router)
environmentController.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() { if auth.InitializeAuthentication() {
@ -73,3 +79,16 @@ func Version(writer http.ResponseWriter, _ *http.Request) {
writer.WriteHeader(http.StatusNotFound) 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)
}
}

View File

@ -254,6 +254,10 @@ func (n *NomadEnvironment) DeleteRunner(id string) {
n.idleRunners.Delete(id) n.idleRunners.Delete(id)
} }
func (n *NomadEnvironment) IdleRunnerCount() int {
return n.idleRunners.Length()
}
// MarshalJSON implements the json.Marshaler interface. // MarshalJSON implements the json.Marshaler interface.
// This converts the NomadEnvironment into the expected schema for dto.ExecutionEnvironmentData. // This converts the NomadEnvironment into the expected schema for dto.ExecutionEnvironmentData.
func (n *NomadEnvironment) MarshalJSON() (res []byte, err error) { func (n *NomadEnvironment) MarshalJSON() (res []byte, err error) {

View File

@ -44,6 +44,9 @@ type Manager interface {
// Delete removes the specified execution environment. // Delete removes the specified execution environment.
// Iff the specified environment could not be found Delete returns false. // Iff the specified environment could not be found Delete returns false.
Delete(id dto.EnvironmentID) (bool, error) Delete(id dto.EnvironmentID) (bool, error)
// Statistics returns statistical data for each execution environment.
Statistics() map[dto.EnvironmentID]*dto.StatisticalExecutionEnvironmentData
} }
type NomadEnvironmentManager struct { type NomadEnvironmentManager struct {
@ -156,6 +159,10 @@ func (m *NomadEnvironmentManager) Delete(id dto.EnvironmentID) (bool, error) {
return true, nil return true, nil
} }
func (m *NomadEnvironmentManager) Statistics() map[dto.EnvironmentID]*dto.StatisticalExecutionEnvironmentData {
return m.runnerManager.EnvironmentStatistics()
}
func (m *NomadEnvironmentManager) Load() error { func (m *NomadEnvironmentManager) Load() error {
templateJobs, err := m.api.LoadEnvironmentJobs() templateJobs, err := m.api.LoadEnvironmentJobs()
if err != nil { if err != nil {

View File

@ -115,3 +115,19 @@ func (_m *ManagerMock) Load() error {
return r0 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
}

View File

@ -66,6 +66,20 @@ func (_m *ExecutionEnvironmentMock) ID() dto.EnvironmentID {
return r0 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: // Image provides a mock function with given fields:
func (_m *ExecutionEnvironmentMock) Image() string { func (_m *ExecutionEnvironmentMock) Image() string {
ret := _m.Called() ret := _m.Called()

View File

@ -62,6 +62,8 @@ type ExecutionEnvironment interface {
AddRunner(r Runner) AddRunner(r Runner)
// DeleteRunner removes an idle runner from the environment. // DeleteRunner removes an idle runner from the environment.
DeleteRunner(id string) 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 // 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. // It does nothing if the specified environment can not be found.
DeleteEnvironment(id dto.EnvironmentID) 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. // 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. // 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) Claim(id dto.EnvironmentID, duration int) (Runner, error)
@ -136,6 +141,27 @@ func (m *NomadRunnerManager) DeleteEnvironment(id dto.EnvironmentID) {
m.environments.Delete(id) 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) { func (m *NomadRunnerManager) Claim(environmentID dto.EnvironmentID, duration int) (Runner, error) {
environment, ok := m.environments.Get(environmentID) environment, ok := m.environments.Get(environmentID)
if !ok { if !ok {

View File

@ -40,6 +40,22 @@ func (_m *ManagerMock) DeleteEnvironment(id dto.EnvironmentID) {
_m.Called(id) _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 // Get provides a mock function with given fields: runnerID
func (_m *ManagerMock) Get(runnerID string) (Runner, error) { func (_m *ManagerMock) Get(runnerID string) (Runner, error) {
ret := _m.Called(runnerID) ret := _m.Called(runnerID)

View File

@ -6,6 +6,9 @@ import (
// Storage is an interface for storing runners. // Storage is an interface for storing runners.
type Storage interface { type Storage interface {
// List returns all runners from the storage.
List() []Runner
// Add adds a runner to the storage. // Add adds a runner to the storage.
// It overwrites the old runner if one with the same id was already stored. // It overwrites the old runner if one with the same id was already stored.
Add(Runner) 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) { func (s *localRunnerStorage) Add(r Runner) {
s.Lock() s.Lock()
defer s.Unlock() defer s.Unlock()

View File

@ -53,6 +53,15 @@ type ExecutionEnvironmentData struct {
ID int `json:"id"` 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 // ExecutionEnvironmentRequest is the expected json structure of the request body
// for the create execution environment function. // for the create execution environment function.
type ExecutionEnvironmentRequest struct { type ExecutionEnvironmentRequest struct {