From 39e6b044bcbfaa6947ff572dd7ed6f608645206c Mon Sep 17 00:00:00 2001 From: Jude Hung Date: Fri, 20 Nov 2020 05:17:46 +0800 Subject: [PATCH] feat(data): Implement DELETE /event/device/name/{name} V2 API (#2874) * feat(data): Implement DELETE /event/device/name/{name} V2 API Implement DELETE /event/device/name/{name} for V2 API. The current V2 API doc still lists DELETE /event/device/{deviceId}; however, as https://github.com/edgexfoundry/go-mod-core-contracts/blob/master/v2/models/event.go#L14 has been updated to DeviceName instead of DeviceId, the V2 API spec shall be updated to reflect this change. Update core-data.yaml with removal of 404 response Fix #2837 Signed-off-by: Jude Hung --- internal/core/data/v2/application/event.go | 17 +++ .../core/data/v2/application/event_test.go | 39 ++++++ .../core/data/v2/controller/http/event.go | 29 +++++ .../data/v2/controller/http/event_test.go | 29 +++++ .../data/v2/infrastructure/interfaces/db.go | 1 + .../interfaces/mocks/DBClient.go | 16 +++ internal/core/data/v2/router.go | 1 + internal/pkg/v2/infrastructure/redis/event.go | 36 ++++-- openapi/v2/core-data.yaml | 112 ++++++++---------- 9 files changed, 208 insertions(+), 72 deletions(-) diff --git a/internal/core/data/v2/application/event.go b/internal/core/data/v2/application/event.go index fb6c1380ab..37444c79d8 100644 --- a/internal/core/data/v2/application/event.go +++ b/internal/core/data/v2/application/event.go @@ -9,6 +9,7 @@ import ( "context" "encoding/json" "fmt" + "strings" dataContainer "github.com/edgexfoundry/edgex-go/internal/core/data/container" v2DataContainer "github.com/edgexfoundry/edgex-go/internal/core/data/v2/bootstrap/container" @@ -170,6 +171,22 @@ func DeletePushedEvents(dic *di.Container) errors.EdgeX { return nil } +// The DeleteEventsByDeviceName function will be invoked by controller functions +// and then invokes DeleteEventsByDeviceName function in the infrastructure layer to remove +// all events/readings that are associated with the given deviceName +func DeleteEventsByDeviceName(deviceName string, dic *di.Container) errors.EdgeX { + if len(strings.TrimSpace(deviceName)) <= 0 { + return errors.NewCommonEdgeX(errors.KindInvalidId, "blank device name is not allowed", nil) + } + dbClient := v2DataContainer.DBClientFrom(dic.Get) + + err := dbClient.DeleteEventsByDeviceName(deviceName) + if err != nil { + return errors.NewCommonEdgeXWrapper(err) + } + return nil +} + // UpdateEventPushedById updates event pushed timestamp per incoming event id func UpdateEventPushedById(id string, dic *di.Container) errors.EdgeX { dbClient := v2DataContainer.DBClientFrom(dic.Get) diff --git a/internal/core/data/v2/application/event_test.go b/internal/core/data/v2/application/event_test.go index 977683564f..a257d5fa5e 100644 --- a/internal/core/data/v2/application/event_test.go +++ b/internal/core/data/v2/application/event_test.go @@ -90,6 +90,7 @@ func newMockDB(persist bool) *dbMock.DBClient { myMock.On("EventTotalCount").Return(testEventCount, nil) myMock.On("EventCountByDevice", testDeviceName).Return(testEventCount, nil) myMock.On("DeletePushedEvents").Return(nil) + myMock.On("DeleteEventsByDeviceName", testDeviceName).Return(nil) } return myMock @@ -272,3 +273,41 @@ func TestDeletePushedEvents(t *testing.T) { err := DeletePushedEvents(dic) require.NoError(t, err) } + +func TestDeleteEventsByDeviceName(t *testing.T) { + dbClientMock := newMockDB(true) + + dic := mocks.NewMockDIC() + dic.Update(di.ServiceConstructorMap{ + v2DataContainer.DBClientInterfaceName: func(get di.Get) interface{} { + return dbClientMock + }, + }) + + tests := []struct { + Name string + deviceName string + ErrorExpected bool + ExpectedErrKind errors.ErrKind + ExpectedStatusCode int + }{ + {"Valid - Delete Event by Id", testDeviceName, false, errors.KindInvalidId, http.StatusOK}, + {"Invalid - Empty device name", "", true, errors.KindInvalidId, http.StatusBadRequest}, + {"Invalid - Empty device name with spaces", " \n\t\r ", true, errors.KindInvalidId, http.StatusBadRequest}, + } + + for _, testCase := range tests { + t.Run(testCase.Name, func(t *testing.T) { + err := DeleteEventsByDeviceName(testCase.deviceName, dic) + + if testCase.ErrorExpected { + require.Error(t, err) + assert.NotEmpty(t, err.Error(), "Error message is empty") + assert.Equal(t, testCase.ExpectedErrKind, errors.Kind(err), "Error kind not as expected") + assert.Equal(t, testCase.ExpectedStatusCode, err.Code(), "Error code not as expected") + } else { + require.NoError(t, err) + } + }) + } +} diff --git a/internal/core/data/v2/controller/http/event.go b/internal/core/data/v2/controller/http/event.go index 628bc473af..67b76e839e 100644 --- a/internal/core/data/v2/controller/http/event.go +++ b/internal/core/data/v2/controller/http/event.go @@ -352,3 +352,32 @@ func (ec *EventController) EventsByDeviceName(w http.ResponseWriter, r *http.Req utils.WriteHttpHeader(w, ctx, statusCode) pkg.Encode(response, w, lc) } + +func (ec *EventController) DeleteEventsByDeviceName(w http.ResponseWriter, r *http.Request) { + // retrieve all the service injections from bootstrap + lc := container.LoggingClientFrom(ec.dic.Get) + + ctx := r.Context() + correlationId := correlation.FromContext(ctx) + + vars := mux.Vars(r) + deviceName := vars[v2.Name] + + var response interface{} + var statusCode int + + // Delete events with associated Device deviceName + err := application.DeleteEventsByDeviceName(deviceName, ec.dic) + if err != nil { + lc.Debug(err.DebugMessages(), clients.CorrelationHeader, correlationId) + response = commonDTO.NewBaseResponse("", err.Message(), err.Code()) + statusCode = err.Code() + } else { + response = commonDTO.NewBaseResponse("", "", http.StatusAccepted) + statusCode = http.StatusAccepted + } + + utils.WriteHttpHeader(w, ctx, statusCode) + // encode and send out the response + pkg.Encode(response, w, lc) +} diff --git a/internal/core/data/v2/controller/http/event_test.go b/internal/core/data/v2/controller/http/event_test.go index a98a3e6069..c48babf3c5 100644 --- a/internal/core/data/v2/controller/http/event_test.go +++ b/internal/core/data/v2/controller/http/event_test.go @@ -437,6 +437,35 @@ func TestDeletePushedEvents(t *testing.T) { assert.Empty(t, actualResponse.Message, "Message should be empty when it is successful") } +func TestDeleteEventsByDeviceName(t *testing.T) { + deviceName := "deviceA" + dbClientMock := &dbMock.DBClient{} + dbClientMock.On("DeleteEventsByDeviceName", deviceName).Return(nil) + + dic := mocks.NewMockDIC() + dic.Update(di.ServiceConstructorMap{ + v2DataContainer.DBClientInterfaceName: func(get di.Get) interface{} { + return dbClientMock + }, + }) + ec := NewEventController(dic) + + req, err := http.NewRequest(http.MethodDelete, v2.ApiEventByDeviceNameRoute, http.NoBody) + req = mux.SetURLVars(req, map[string]string{v2.Name: deviceName}) + require.NoError(t, err) + + recorder := httptest.NewRecorder() + handler := http.HandlerFunc(ec.DeleteEventsByDeviceName) + handler.ServeHTTP(recorder, req) + + var actualResponse common.BaseResponse + err = json.Unmarshal(recorder.Body.Bytes(), &actualResponse) + + assert.Equal(t, v2.ApiVersion, actualResponse.ApiVersion, "API Version not as expected") + assert.Equal(t, http.StatusAccepted, recorder.Result().StatusCode, "HTTP status code not as expected") + assert.Empty(t, actualResponse.Message, "Message should be empty when it is successful") +} + func TestUpdateEventPushedById(t *testing.T) { expectedResponseCode := http.StatusMultiStatus diff --git a/internal/core/data/v2/infrastructure/interfaces/db.go b/internal/core/data/v2/infrastructure/interfaces/db.go index 6b0456c1da..d7c4ea0193 100644 --- a/internal/core/data/v2/infrastructure/interfaces/db.go +++ b/internal/core/data/v2/infrastructure/interfaces/db.go @@ -22,4 +22,5 @@ type DBClient interface { AllEvents(offset int, limit int) ([]model.Event, errors.EdgeX) EventsByDeviceName(offset int, limit int, name string) ([]model.Event, errors.EdgeX) DeletePushedEvents() errors.EdgeX + DeleteEventsByDeviceName(deviceName string) errors.EdgeX } diff --git a/internal/core/data/v2/infrastructure/interfaces/mocks/DBClient.go b/internal/core/data/v2/infrastructure/interfaces/mocks/DBClient.go index fe6c984703..05c1d5ea4e 100644 --- a/internal/core/data/v2/infrastructure/interfaces/mocks/DBClient.go +++ b/internal/core/data/v2/infrastructure/interfaces/mocks/DBClient.go @@ -84,6 +84,22 @@ func (_m *DBClient) DeleteEventById(id string) errors.EdgeX { return r0 } +// DeleteEventsByDeviceName provides a mock function with given fields: deviceName +func (_m *DBClient) DeleteEventsByDeviceName(deviceName string) errors.EdgeX { + ret := _m.Called(deviceName) + + var r0 errors.EdgeX + if rf, ok := ret.Get(0).(func(string) errors.EdgeX); ok { + r0 = rf(deviceName) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(errors.EdgeX) + } + } + + return r0 +} + // DeletePushedEvents provides a mock function with given fields: func (_m *DBClient) DeletePushedEvents() errors.EdgeX { ret := _m.Called() diff --git a/internal/core/data/v2/router.go b/internal/core/data/v2/router.go index 991c5b2a08..6c6ea8bc6d 100644 --- a/internal/core/data/v2/router.go +++ b/internal/core/data/v2/router.go @@ -33,6 +33,7 @@ func LoadRestRoutes(r *mux.Router, dic *di.Container) { r.HandleFunc(v2Constant.ApiAllEventRoute, ec.AllEvents).Methods(http.MethodGet) r.HandleFunc(v2Constant.ApiEventByDeviceNameRoute, ec.EventsByDeviceName).Methods(http.MethodGet) r.HandleFunc(v2Constant.ApiEventScrubRoute, ec.DeletePushedEvents).Methods(http.MethodDelete) + r.HandleFunc(v2Constant.ApiEventByDeviceNameRoute, ec.DeleteEventsByDeviceName).Methods(http.MethodDelete) r.Use(correlation.ManageHeader) r.Use(correlation.OnResponseComplete) diff --git a/internal/pkg/v2/infrastructure/redis/event.go b/internal/pkg/v2/infrastructure/redis/event.go index 350b3949c8..ad6477fd94 100644 --- a/internal/pkg/v2/infrastructure/redis/event.go +++ b/internal/pkg/v2/infrastructure/redis/event.go @@ -87,7 +87,25 @@ func (c *Client) DeletePushedEvents() (edgeXerr errors.EdgeX) { conn := c.Pool.Get() defer conn.Close() - eventIds, readingIds, err := getPushedEventReadingIds(conn) + eventIds, readingIds, err := getEventReadingIdsByKey(conn, EventsCollectionPushed) + if err != nil { + return errors.NewCommonEdgeXWrapper(err) + } + c.loggingClient.Debug(fmt.Sprintf("Prepare to delete %v readings", len(readingIds))) + go c.asyncDeleteReadingsByIds(readingIds) + c.loggingClient.Debug(fmt.Sprintf("Prepare to delete %v events", len(eventIds))) + go c.asyncDeleteEventsByIds(eventIds) + + return nil +} + +// DeleteEventsByDeviceName deletes all pushed events and corresponding readings. This function is implemented to starts up +// two goroutines to delete readings and events in the bckground to achieve better performance. +func (c *Client) DeleteEventsByDeviceName(deviceName string) (edgeXerr errors.EdgeX) { + conn := c.Pool.Get() + defer conn.Close() + + eventIds, readingIds, err := getEventReadingIdsByKey(conn, fmt.Sprintf("%s:%s", EventsCollectionDeviceName, deviceName)) if err != nil { return errors.NewCommonEdgeXWrapper(err) } @@ -205,28 +223,28 @@ func deleteEventById(conn redis.Conn, id string) (edgeXerr errors.EdgeX) { return edgeXerr } -func getPushedEventReadingIds(conn redis.Conn) (eventIds []string, readingIds []string, edgeXerr errors.EdgeX) { - pushedEventIds, err := redis.Strings(conn.Do(ZRANGEBYSCORE, EventsCollectionPushed, GreaterThanZero, InfiniteMax)) +func getEventReadingIdsByKey(conn redis.Conn, key string) (eventIds []string, readingIds []string, edgeXerr errors.EdgeX) { + eventIds, err := redis.Strings(conn.Do(ZRANGEBYSCORE, key, GreaterThanZero, InfiniteMax)) if err != nil { - return nil, nil, errors.NewCommonEdgeX(errors.KindDatabaseError, "retrieve all pushed event ids failed", err) + return nil, nil, errors.NewCommonEdgeX(errors.KindDatabaseError, fmt.Sprintf("retrieve event ids by key %s failed", key), err) } - pushedEvents, edgeXerr := getObjectsByIds(conn, common.ConvertStringsToInterfaces(pushedEventIds)) + events, edgeXerr := getObjectsByIds(conn, common.ConvertStringsToInterfaces(eventIds)) if edgeXerr != nil { return nil, nil, edgeXerr } e := models.Event{} - for _, pushedEvent := range pushedEvents { - err = json.Unmarshal(pushedEvent, &e) + for _, event := range events { + err = json.Unmarshal(event, &e) if err != nil { return nil, nil, errors.NewCommonEdgeX(errors.KindContractInvalid, "unable to marshal event", err) } rIds, err := redis.Strings(conn.Do(ZRANGE, fmt.Sprintf("%s:%s", EventsCollectionReadings, e.Id), 0, -1)) if err != nil { - return nil, nil, errors.NewCommonEdgeX(errors.KindDatabaseError, fmt.Sprintf("retrieve all reading Ids of pushed event %s failed", e.Id), err) + return nil, nil, errors.NewCommonEdgeX(errors.KindDatabaseError, fmt.Sprintf("retrieve all reading Ids of event %s failed", e.Id), err) } readingIds = append(readingIds, rIds...) } - return pushedEventIds, readingIds, nil + return eventIds, readingIds, nil } func eventById(conn redis.Conn, id string) (event models.Event, edgeXerr errors.EdgeX) { diff --git a/openapi/v2/core-data.yaml b/openapi/v2/core-data.yaml index 1cb358e6da..de2b7bb3f5 100644 --- a/openapi/v2/core-data.yaml +++ b/openapi/v2/core-data.yaml @@ -910,68 +910,19 @@ paths: examples: 500Example: $ref: '#/components/examples/500Example' - /event/device/{deviceId}: - parameters: - - $ref: '#/components/parameters/correlatedRequestHeader' - - name: deviceId - in: path - required: true - schema: - type: string - format: uuid - description: "Uniquely identifies a given device" - delete: - summary: "Deletes all events for the specified device" - responses: - '200': - description: "Delete successful" - headers: - X-Correlation-ID: - $ref: '#/components/headers/correlatedResponseHeader' - content: - application/json: - schema: - $ref: '#/components/schemas/BaseResponse' - examples: - 200Example: - $ref: '#/components/examples/200Example' - '404': - description: "The requested resource does not exist" - headers: - X-Correlation-ID: - $ref: '#/components/headers/correlatedResponseHeader' - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - examples: - 404Example: - $ref: '#/components/examples/404Example' - '500': - description: "An unexpected error occurred on the server" - headers: - X-Correlation-ID: - $ref: '#/components/headers/correlatedResponseHeader' - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - examples: - 500Example: - $ref: '#/components/examples/500Example' /event/device/name/{name}: - parameters: - - $ref: '#/components/parameters/correlatedRequestHeader' - - name: name - in: path - required: true - schema: - type: string - description: "Uniquely identifies a given device" - - $ref: '#/components/parameters/offsetParam' - - $ref: '#/components/parameters/limitParam' get: summary: "Given the entire range of events sorted by created descending, returns a portion of that range according to the device name, offset and limit parameters." + parameters: + - $ref: '#/components/parameters/correlatedRequestHeader' + - name: name + in: path + required: true + schema: + type: string + description: "Uniquely identifies a given device" + - $ref: '#/components/parameters/offsetParam' + - $ref: '#/components/parameters/limitParam' responses: '200': description: "OK" @@ -987,13 +938,13 @@ paths: apiVersion: "v2" statusCode: 200 message: "" - events: + events: - apiVersion: "v2" created: 1594877691305 deviceName: "device-002" id: "73fc4f9c-2d64-4920-addb-b1f33a8f8514" origin: 1602168089665565300 - readings: + readings: - created: 1594879337014 deviceName: "device-002" id: "71c601d9-cb56-453a-8c75-54461e444713" @@ -1028,7 +979,7 @@ paths: application/json: schema: type: array - items: + items: $ref: '#/components/schemas/ErrorResponse' examples: 404Example: @@ -1042,11 +993,46 @@ paths: application/json: schema: type: array - items: + items: $ref: '#/components/schemas/ErrorResponse' examples: 500Example: $ref: '#/components/examples/500Example' + delete: + summary: "Deletes all events for the specified device" + parameters: + - $ref: '#/components/parameters/correlatedRequestHeader' + - name: name + in: path + required: true + schema: + type: string + description: "Uniquely identifies a given device" + responses: + '202': + description: "Delete request accepted" + headers: + X-Correlation-ID: + $ref: '#/components/headers/correlatedResponseHeader' + content: + application/json: + schema: + $ref: '#/components/schemas/BaseResponse' + examples: + 200Example: + $ref: '#/components/examples/200Example' + '500': + description: "An unexpected error occurred on the server" + headers: + X-Correlation-ID: + $ref: '#/components/headers/correlatedResponseHeader' + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + examples: + 500Example: + $ref: '#/components/examples/500Example' /event/start/{start}/end/{end}: parameters: - $ref: '#/components/parameters/correlatedRequestHeader'