From 242f5efeb982629a318e007667b695477141cbfb Mon Sep 17 00:00:00 2001 From: JeffMboya Date: Wed, 8 Apr 2026 10:30:50 +0300 Subject: [PATCH 1/2] feat(proplet): separate alive history into dedicated endpoint Removes AliveHistory from the proplet response and exposes it via a new GET /proplets/{id}/alive-history endpoint with offset/limit pagination. The single-proplet response retains alive (bool) and gains last_alive_at (the most recent heartbeat timestamp). Adds GetAliveHistory to all storage backends (postgres, sqlite, badger, memory) and wires GetPropletAliveHistory through the full manager stack: service, endpoint, transport, logging/metrics/tracing middleware, mock, and SDK. --- manager/api/endpoint.go | 23 +++++++- manager/api/responses.go | 21 ++++++- manager/api/transport.go | 6 ++ manager/manager.go | 1 + manager/middleware/logging.go | 22 ++++++++ manager/middleware/metrics.go | 9 +++ manager/middleware/tracing.go | 11 ++++ manager/mocks/service.go | 66 ++++++++++++++++++++++ manager/service.go | 14 +++++ pkg/proplet/proplet.go | 51 +++++++++++++++++ pkg/sdk/proplet.go | 24 +++++++- pkg/sdk/sdk.go | 9 +++ pkg/storage/badger/init.go | 1 + pkg/storage/badger/proplets.go | 20 +++++++ pkg/storage/factory.go | 13 +++++ pkg/storage/memory_adapter.go | 20 +++++++ pkg/storage/mocks/proplet_repository.go | 75 +++++++++++++++++++++++++ pkg/storage/postgres/init.go | 1 + pkg/storage/postgres/proplets.go | 20 +++++++ pkg/storage/repository.go | 2 + pkg/storage/sqlite/init.go | 1 + pkg/storage/sqlite/proplets.go | 20 +++++++ 22 files changed, 425 insertions(+), 5 deletions(-) diff --git a/manager/api/endpoint.go b/manager/api/endpoint.go index 3f68a577..871cced1 100644 --- a/manager/api/endpoint.go +++ b/manager/api/endpoint.go @@ -26,7 +26,7 @@ func listPropletsEndpoint(svc manager.Service) endpoint.Endpoint { } return listpropletResponse{ - PropletPage: proplets, + PropletPageView: proplets.View(), }, nil } } @@ -47,11 +47,30 @@ func getPropletEndpoint(svc manager.Service) endpoint.Endpoint { } return propletResponse{ - Proplet: node, + PropletView: node.View(), }, nil } } +func getPropletAliveHistoryEndpoint(svc manager.Service) endpoint.Endpoint { + return func(ctx context.Context, request any) (any, error) { + req, ok := request.(metricsReq) + if !ok { + return propletAliveHistoryResponse{}, errors.Join(apiutil.ErrValidation, pkgerrors.ErrInvalidData) + } + if err := req.validate(); err != nil { + return propletAliveHistoryResponse{}, errors.Join(apiutil.ErrValidation, err) + } + + page, err := svc.GetPropletAliveHistory(ctx, req.id, req.offset, req.limit) + if err != nil { + return propletAliveHistoryResponse{}, err + } + + return propletAliveHistoryResponse{PropletAliveHistoryPage: page}, nil + } +} + func deletePropletEndpoint(svc manager.Service) endpoint.Endpoint { return func(ctx context.Context, request any) (any, error) { req, ok := request.(entityReq) diff --git a/manager/api/responses.go b/manager/api/responses.go index 536aea9c..11465c06 100644 --- a/manager/api/responses.go +++ b/manager/api/responses.go @@ -12,6 +12,7 @@ import ( var ( _ supermq.Response = (*propletResponse)(nil) _ supermq.Response = (*listpropletResponse)(nil) + _ supermq.Response = (*propletAliveHistoryResponse)(nil) _ supermq.Response = (*taskResponse)(nil) _ supermq.Response = (*listTaskResponse)(nil) _ supermq.Response = (*messageResponse)(nil) @@ -24,7 +25,7 @@ var ( ) type propletResponse struct { - proplet.Proplet + proplet.PropletView created bool deleted bool @@ -56,7 +57,7 @@ func (w propletResponse) Empty() bool { } type listpropletResponse struct { - proplet.PropletPage + proplet.PropletPageView } func (l listpropletResponse) Code() int { @@ -71,6 +72,22 @@ func (l listpropletResponse) Empty() bool { return false } +type propletAliveHistoryResponse struct { + proplet.PropletAliveHistoryPage +} + +func (r propletAliveHistoryResponse) Code() int { + return http.StatusOK +} + +func (r propletAliveHistoryResponse) Headers() map[string]string { + return map[string]string{} +} + +func (r propletAliveHistoryResponse) Empty() bool { + return false +} + type taskResponse struct { task.Task diff --git a/manager/api/transport.go b/manager/api/transport.go index faf0e260..1efd65e9 100644 --- a/manager/api/transport.go +++ b/manager/api/transport.go @@ -57,6 +57,12 @@ func MakeHandler(svc manager.Service, logger *slog.Logger, instanceID string) ht api.EncodeResponse, opts..., ), "get-proplet-metrics").ServeHTTP) + r.Get("/alive-history", otelhttp.NewHandler(kithttp.NewServer( + getPropletAliveHistoryEndpoint(svc), + decodeMetricsReq("propletID"), + api.EncodeResponse, + opts..., + ), "get-proplet-alive-history").ServeHTTP) }) }) diff --git a/manager/manager.go b/manager/manager.go index e38fc7af..f288ccdf 100644 --- a/manager/manager.go +++ b/manager/manager.go @@ -32,6 +32,7 @@ type Service interface { GetTaskMetrics(ctx context.Context, taskID string, offset, limit uint64) (TaskMetricsPage, error) GetPropletMetrics(ctx context.Context, propletID string, offset, limit uint64) (PropletMetricsPage, error) + GetPropletAliveHistory(ctx context.Context, propletID string, offset, limit uint64) (proplet.PropletAliveHistoryPage, error) // Orchestrator/Experiment Config API (Manager acts as Orchestrator per diagram) // Step 1: Configure experiment with FL Coordinator diff --git a/manager/middleware/logging.go b/manager/middleware/logging.go index 9ef334a3..96f53822 100644 --- a/manager/middleware/logging.go +++ b/manager/middleware/logging.go @@ -86,6 +86,28 @@ func (lm *loggingMiddleware) SelectProplet(ctx context.Context, t task.Task) (w return lm.svc.SelectProplet(ctx, t) } +func (lm *loggingMiddleware) GetPropletAliveHistory(ctx context.Context, propletID string, offset, limit uint64) (resp proplet.PropletAliveHistoryPage, err error) { + defer func(begin time.Time) { + args := []any{ + slog.String("duration", time.Since(begin).String()), + slog.Group("proplet", + slog.String("id", propletID), + ), + slog.Uint64("offset", offset), + slog.Uint64("limit", limit), + } + if err != nil { + args = append(args, slog.Any("error", err)) + lm.logger.Warn("Get proplet alive history failed", args...) + + return + } + lm.logger.Info("Get proplet alive history completed successfully", args...) + }(time.Now()) + + return lm.svc.GetPropletAliveHistory(ctx, propletID, offset, limit) +} + func (lm *loggingMiddleware) DeleteProplet(ctx context.Context, id string) (err error) { defer func(begin time.Time) { args := []any{ diff --git a/manager/middleware/metrics.go b/manager/middleware/metrics.go index c4ae24db..86237ab4 100644 --- a/manager/middleware/metrics.go +++ b/manager/middleware/metrics.go @@ -53,6 +53,15 @@ func (mm *metricsMiddleware) SelectProplet(ctx context.Context, t task.Task) (pr return mm.svc.SelectProplet(ctx, t) } +func (mm *metricsMiddleware) GetPropletAliveHistory(ctx context.Context, propletID string, offset, limit uint64) (proplet.PropletAliveHistoryPage, error) { + defer func(begin time.Time) { + mm.counter.With("method", "get-proplet-alive-history").Add(1) + mm.latency.With("method", "get-proplet-alive-history").Observe(time.Since(begin).Seconds()) + }(time.Now()) + + return mm.svc.GetPropletAliveHistory(ctx, propletID, offset, limit) +} + func (mm *metricsMiddleware) DeleteProplet(ctx context.Context, id string) error { defer func(begin time.Time) { mm.counter.With("method", "delete-proplet").Add(1) diff --git a/manager/middleware/tracing.go b/manager/middleware/tracing.go index 3000a4f0..c740169b 100644 --- a/manager/middleware/tracing.go +++ b/manager/middleware/tracing.go @@ -52,6 +52,17 @@ func (tm *tracing) SelectProplet(ctx context.Context, t task.Task) (resp proplet return tm.svc.SelectProplet(ctx, t) } +func (tm *tracing) GetPropletAliveHistory(ctx context.Context, propletID string, offset, limit uint64) (proplet.PropletAliveHistoryPage, error) { + ctx, span := tm.tracer.Start(ctx, "get-proplet-alive-history", trace.WithAttributes( + attribute.String("id", propletID), + attribute.Int64("offset", int64(offset)), + attribute.Int64("limit", int64(limit)), + )) + defer span.End() + + return tm.svc.GetPropletAliveHistory(ctx, propletID, offset, limit) +} + func (tm *tracing) DeleteProplet(ctx context.Context, id string) (err error) { ctx, span := tm.tracer.Start(ctx, "delete-proplet", trace.WithAttributes( attribute.String("id", id), diff --git a/manager/mocks/service.go b/manager/mocks/service.go index 77f39d3f..7e17e401 100644 --- a/manager/mocks/service.go +++ b/manager/mocks/service.go @@ -783,6 +783,72 @@ func (_c *MockService_GetPropletMetrics_Call) RunAndReturn(run func(ctx context. return _c } +func (_mock *MockService) GetPropletAliveHistory(ctx context.Context, propletID string, offset uint64, limit uint64) (proplet.PropletAliveHistoryPage, error) { + ret := _mock.Called(ctx, propletID, offset, limit) + + if len(ret) == 0 { + panic("no return value specified for GetPropletAliveHistory") + } + + var r0 proplet.PropletAliveHistoryPage + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, string, uint64, uint64) (proplet.PropletAliveHistoryPage, error)); ok { + return returnFunc(ctx, propletID, offset, limit) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, string, uint64, uint64) proplet.PropletAliveHistoryPage); ok { + r0 = returnFunc(ctx, propletID, offset, limit) + } else { + r0 = ret.Get(0).(proplet.PropletAliveHistoryPage) + } + if returnFunc, ok := ret.Get(1).(func(context.Context, string, uint64, uint64) error); ok { + r1 = returnFunc(ctx, propletID, offset, limit) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +type MockService_GetPropletAliveHistory_Call struct { + *mock.Call +} + +func (_e *MockService_Expecter) GetPropletAliveHistory(ctx any, propletID any, offset any, limit any) *MockService_GetPropletAliveHistory_Call { + return &MockService_GetPropletAliveHistory_Call{Call: _e.mock.On("GetPropletAliveHistory", ctx, propletID, offset, limit)} +} + +func (_c *MockService_GetPropletAliveHistory_Call) Run(run func(ctx context.Context, propletID string, offset uint64, limit uint64)) *MockService_GetPropletAliveHistory_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + var arg2 uint64 + if args[2] != nil { + arg2 = args[2].(uint64) + } + var arg3 uint64 + if args[3] != nil { + arg3 = args[3].(uint64) + } + run(arg0, arg1, arg2, arg3) + }) + return _c +} + +func (_c *MockService_GetPropletAliveHistory_Call) Return(page proplet.PropletAliveHistoryPage, err error) *MockService_GetPropletAliveHistory_Call { + _c.Call.Return(page, err) + return _c +} + +func (_c *MockService_GetPropletAliveHistory_Call) RunAndReturn(run func(ctx context.Context, propletID string, offset uint64, limit uint64) (proplet.PropletAliveHistoryPage, error)) *MockService_GetPropletAliveHistory_Call { + _c.Call.Return(run) + return _c +} + // GetRoundStatus provides a mock function for the type MockService func (_mock *MockService) GetRoundStatus(ctx context.Context, roundID string) (manager.RoundStatus, error) { ret := _mock.Called(ctx, roundID) diff --git a/manager/service.go b/manager/service.go index c06d40f1..81183060 100644 --- a/manager/service.go +++ b/manager/service.go @@ -805,6 +805,20 @@ func (svc *service) GetPropletMetrics(ctx context.Context, propletID string, off }, nil } +func (svc *service) GetPropletAliveHistory(ctx context.Context, propletID string, offset, limit uint64) (proplet.PropletAliveHistoryPage, error) { + history, total, err := svc.propletRepo.GetAliveHistory(ctx, propletID, offset, limit) + if err != nil { + return proplet.PropletAliveHistoryPage{}, err + } + + return proplet.PropletAliveHistoryPage{ + Offset: offset, + Limit: limit, + Total: total, + History: history, + }, nil +} + func (svc *service) GetTaskResults(ctx context.Context, taskID string) (any, error) { t, err := svc.GetTask(ctx, taskID) if err != nil { diff --git a/pkg/proplet/proplet.go b/pkg/proplet/proplet.go index 1f76ed6e..4bf89aad 100644 --- a/pkg/proplet/proplet.go +++ b/pkg/proplet/proplet.go @@ -46,6 +46,57 @@ type PropletPage struct { Proplets []Proplet `json:"proplets"` } +type PropletView struct { + ID string `json:"id"` + Name string `json:"name"` + TaskCount uint64 `json:"task_count"` + Alive bool `json:"alive"` + LastAliveAt *time.Time `json:"last_alive_at,omitempty"` + Metadata PropletMetadata `json:"metadata"` +} + +func (p Proplet) View() PropletView { + v := PropletView{ + ID: p.ID, + Name: p.Name, + TaskCount: p.TaskCount, + Alive: p.Alive, + Metadata: p.Metadata, + } + if n := len(p.AliveHistory); n > 0 { + t := p.AliveHistory[n-1] + v.LastAliveAt = &t + } + return v +} + +type PropletPageView struct { + Offset uint64 `json:"offset"` + Limit uint64 `json:"limit"` + Total uint64 `json:"total"` + Proplets []PropletView `json:"proplets"` +} + +func (pp PropletPage) View() PropletPageView { + views := make([]PropletView, len(pp.Proplets)) + for i, p := range pp.Proplets { + views[i] = p.View() + } + return PropletPageView{ + Offset: pp.Offset, + Limit: pp.Limit, + Total: pp.Total, + Proplets: views, + } +} + +type PropletAliveHistoryPage struct { + Offset uint64 `json:"offset"` + Limit uint64 `json:"limit"` + Total uint64 `json:"total"` + History []time.Time `json:"history"` +} + type ChunkPayload struct { AppName string `json:"app_name"` ChunkIdx int `json:"chunk_idx"` diff --git a/pkg/sdk/proplet.go b/pkg/sdk/proplet.go index 19c68361..c368c3ff 100644 --- a/pkg/sdk/proplet.go +++ b/pkg/sdk/proplet.go @@ -1,9 +1,31 @@ package sdk -import "net/http" +import ( + "encoding/json" + "fmt" + "net/http" + + "github.com/absmach/propeller/pkg/proplet" +) const propletsEndpoint = "/proplets" +func (sdk *propSDK) GetPropletAliveHistory(id string, offset, limit uint64) (proplet.PropletAliveHistoryPage, error) { + url := fmt.Sprintf("%s%s/%s/alive-history?offset=%d&limit=%d", sdk.managerURL, propletsEndpoint, id, offset, limit) + + body, err := sdk.processRequest(http.MethodGet, url, nil, http.StatusOK) + if err != nil { + return proplet.PropletAliveHistoryPage{}, err + } + + var page proplet.PropletAliveHistoryPage + if err := json.Unmarshal(body, &page); err != nil { + return proplet.PropletAliveHistoryPage{}, err + } + + return page, nil +} + func (sdk *propSDK) DeleteProplet(id string) error { url := sdk.managerURL + propletsEndpoint + "/" + id diff --git a/pkg/sdk/sdk.go b/pkg/sdk/sdk.go index c3cb6b28..1b9326b5 100644 --- a/pkg/sdk/sdk.go +++ b/pkg/sdk/sdk.go @@ -6,6 +6,8 @@ import ( "fmt" "io" "net/http" + + "github.com/absmach/propeller/pkg/proplet" ) const CTJSON string = "application/json" @@ -106,6 +108,13 @@ type SDK interface { // _ := sdk.StopJob("b1d10738-c5d7-4ff1-8f4d-b9328ce6f040") StopJob(jobID string) error + // GetPropletAliveHistory returns the paginated heartbeat history for a proplet. + // + // example: + // page, _ := sdk.GetPropletAliveHistory("b1d10738-c5d7-4ff1-8f4d-b9328ce6f040", 0, 10) + // fmt.Println(page) + GetPropletAliveHistory(id string, offset, limit uint64) (proplet.PropletAliveHistoryPage, error) + // DeleteProplet deletes a proplet by id. // // example: diff --git a/pkg/storage/badger/init.go b/pkg/storage/badger/init.go index 0155a1b4..52a2a428 100644 --- a/pkg/storage/badger/init.go +++ b/pkg/storage/badger/init.go @@ -63,6 +63,7 @@ type PropletRepository interface { Update(ctx context.Context, p proplet.Proplet) error List(ctx context.Context, offset, limit uint64) ([]proplet.Proplet, uint64, error) Delete(ctx context.Context, id string) error + GetAliveHistory(ctx context.Context, id string, offset, limit uint64) ([]time.Time, uint64, error) } type TaskPropletRepository interface { diff --git a/pkg/storage/badger/proplets.go b/pkg/storage/badger/proplets.go index f4ec7712..e9e696f0 100644 --- a/pkg/storage/badger/proplets.go +++ b/pkg/storage/badger/proplets.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "time" "github.com/absmach/propeller/pkg/proplet" ) @@ -83,3 +84,22 @@ func (r *propletRepo) Delete(ctx context.Context, id string) error { return r.db.delete(key) } + +func (r *propletRepo) GetAliveHistory(ctx context.Context, id string, offset, limit uint64) ([]time.Time, uint64, error) { + p, err := r.Get(ctx, id) + if err != nil { + return nil, 0, err + } + + total := uint64(len(p.AliveHistory)) + if offset >= total { + return []time.Time{}, total, nil + } + + end := offset + limit + if end > total { + end = total + } + + return p.AliveHistory[offset:end], total, nil +} diff --git a/pkg/storage/factory.go b/pkg/storage/factory.go index f4d37798..30785b52 100644 --- a/pkg/storage/factory.go +++ b/pkg/storage/factory.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "io" + "time" "github.com/absmach/propeller/pkg/job" "github.com/absmach/propeller/pkg/proplet" @@ -193,6 +194,10 @@ func (a *postgresPropletAdapter) Delete(ctx context.Context, id string) error { return a.repo.Delete(ctx, id) } +func (a *postgresPropletAdapter) GetAliveHistory(ctx context.Context, id string, offset, limit uint64) ([]time.Time, uint64, error) { + return a.repo.GetAliveHistory(ctx, id, offset, limit) +} + type postgresTaskPropletAdapter struct { repo postgres.TaskPropletRepository } @@ -336,6 +341,10 @@ func (a *sqlitePropletAdapter) Delete(ctx context.Context, id string) error { return a.repo.Delete(ctx, id) } +func (a *sqlitePropletAdapter) GetAliveHistory(ctx context.Context, id string, offset, limit uint64) ([]time.Time, uint64, error) { + return a.repo.GetAliveHistory(ctx, id, offset, limit) +} + type sqliteTaskPropletAdapter struct { repo sqlite.TaskPropletRepository } @@ -479,6 +488,10 @@ func (a *badgerPropletAdapter) Delete(ctx context.Context, id string) error { return a.repo.Delete(ctx, id) } +func (a *badgerPropletAdapter) GetAliveHistory(ctx context.Context, id string, offset, limit uint64) ([]time.Time, uint64, error) { + return a.repo.GetAliveHistory(ctx, id, offset, limit) +} + type badgerTaskPropletAdapter struct { repo badger.TaskPropletRepository } diff --git a/pkg/storage/memory_adapter.go b/pkg/storage/memory_adapter.go index 77399099..b74c5e9b 100644 --- a/pkg/storage/memory_adapter.go +++ b/pkg/storage/memory_adapter.go @@ -3,6 +3,7 @@ package storage import ( "context" "fmt" + "time" pkgerrors "github.com/absmach/propeller/pkg/errors" "github.com/absmach/propeller/pkg/job" @@ -156,6 +157,25 @@ func (r *memoryPropletRepo) Delete(ctx context.Context, id string) error { return r.storage.Delete(ctx, id) } +func (r *memoryPropletRepo) GetAliveHistory(ctx context.Context, id string, offset, limit uint64) ([]time.Time, uint64, error) { + p, err := r.Get(ctx, id) + if err != nil { + return nil, 0, err + } + + total := uint64(len(p.AliveHistory)) + if offset >= total { + return []time.Time{}, total, nil + } + + end := offset + limit + if end > total { + end = total + } + + return p.AliveHistory[offset:end], total, nil +} + type memoryTaskPropletRepo struct { storage Storage } diff --git a/pkg/storage/mocks/proplet_repository.go b/pkg/storage/mocks/proplet_repository.go index 6c4040ab..7fdb7194 100644 --- a/pkg/storage/mocks/proplet_repository.go +++ b/pkg/storage/mocks/proplet_repository.go @@ -6,6 +6,7 @@ package mocks import ( "context" + "time" "github.com/absmach/propeller/pkg/proplet" mock "github.com/stretchr/testify/mock" @@ -354,3 +355,77 @@ func (_c *MockPropletRepository_Update_Call) RunAndReturn(run func(ctx context.C _c.Call.Return(run) return _c } + +func (_mock *MockPropletRepository) GetAliveHistory(ctx context.Context, id string, offset uint64, limit uint64) ([]time.Time, uint64, error) { + ret := _mock.Called(ctx, id, offset, limit) + + if len(ret) == 0 { + panic("no return value specified for GetAliveHistory") + } + + var r0 []time.Time + var r1 uint64 + var r2 error + if returnFunc, ok := ret.Get(0).(func(context.Context, string, uint64, uint64) ([]time.Time, uint64, error)); ok { + return returnFunc(ctx, id, offset, limit) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, string, uint64, uint64) []time.Time); ok { + r0 = returnFunc(ctx, id, offset, limit) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]time.Time) + } + } + if returnFunc, ok := ret.Get(1).(func(context.Context, string, uint64, uint64) uint64); ok { + r1 = returnFunc(ctx, id, offset, limit) + } else { + r1 = ret.Get(1).(uint64) + } + if returnFunc, ok := ret.Get(2).(func(context.Context, string, uint64, uint64) error); ok { + r2 = returnFunc(ctx, id, offset, limit) + } else { + r2 = ret.Error(2) + } + return r0, r1, r2 +} + +type MockPropletRepository_GetAliveHistory_Call struct { + *mock.Call +} + +func (_e *MockPropletRepository_Expecter) GetAliveHistory(ctx interface{}, id interface{}, offset interface{}, limit interface{}) *MockPropletRepository_GetAliveHistory_Call { + return &MockPropletRepository_GetAliveHistory_Call{Call: _e.mock.On("GetAliveHistory", ctx, id, offset, limit)} +} + +func (_c *MockPropletRepository_GetAliveHistory_Call) Run(run func(ctx context.Context, id string, offset uint64, limit uint64)) *MockPropletRepository_GetAliveHistory_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + var arg2 uint64 + if args[2] != nil { + arg2 = args[2].(uint64) + } + var arg3 uint64 + if args[3] != nil { + arg3 = args[3].(uint64) + } + run(arg0, arg1, arg2, arg3) + }) + return _c +} + +func (_c *MockPropletRepository_GetAliveHistory_Call) Return(history []time.Time, total uint64, err error) *MockPropletRepository_GetAliveHistory_Call { + _c.Call.Return(history, total, err) + return _c +} + +func (_c *MockPropletRepository_GetAliveHistory_Call) RunAndReturn(run func(ctx context.Context, id string, offset uint64, limit uint64) ([]time.Time, uint64, error)) *MockPropletRepository_GetAliveHistory_Call { + _c.Call.Return(run) + return _c +} diff --git a/pkg/storage/postgres/init.go b/pkg/storage/postgres/init.go index 6465d4b6..fda99ae2 100644 --- a/pkg/storage/postgres/init.go +++ b/pkg/storage/postgres/init.go @@ -65,6 +65,7 @@ type PropletRepository interface { Update(ctx context.Context, p proplet.Proplet) error List(ctx context.Context, offset, limit uint64) ([]proplet.Proplet, uint64, error) Delete(ctx context.Context, id string) error + GetAliveHistory(ctx context.Context, id string, offset, limit uint64) ([]time.Time, uint64, error) } type TaskPropletRepository interface { diff --git a/pkg/storage/postgres/proplets.go b/pkg/storage/postgres/proplets.go index 0b45700b..a19423f4 100644 --- a/pkg/storage/postgres/proplets.go +++ b/pkg/storage/postgres/proplets.go @@ -5,6 +5,7 @@ import ( "database/sql" "errors" "fmt" + "time" "github.com/absmach/propeller/pkg/proplet" ) @@ -129,6 +130,25 @@ func (r *propletRepo) Delete(ctx context.Context, id string) error { return nil } +func (r *propletRepo) GetAliveHistory(ctx context.Context, id string, offset, limit uint64) ([]time.Time, uint64, error) { + p, err := r.Get(ctx, id) + if err != nil { + return nil, 0, err + } + + total := uint64(len(p.AliveHistory)) + if offset >= total { + return []time.Time{}, total, nil + } + + end := offset + limit + if end > total { + end = total + } + + return p.AliveHistory[offset:end], total, nil +} + func (r *propletRepo) toProplet(dbp dbProplet) (proplet.Proplet, error) { p := proplet.Proplet{ ID: dbp.ID, diff --git a/pkg/storage/repository.go b/pkg/storage/repository.go index 0379139c..bff7fe0d 100644 --- a/pkg/storage/repository.go +++ b/pkg/storage/repository.go @@ -2,6 +2,7 @@ package storage import ( "context" + "time" "github.com/absmach/propeller/pkg/job" "github.com/absmach/propeller/pkg/proplet" @@ -24,6 +25,7 @@ type PropletRepository interface { Update(ctx context.Context, p proplet.Proplet) error List(ctx context.Context, offset, limit uint64) ([]proplet.Proplet, uint64, error) Delete(ctx context.Context, id string) error + GetAliveHistory(ctx context.Context, id string, offset, limit uint64) ([]time.Time, uint64, error) } type TaskPropletRepository interface { diff --git a/pkg/storage/sqlite/init.go b/pkg/storage/sqlite/init.go index 2ad7da03..9104a202 100644 --- a/pkg/storage/sqlite/init.go +++ b/pkg/storage/sqlite/init.go @@ -65,6 +65,7 @@ type PropletRepository interface { Update(ctx context.Context, p proplet.Proplet) error List(ctx context.Context, offset, limit uint64) ([]proplet.Proplet, uint64, error) Delete(ctx context.Context, id string) error + GetAliveHistory(ctx context.Context, id string, offset, limit uint64) ([]time.Time, uint64, error) } type TaskPropletRepository interface { diff --git a/pkg/storage/sqlite/proplets.go b/pkg/storage/sqlite/proplets.go index 86c991f8..a26a010d 100644 --- a/pkg/storage/sqlite/proplets.go +++ b/pkg/storage/sqlite/proplets.go @@ -5,6 +5,7 @@ import ( "database/sql" "errors" "fmt" + "time" "github.com/absmach/propeller/pkg/proplet" ) @@ -152,3 +153,22 @@ func (r *propletRepo) toProplet(dbp dbProplet) (proplet.Proplet, error) { return p, nil } + +func (r *propletRepo) GetAliveHistory(ctx context.Context, id string, offset, limit uint64) ([]time.Time, uint64, error) { + p, err := r.Get(ctx, id) + if err != nil { + return nil, 0, err + } + + total := uint64(len(p.AliveHistory)) + if offset >= total { + return []time.Time{}, total, nil + } + + end := offset + limit + if end > total { + end = total + } + + return p.AliveHistory[offset:end], total, nil +} From 0f4849116b74deb20b5531f69dd1f5995e9c5930 Mon Sep 17 00:00:00 2001 From: JeffMboya Date: Wed, 8 Apr 2026 11:14:36 +0300 Subject: [PATCH 2/2] fix(proplet): resolve linter errors in proplet and storage packages --- pkg/proplet/proplet.go | 8 +++++--- pkg/storage/badger/proplets.go | 5 +---- pkg/storage/memory_adapter.go | 5 +---- pkg/storage/postgres/proplets.go | 5 +---- pkg/storage/sqlite/proplets.go | 35 +++++++++++++++----------------- 5 files changed, 24 insertions(+), 34 deletions(-) diff --git a/pkg/proplet/proplet.go b/pkg/proplet/proplet.go index 4bf89aad..92326ea9 100644 --- a/pkg/proplet/proplet.go +++ b/pkg/proplet/proplet.go @@ -55,7 +55,7 @@ type PropletView struct { Metadata PropletMetadata `json:"metadata"` } -func (p Proplet) View() PropletView { +func (p *Proplet) View() PropletView { v := PropletView{ ID: p.ID, Name: p.Name, @@ -67,6 +67,7 @@ func (p Proplet) View() PropletView { t := p.AliveHistory[n-1] v.LastAliveAt = &t } + return v } @@ -79,9 +80,10 @@ type PropletPageView struct { func (pp PropletPage) View() PropletPageView { views := make([]PropletView, len(pp.Proplets)) - for i, p := range pp.Proplets { - views[i] = p.View() + for i := range pp.Proplets { + views[i] = pp.Proplets[i].View() } + return PropletPageView{ Offset: pp.Offset, Limit: pp.Limit, diff --git a/pkg/storage/badger/proplets.go b/pkg/storage/badger/proplets.go index e9e696f0..f6054a36 100644 --- a/pkg/storage/badger/proplets.go +++ b/pkg/storage/badger/proplets.go @@ -96,10 +96,7 @@ func (r *propletRepo) GetAliveHistory(ctx context.Context, id string, offset, li return []time.Time{}, total, nil } - end := offset + limit - if end > total { - end = total - } + end := min(offset+limit, total) return p.AliveHistory[offset:end], total, nil } diff --git a/pkg/storage/memory_adapter.go b/pkg/storage/memory_adapter.go index b74c5e9b..e41ddb4c 100644 --- a/pkg/storage/memory_adapter.go +++ b/pkg/storage/memory_adapter.go @@ -168,10 +168,7 @@ func (r *memoryPropletRepo) GetAliveHistory(ctx context.Context, id string, offs return []time.Time{}, total, nil } - end := offset + limit - if end > total { - end = total - } + end := min(offset+limit, total) return p.AliveHistory[offset:end], total, nil } diff --git a/pkg/storage/postgres/proplets.go b/pkg/storage/postgres/proplets.go index a19423f4..12e2ff43 100644 --- a/pkg/storage/postgres/proplets.go +++ b/pkg/storage/postgres/proplets.go @@ -141,10 +141,7 @@ func (r *propletRepo) GetAliveHistory(ctx context.Context, id string, offset, li return []time.Time{}, total, nil } - end := offset + limit - if end > total { - end = total - } + end := min(offset+limit, total) return p.AliveHistory[offset:end], total, nil } diff --git a/pkg/storage/sqlite/proplets.go b/pkg/storage/sqlite/proplets.go index a26a010d..d7fd3e58 100644 --- a/pkg/storage/sqlite/proplets.go +++ b/pkg/storage/sqlite/proplets.go @@ -131,6 +131,22 @@ func (r *propletRepo) Delete(ctx context.Context, id string) error { return nil } +func (r *propletRepo) GetAliveHistory(ctx context.Context, id string, offset, limit uint64) ([]time.Time, uint64, error) { + p, err := r.Get(ctx, id) + if err != nil { + return nil, 0, err + } + + total := uint64(len(p.AliveHistory)) + if offset >= total { + return []time.Time{}, total, nil + } + + end := min(offset+limit, total) + + return p.AliveHistory[offset:end], total, nil +} + func (r *propletRepo) toProplet(dbp dbProplet) (proplet.Proplet, error) { p := proplet.Proplet{ ID: dbp.ID, @@ -153,22 +169,3 @@ func (r *propletRepo) toProplet(dbp dbProplet) (proplet.Proplet, error) { return p, nil } - -func (r *propletRepo) GetAliveHistory(ctx context.Context, id string, offset, limit uint64) ([]time.Time, uint64, error) { - p, err := r.Get(ctx, id) - if err != nil { - return nil, 0, err - } - - total := uint64(len(p.AliveHistory)) - if offset >= total { - return []time.Time{}, total, nil - } - - end := offset + limit - if end > total { - end = total - } - - return p.AliveHistory[offset:end], total, nil -}