Skip to content

Commit c867859

Browse files
committed
Fix heartbeat service
1 parent fdbb489 commit c867859

11 files changed

Lines changed: 429 additions & 14 deletions

api/go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ require (
1919
github.com/joho/godotenv v1.4.0
2020
github.com/nyaruka/phonenumbers v1.1.0
2121
github.com/palantir/stacktrace v0.0.0-20161112013806-78658fd2d177
22+
github.com/pkg/errors v0.9.1
2223
github.com/rs/zerolog v1.26.1
2324
github.com/swaggo/swag v1.8.2
2425
github.com/thedevsaddam/govalidator v1.9.10

api/pkg/entities/heartbeat.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@ import (
66
"github.com/google/uuid"
77
)
88

9-
// Heartbeat represents a message sent between 2 phone numbers
9+
// Heartbeat represents is a pulse from an active phone
1010
type Heartbeat struct {
1111
ID uuid.UUID `json:"id" gorm:"primaryKey;type:uuid;" example:"32343a19-da5e-4b1b-a767-3298a73703cb"`
1212
Owner string `json:"owner" gorm:"index:idx_heartbeats_owner_timestamp" example:"+18005550199"`
13-
UserID UserID `json:"userID" example:"WB7DRDWrJZRGbYrv2CKGkqbzvqdC"`
13+
UserID UserID `json:"user_id" example:"WB7DRDWrJZRGbYrv2CKGkqbzvqdC"`
1414
Timestamp time.Time `json:"timestamp" gorm:"index:idx_heartbeats_owner_timestamp" example:"2022-06-05T14:26:01.520828+03:00"`
1515
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package entities
2+
3+
import (
4+
"github.com/google/uuid"
5+
)
6+
7+
// HeartbeatMonitor is used to monitor heartbeats of a phone
8+
type HeartbeatMonitor struct {
9+
ID uuid.UUID `json:"id" gorm:"primaryKey;type:uuid;" example:"32343a19-da5e-4b1b-a767-3298a73703cb"`
10+
PhoneID uuid.UUID `json:"phone_id" example:"32343a19-da5e-4b1b-a767-3298a73703cb"`
11+
UserID UserID `json:"user_id" example:"WB7DRDWrJZRGbYrv2CKGkqbzvqdC"`
12+
Owner string `json:"owner" example:"+18005550199"`
13+
}

api/pkg/entities/user.go

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,11 @@ type UserID string
1111

1212
// User stores information about a user
1313
type User struct {
14-
ID UserID `json:"id" gorm:"primaryKey;type:string;" example:"WB7DRDWrJZRGbYrv2CKGkqbzvqdC"`
15-
Email string `json:"email" example:"name@email.com"` // gorm:"uniqueIndex"
16-
APIKey string `json:"api_key" example:"xyz"` // gorm:"uniqueIndex"
17-
ActivePhoneID *uuid.UUID `json:"active_phone_id" gorm:"type:uuid;" example:"32343a19-da5e-4b1b-a767-3298a73703cb"`
18-
CreatedAt time.Time `json:"created_at" example:"2022-06-05T14:26:02.302718+03:00"`
19-
UpdatedAt time.Time `json:"updated_at" example:"2022-06-05T14:26:10.303278+03:00"`
14+
ID UserID `json:"id" gorm:"primaryKey;type:string;" example:"WB7DRDWrJZRGbYrv2CKGkqbzvqdC"`
15+
Email string `json:"email" example:"name@email.com"` // gorm:"uniqueIndex"
16+
APIKey string `json:"api_key" example:"xyz"` // gorm:"uniqueIndex"
17+
ActivePhoneID *uuid.UUID `json:"active_phone_id" gorm:"type:uuid;" example:"32343a19-da5e-4b1b-a767-3298a73703cb"`
18+
NotificationEmail *string `json:"notification_email" example:"john@example.com"`
19+
CreatedAt time.Time `json:"created_at" example:"2022-06-05T14:26:02.302718+03:00"`
20+
UpdatedAt time.Time `json:"updated_at" example:"2022-06-05T14:26:10.303278+03:00"`
2021
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package events
2+
3+
import (
4+
"time"
5+
6+
"github.com/NdoleStudio/httpsms/pkg/entities"
7+
"github.com/google/uuid"
8+
)
9+
10+
// EventTypePhoneHeartbeatCheck is emitted when the phone is missing a heartbeat
11+
const EventTypePhoneHeartbeatCheck = "phone.heartbeat.check"
12+
13+
// PhoneHeartbeatCheckPayload is the payload of the EventTypePhoneHeartbeatCheck event
14+
type PhoneHeartbeatCheckPayload struct {
15+
PhoneID uuid.UUID `json:"phone_id"`
16+
UserID entities.UserID `json:"user_id"`
17+
ScheduledAt time.Time `json:"scheduled_at"`
18+
Owner string `json:"owner"`
19+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package events
2+
3+
import (
4+
"time"
5+
6+
"github.com/NdoleStudio/httpsms/pkg/entities"
7+
"github.com/google/uuid"
8+
)
9+
10+
// EventTypePhoneHeartbeatDead is emitted when the phone is missing a heartbeat
11+
const EventTypePhoneHeartbeatDead = "phone.heartbeat.dead"
12+
13+
// PhoneHeartbeatDeadPayload is the payload of the EventTypePhoneHeartbeatDead event
14+
type PhoneHeartbeatDeadPayload struct {
15+
PhoneID uuid.UUID `json:"phone_id"`
16+
UserID entities.UserID `json:"user_id"`
17+
Timestamp time.Time `json:"timestamp"`
18+
Owner string `json:"owner"`
19+
}
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
package repositories
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/pkg/errors"
8+
9+
"github.com/NdoleStudio/httpsms/pkg/entities"
10+
"github.com/NdoleStudio/httpsms/pkg/telemetry"
11+
"github.com/palantir/stacktrace"
12+
"gorm.io/gorm"
13+
)
14+
15+
// gormHeartbeatRepository is responsible for persisting entities.Heartbeat
16+
type gormHeartbeatMonitorRepository struct {
17+
logger telemetry.Logger
18+
tracer telemetry.Tracer
19+
db *gorm.DB
20+
}
21+
22+
func (repository *gormHeartbeatMonitorRepository) Delete(ctx context.Context, userID entities.UserID, owner string) error {
23+
ctx, span := repository.tracer.Start(ctx)
24+
defer span.End()
25+
26+
err := repository.db.WithContext(ctx).
27+
Where("user_id = ?", userID).
28+
Where("owner = ?", owner).
29+
Delete(&entities.HeartbeatMonitor{}).Error
30+
if err != nil {
31+
msg := fmt.Sprintf("cannot delete heartbeat monitor with owner [%s] and userID [%s]", owner, userID)
32+
return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
33+
}
34+
35+
return nil
36+
}
37+
38+
// NewGormHeartbeatMonitorRepository creates the GORM version of the HeartbeatMonitorRepository
39+
func NewGormHeartbeatMonitorRepository(
40+
logger telemetry.Logger,
41+
tracer telemetry.Tracer,
42+
db *gorm.DB,
43+
) HeartbeatMonitorRepository {
44+
return &gormHeartbeatMonitorRepository{
45+
logger: logger.WithService(fmt.Sprintf("%T", &gormHeartbeatRepository{})),
46+
tracer: tracer,
47+
db: db,
48+
}
49+
}
50+
51+
// Index entities.Message between 2 parties
52+
func (repository *gormHeartbeatMonitorRepository) Index(ctx context.Context, userID entities.UserID, owner string, params IndexParams) (*[]entities.Heartbeat, error) {
53+
ctx, span := repository.tracer.Start(ctx)
54+
defer span.End()
55+
56+
query := repository.db.WithContext(ctx).Where("user_id = ?", userID).Where("owner = ?", owner)
57+
if len(params.Query) > 0 {
58+
queryPattern := "%" + params.Query + "%"
59+
query.Where("quantity ILIKE ?", queryPattern)
60+
}
61+
62+
heartbeats := new([]entities.Heartbeat)
63+
if err := query.Order("timestamp DESC").Limit(params.Limit).Offset(params.Skip).Find(&heartbeats).Error; err != nil {
64+
msg := fmt.Sprintf("cannot fetch heartbeats with owner [%s] and params [%+#v]", owner, params)
65+
return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
66+
}
67+
68+
return heartbeats, nil
69+
}
70+
71+
// Store a new heartbeat monitor
72+
func (repository *gormHeartbeatMonitorRepository) Store(ctx context.Context, heartbeatMonitor *entities.HeartbeatMonitor) error {
73+
ctx, span := repository.tracer.Start(ctx)
74+
defer span.End()
75+
76+
if err := repository.db.WithContext(ctx).Create(heartbeatMonitor).Error; err != nil {
77+
msg := fmt.Sprintf("cannot save heartbeatMonitor monitor with ID [%s]", heartbeatMonitor.ID)
78+
return repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
79+
}
80+
81+
return nil
82+
}
83+
84+
// Load a heartbeat monitor by userID and owner
85+
func (repository *gormHeartbeatMonitorRepository) Load(ctx context.Context, userID entities.UserID, owner string) (*entities.HeartbeatMonitor, error) {
86+
ctx, span := repository.tracer.Start(ctx)
87+
defer span.End()
88+
89+
phone := new(entities.HeartbeatMonitor)
90+
err := repository.db.WithContext(ctx).
91+
Where("user_id = ?", userID).
92+
Where("owner = ?", owner).
93+
First(&phone).Error
94+
95+
if errors.Is(err, gorm.ErrRecordNotFound) {
96+
msg := fmt.Sprintf("heartbeat monitor with userID [%s] and owner [%s] does not exist", userID, owner)
97+
return nil, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, ErrCodeNotFound, msg))
98+
}
99+
100+
if err != nil {
101+
msg := fmt.Sprintf("cannot load heartbeat monitor with userID [%s] and owner [%s]", userID, owner)
102+
return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
103+
}
104+
105+
return phone, nil
106+
}
107+
108+
// Exists checks of a heartbeat monitor exists for the userID and owner
109+
func (repository *gormHeartbeatMonitorRepository) Exists(ctx context.Context, userID entities.UserID, owner string) (bool, error) {
110+
ctx, span := repository.tracer.Start(ctx)
111+
defer span.End()
112+
113+
var exists bool
114+
err := repository.db.WithContext(ctx).
115+
Model(&entities.HeartbeatMonitor{}).
116+
Select("count(*) > 0").
117+
Where("user_id = ?", userID).
118+
Where("owner = ?", owner).
119+
Find(&exists).Error
120+
if err != nil {
121+
msg := fmt.Sprintf("cannot check if heartbeat monitor exists with userID [%s] and owner [%s]", userID, owner)
122+
return exists, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
123+
}
124+
125+
return exists, nil
126+
}

api/pkg/repositories/gorm_heartbeat_repository.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import (
44
"context"
55
"fmt"
66

7+
"github.com/pkg/errors"
8+
79
"github.com/NdoleStudio/httpsms/pkg/entities"
810
"github.com/NdoleStudio/httpsms/pkg/telemetry"
911
"github.com/palantir/stacktrace"
@@ -17,6 +19,29 @@ type gormHeartbeatRepository struct {
1719
db *gorm.DB
1820
}
1921

22+
func (repository *gormHeartbeatRepository) Last(ctx context.Context, userID entities.UserID, owner string) (*entities.Heartbeat, error) {
23+
ctx, span := repository.tracer.Start(ctx)
24+
defer span.End()
25+
26+
heartbeat := new(entities.Heartbeat)
27+
err := repository.db.WithContext(ctx).
28+
Where("user_id = ?", userID).
29+
Where("owner = ?", owner).
30+
Order("timestamp DESC").
31+
First(&heartbeat).Error
32+
if errors.Is(err, gorm.ErrRecordNotFound) {
33+
msg := fmt.Sprintf("heartbeat with userID [%s] and owner [%s] does not exist", userID, owner)
34+
return nil, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, ErrCodeNotFound, msg))
35+
}
36+
37+
if err != nil {
38+
msg := fmt.Sprintf("cannot load heartbeat with userID [%s] and owner [%s]", userID, owner)
39+
return nil, repository.tracer.WrapErrorSpan(span, stacktrace.Propagate(err, msg))
40+
}
41+
42+
return heartbeat, nil
43+
}
44+
2045
// NewGormHeartbeatRepository creates the GORM version of the HeartbeatRepository
2146
func NewGormHeartbeatRepository(
2247
logger telemetry.Logger,
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package repositories
2+
3+
import (
4+
"context"
5+
6+
"github.com/NdoleStudio/httpsms/pkg/entities"
7+
)
8+
9+
// HeartbeatMonitorRepository loads and persists an entities.HeartbeatMonitor
10+
type HeartbeatMonitorRepository interface {
11+
// Store a new entities.HeartbeatMonitor
12+
Store(ctx context.Context, heartbeat *entities.HeartbeatMonitor) error
13+
14+
// Load a phone by user and phone number
15+
Load(ctx context.Context, userID entities.UserID, phoneNumber string) (*entities.HeartbeatMonitor, error)
16+
17+
// Exists checks if a heartbeat monitor exists for a phone number
18+
Exists(ctx context.Context, userID entities.UserID, phoneNumber string) (bool, error)
19+
20+
// Delete an entities.HeartbeatMonitor
21+
Delete(ctx context.Context, userID entities.UserID, phoneNumber string) error
22+
}

api/pkg/repositories/heartbeat_repository.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,7 @@ type HeartbeatRepository interface {
1313

1414
// Index entities.Heartbeat of an owner
1515
Index(ctx context.Context, userID entities.UserID, owner string, params IndexParams) (*[]entities.Heartbeat, error)
16+
17+
// Last entities.Heartbeat returns the last heartbeat
18+
Last(ctx context.Context, userID entities.UserID, owner string) (*entities.Heartbeat, error)
1619
}

0 commit comments

Comments
 (0)