Skip to content

Commit 38e6dd9

Browse files
committed
Add listener for outstanding messages
1 parent 28b8794 commit 38e6dd9

8 files changed

Lines changed: 190 additions & 66 deletions

File tree

api/pkg/entities/message.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,16 @@ type Message struct {
5959

6060
FailureReason *string `json:"failure_reason"`
6161
}
62+
63+
// IsSending determines if a message is being sent
64+
func (message Message) IsSending() bool {
65+
return message.Status == MessageStatusSending
66+
}
67+
68+
// AddSendAttempt configures a Message for sending
69+
func (message *Message) AddSendAttempt(timestamp time.Time) *Message {
70+
message.Status = MessageStatusSending
71+
message.LastAttemptedAt = &timestamp
72+
message.OrderTimestamp = timestamp
73+
return message
74+
}

api/pkg/listeners/listener.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package listeners
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"time"
7+
8+
"github.com/NdoleStudio/http-sms-manager/pkg/entities"
9+
"github.com/NdoleStudio/http-sms-manager/pkg/repositories"
10+
cloudevents "github.com/cloudevents/sdk-go/v2"
11+
"github.com/google/uuid"
12+
)
13+
14+
type listener struct {
15+
repository repositories.EventListenerLogRepository
16+
}
17+
18+
func (listener listener) handlerSignature(handler any, event cloudevents.Event) string {
19+
return fmt.Sprintf("%s.%T", event.Type(), handler)
20+
}
21+
22+
func (listener listener) storeEventListenerLog(ctx context.Context, handler string, event cloudevents.Event) error {
23+
return listener.repository.Store(ctx, &entities.EventListenerLog{
24+
ID: uuid.New(),
25+
EventID: event.ID(),
26+
EventType: event.Type(),
27+
Handler: handler,
28+
Duration: time.Now().Sub(event.Time()),
29+
HandledAt: time.Now().UTC(),
30+
CreatedAt: time.Now().UTC(),
31+
})
32+
}

api/pkg/listeners/message_listener.go

Lines changed: 60 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,21 @@ package listeners
33
import (
44
"context"
55
"fmt"
6-
"time"
76

8-
"github.com/NdoleStudio/http-sms-manager/pkg/entities"
97
"github.com/NdoleStudio/http-sms-manager/pkg/events"
108
"github.com/NdoleStudio/http-sms-manager/pkg/repositories"
119
"github.com/NdoleStudio/http-sms-manager/pkg/services"
1210
"github.com/NdoleStudio/http-sms-manager/pkg/telemetry"
1311
cloudevents "github.com/cloudevents/sdk-go/v2"
14-
"github.com/google/uuid"
1512
"github.com/palantir/stacktrace"
1613
)
1714

1815
// MessageListener handles cloud events which need to update the messages table
1916
type MessageListener struct {
20-
logger telemetry.Logger
21-
tracer telemetry.Tracer
22-
service *services.MessageService
23-
repository repositories.EventListenerLogRepository
17+
listener
18+
logger telemetry.Logger
19+
tracer telemetry.Tracer
20+
service *services.MessageService
2421
}
2522

2623
// NewMessageListener creates a new instance of MessageListener
@@ -29,16 +26,19 @@ func NewMessageListener(
2926
tracer telemetry.Tracer,
3027
service *services.MessageService,
3128
repository repositories.EventListenerLogRepository,
32-
) (listener *MessageListener, routes map[string]events.EventListener) {
33-
listener = &MessageListener{
34-
logger: logger.WithService(fmt.Sprintf("%T", listener)),
35-
tracer: tracer,
36-
service: service,
37-
repository: repository,
29+
) (l *MessageListener, routes map[string]events.EventListener) {
30+
l = &MessageListener{
31+
logger: logger.WithService(fmt.Sprintf("%T", l)),
32+
tracer: tracer,
33+
service: service,
34+
listener: listener{
35+
repository: repository,
36+
},
3837
}
3938

40-
return listener, map[string]events.EventListener{
41-
events.EventTypeMessageAPISent: listener.OnMessageAPISent,
39+
return l, map[string]events.EventListener{
40+
events.EventTypeMessageAPISent: l.OnMessageAPISent,
41+
events.EventTypeMessagePhoneSending: l.OnMessagePhoneSending,
4242
}
4343
}
4444

@@ -47,19 +47,16 @@ func (listener *MessageListener) OnMessageAPISent(ctx context.Context, event clo
4747
ctx, span := listener.tracer.Start(ctx)
4848
defer span.End()
4949

50-
handlerName := fmt.Sprintf("%s.%T", event.Type(), listener)
51-
listener.logger.Warn(stacktrace.NewError(handlerName))
52-
53-
handled, err := listener.repository.Has(ctx, event.ID(), handlerName)
50+
handled, err := listener.repository.Has(ctx, event.ID(), listener.signature(event))
5451
if err != nil {
55-
msg := fmt.Sprintf("cannot test if event [%s] has been handled by [%T]", event.ID(), handlerName)
52+
msg := fmt.Sprintf("cannot verify if event [%s] has been handled by [%T]", event.ID(), listener.signature(event))
5653
return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
5754
}
5855

5956
ctxLogger := listener.tracer.CtxLogger(listener.logger, span)
6057

6158
if handled {
62-
ctxLogger.Info(fmt.Sprintf("event [%s] has already been handled by [%s]", event.ID(), handlerName))
59+
ctxLogger.Info(fmt.Sprintf("event [%s] has already been handled by [%s]", event.ID(), listener.signature(event)))
6360
return nil
6461
}
6562

@@ -83,13 +80,46 @@ func (listener *MessageListener) OnMessageAPISent(ctx context.Context, event clo
8380
return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
8481
}
8582

86-
return listener.repository.Save(ctx, &entities.EventListenerLog{
87-
ID: uuid.New(),
88-
EventID: event.ID(),
89-
EventType: event.Type(),
90-
Handler: handlerName,
91-
Duration: time.Now().Sub(event.Time()),
92-
HandledAt: time.Now().UTC(),
93-
CreatedAt: time.Now().UTC(),
94-
})
83+
return listener.storeEventListenerLog(ctx, listener.signature(event), event)
84+
}
85+
86+
// OnMessagePhoneSending handles the events.EventTypeMessagePhoneSending event
87+
func (listener *MessageListener) OnMessagePhoneSending(ctx context.Context, event cloudevents.Event) error {
88+
ctx, span := listener.tracer.Start(ctx)
89+
defer span.End()
90+
91+
handled, err := listener.repository.Has(ctx, event.ID(), listener.signature(event))
92+
if err != nil {
93+
msg := fmt.Sprintf("cannot verify if event [%s] has been handled by [%T]", event.ID(), listener.signature(event))
94+
return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
95+
}
96+
97+
ctxLogger := listener.tracer.CtxLogger(listener.logger, span)
98+
99+
if handled {
100+
ctxLogger.Info(fmt.Sprintf("event [%s] has already been handled by [%s]", event.ID(), listener.signature(event)))
101+
return nil
102+
}
103+
104+
var payload events.MessagePhoneSendingPayload
105+
if err = event.DataAs(&payload); err != nil {
106+
msg := fmt.Sprintf("cannot decode [%s] into [%T]", event.Data(), payload)
107+
return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
108+
}
109+
110+
handleParams := services.HandleMessageSendingParams{
111+
ID: payload.ID,
112+
Timestamp: event.Time(),
113+
}
114+
115+
if err = listener.service.HandleMessageSending(ctx, handleParams); err != nil {
116+
msg := fmt.Sprintf("cannot handle sending for message with ID [%s] for event with ID [%s]", handleParams.ID, event.ID())
117+
return listener.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
118+
}
119+
120+
return listener.storeEventListenerLog(ctx, listener.signature(event), event)
121+
}
122+
123+
func (listener *MessageListener) signature(event cloudevents.Event) string {
124+
return listener.handlerSignature(listener, event)
95125
}

api/pkg/repositories/event_listener_log_repository.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import (
99
// EventListenerLogRepository loads and persists an entities.EventListenerLog
1010
type EventListenerLogRepository interface {
1111
// Save a new entities.EventListenerLog
12-
Save(ctx context.Context, log *entities.EventListenerLog) error
12+
Store(ctx context.Context, log *entities.EventListenerLog) error
1313

1414
// Has verifies that the listener has not already been called
1515
Has(ctx context.Context, eventID string, handler string) (bool, error)

api/pkg/repositories/gorm_event_listener_log_repository.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ func NewGormEventListenerLogRepository(
3131
}
3232

3333
// Save a new entities.Message
34-
func (repository *gormEventListenerLogRepository) Save(ctx context.Context, message *entities.EventListenerLog) error {
34+
func (repository *gormEventListenerLogRepository) Store(ctx context.Context, message *entities.EventListenerLog) error {
3535
ctx, span := repository.tracer.Start(ctx)
3636
defer span.End()
3737

api/pkg/repositories/gorm_message_repository.go

Lines changed: 44 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -21,35 +21,6 @@ type gormMessageRepository struct {
2121
db *gorm.DB
2222
}
2323

24-
// GetOutstanding fetches messages that still to be sent to the phone
25-
func (repository *gormMessageRepository) GetOutstanding(ctx context.Context, take int) (*[]entities.Message, error) {
26-
ctx, span := repository.tracer.Start(ctx)
27-
defer span.End()
28-
29-
messages := new([]entities.Message)
30-
err := crdbgorm.ExecuteTx(ctx, repository.db, nil,
31-
func(tx *gorm.DB) error {
32-
return tx.Model(messages).
33-
Clauses(clause.Returning{}).
34-
Where(
35-
"id IN (?)",
36-
tx.Model(&entities.Message{}).
37-
Where("status = ?", entities.MessageStatusPending).
38-
Order("request_received_at ASC").
39-
Select("id").
40-
Limit(take),
41-
).
42-
Update("status", "sending").Error
43-
},
44-
)
45-
if err != nil {
46-
msg := fmt.Sprintf("cannot fetch [%d] outstanding messages", take)
47-
return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
48-
}
49-
50-
return messages, nil
51-
}
52-
5324
// NewGormMessageRepository creates the GORM version of the MessageRepository
5425
func NewGormMessageRepository(
5526
logger telemetry.Logger,
@@ -63,8 +34,8 @@ func NewGormMessageRepository(
6334
}
6435
}
6536

66-
// Save a new entities.Message
67-
func (repository *gormMessageRepository) Save(ctx context.Context, message *entities.Message) error {
37+
// Store a new entities.Message
38+
func (repository *gormMessageRepository) Store(ctx context.Context, message *entities.Message) error {
6839
ctx, span := repository.tracer.Start(ctx)
6940
defer span.End()
7041

@@ -89,3 +60,45 @@ func (repository *gormMessageRepository) Load(ctx context.Context, messageID uui
8960

9061
return message, nil
9162
}
63+
64+
// Update an entities.Message
65+
func (repository *gormMessageRepository) Update(ctx context.Context, message *entities.Message) error {
66+
ctx, span := repository.tracer.Start(ctx)
67+
defer span.End()
68+
69+
if err := repository.db.Save(message).Error; err != nil {
70+
msg := fmt.Sprintf("cannot update message with ID [%s]", message.ID)
71+
return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
72+
}
73+
74+
return nil
75+
}
76+
77+
// GetOutstanding fetches messages that still to be sent to the phone
78+
func (repository *gormMessageRepository) GetOutstanding(ctx context.Context, take int) (*[]entities.Message, error) {
79+
ctx, span := repository.tracer.Start(ctx)
80+
defer span.End()
81+
82+
messages := new([]entities.Message)
83+
err := crdbgorm.ExecuteTx(ctx, repository.db, nil,
84+
func(tx *gorm.DB) error {
85+
return tx.Model(messages).
86+
Clauses(clause.Returning{}).
87+
Where(
88+
"id IN (?)",
89+
tx.Model(&entities.Message{}).
90+
Where("status = ?", entities.MessageStatusPending).
91+
Order("request_received_at ASC").
92+
Select("id").
93+
Limit(take),
94+
).
95+
Update("status", "sending").Error
96+
},
97+
)
98+
if err != nil {
99+
msg := fmt.Sprintf("cannot fetch [%d] outstanding messages", take)
100+
return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
101+
}
102+
103+
return messages, nil
104+
}

api/pkg/repositories/message_repository.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,11 @@ import (
99

1010
// MessageRepository loads and persists an entities.Message
1111
type MessageRepository interface {
12-
// Save a new entities.Message
13-
Save(ctx context.Context, message *entities.Message) error
12+
// Store a new entities.Message
13+
Store(ctx context.Context, message *entities.Message) error
14+
15+
// Update a new entities.Message
16+
Update(ctx context.Context, message *entities.Message) error
1417

1518
// Load an entities.Message by ID
1619
Load(ctx context.Context, messageID uuid.UUID) (*entities.Message, error)

api/pkg/services/message_service.go

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ func (service *MessageService) StoreMessage(ctx context.Context, params MessageS
199199
ReceivedAt: nil,
200200
}
201201

202-
if err := service.repository.Save(ctx, message); err != nil {
202+
if err := service.repository.Store(ctx, message); err != nil {
203203
msg := fmt.Sprintf("cannot save message with id [%s]", params.ID)
204204
return nil, service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
205205
}
@@ -208,6 +208,39 @@ func (service *MessageService) StoreMessage(ctx context.Context, params MessageS
208208
return message, nil
209209
}
210210

211+
// HandleMessageSendingParams are parameters for registering a new message being sent
212+
type HandleMessageSendingParams struct {
213+
ID uuid.UUID
214+
Timestamp time.Time
215+
}
216+
217+
// HandleMessageSending handles when a message is being sent
218+
func (service *MessageService) HandleMessageSending(ctx context.Context, params HandleMessageSendingParams) error {
219+
ctx, span := service.tracer.Start(ctx)
220+
defer span.End()
221+
222+
ctxLogger := service.tracer.CtxLogger(service.logger, span)
223+
224+
message, err := service.repository.Load(ctx, params.ID)
225+
if err != nil {
226+
msg := fmt.Sprintf("cannot find message with id [%s]", params.ID)
227+
return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
228+
}
229+
230+
if !message.IsSending() {
231+
msg := fmt.Sprintf("message has wrong status [%s]. expected %s", message.Status, entities.MessageStatusSending)
232+
return service.tracer.WrapErrorSpan(span, stacktrace.NewError(msg))
233+
}
234+
235+
if err = service.repository.Update(ctx, message.AddSendAttempt(params.Timestamp)); err != nil {
236+
msg := fmt.Sprintf("cannot update message with id [%s] after sending", message.ID)
237+
return service.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
238+
}
239+
240+
ctxLogger.Info(fmt.Sprintf("message with id [%s] in the repository after adding send attempt", message.ID))
241+
return nil
242+
}
243+
211244
func (service *MessageService) createMessageAPISentEvent(source string, payload events.MessageAPISentPayload) (cloudevents.Event, error) {
212245
return service.createEvent(events.EventTypeMessageAPISent, source, payload)
213246
}

0 commit comments

Comments
 (0)