Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,3 @@ A highly scalable, performant and distributed durable timer service
* [x] DynamoDB
* [x] MySQL
* [x] PostgreSQL
* [ ] TiDB: TODO
* [ ] MS SQL Server: TODO
* [ ] Oracle DB: TODO
6 changes: 3 additions & 3 deletions api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ components:
callbackUrl:
type: string
format: uri
description: HTTP URL to call when the timer executes, returning 200 with CallbackResponse means success, otherwise will be retried.
description: HTTP URL to call when the timer executes, returning 200 with CallbackResponse means success, 4xx means invalid timer and no retry, otherwise will be retried.
example: "https://api.example.com/webhooks/timer"
maxLength: 2048
payload:
Expand Down Expand Up @@ -283,7 +283,7 @@ components:
callbackUrl:
type: string
format: uri
description: New callback URL, returning 200 with CallbackResponse means success, otherwise will be retried.
description: New callback URL, returning 200 with CallbackResponse means success, 4xx means invalid timer and no retry, otherwise will be retried.
example: "https://api.example.com/webhooks/timer-updated"
maxLength: 2048
payload:
Expand Down Expand Up @@ -322,7 +322,7 @@ components:
callbackUrl:
type: string
format: uri
description: HTTP URL to call when executing, returning 200 with CallbackResponse means success, otherwise will be retried.
description: HTTP URL to call when executing, returning 200 with CallbackResponse means success, 4xx means invalid timer and no retry, otherwise will be retried.
example: "https://api.example.com/webhooks/timer"
payload:
type: object
Expand Down
25 changes: 10 additions & 15 deletions docker/dev-all.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -95,24 +95,19 @@ services:
mongodb:
image: mongo:7
container_name: timer-service-mongodb-dev
command: ["mongod", "--replSet", "timer-rs", "--bind_ip_all", "--keyFile", "/etc/mongodb-keyfile", "--auth"]
environment:
MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: mongodb_root_password
MONGO_INITDB_DATABASE: timer_service
ports:
- "27017:27017"
volumes:
- mongodb_data:/data/db
- ./scripts/mongodb-keyfile:/etc/mongodb-keyfile:ro
networks:
- timer-network
command: ["mongod", "--replSet", "timer-rs", "--bind_ip_all", "--noauth"]
healthcheck:
test: ["CMD", "mongosh", "--quiet", "--eval", "db.runCommand('ping').ok"]
timeout: 20s
retries: 10
retries: 15
interval: 10s
start_period: 40s
start_period: 60s

mongodb-replica-init:
image: mongo:7
Expand All @@ -124,20 +119,20 @@ services:
entrypoint: >
sh -c "
echo 'Waiting for MongoDB to be ready...' &&
until mongosh --host mongodb --username root --password mongodb_root_password --authenticationDatabase admin --eval 'db.runCommand({ping:1})' --quiet; do
until mongosh --host mongodb --eval 'db.runCommand({ping:1})' --quiet; do
echo 'MongoDB not ready, waiting...'
sleep 2
done &&
echo 'Initializing replica set...' &&
mongosh --host mongodb --username root --password mongodb_root_password --authenticationDatabase admin --eval '
mongosh --host mongodb --eval '
try {
result = rs.initiate({
rs.initiate({
_id: \"timer-rs\",
members: [
{ _id: 0, host: \"mongodb:27017\" }
]
});
print(\"Replica set initiation result:\", JSON.stringify(result));
print(\"Replica set initialized successfully\");
} catch (e) {
if (e.code === 23 || e.message.includes(\"already initialized\")) {
print(\"Replica set already initialized\");
Expand All @@ -147,12 +142,12 @@ services:
}
}
' &&
echo 'Waiting for replica set to be ready...' &&
until mongosh --host mongodb --username root --password mongodb_root_password --authenticationDatabase admin --eval 'rs.status().ok' --quiet; do
echo 'Waiting for replica set to become ready...' &&
until mongosh --host mongodb --eval 'rs.status().ok' --quiet; do
echo 'Replica set not ready, waiting...'
sleep 2
done &&
echo 'Replica set initialized successfully!'
echo 'MongoDB replica set setup completed successfully!'
"

mongodb-init:
Expand Down
57 changes: 22 additions & 35 deletions docker/dev-mongodb.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,19 @@ services:
mongodb:
image: mongo:7
container_name: timer-service-mongodb-dev
command: ["mongod", "--replSet", "timer-rs", "--bind_ip_all", "--keyFile", "/etc/mongodb-keyfile", "--auth"]
environment:
MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: mongodb_root_password
MONGO_INITDB_DATABASE: timer_service
ports:
- "27017:27017"
volumes:
- mongodb_data:/data/db
- ./scripts/mongodb-keyfile:/etc/mongodb-keyfile:ro
networks:
- timer-network
command: ["mongod", "--replSet", "timer-rs", "--bind_ip_all", "--noauth"]
healthcheck:
test: ["CMD", "mongosh", "--quiet", "--eval", "db.runCommand('ping').ok"]
timeout: 20s
retries: 10
retries: 15
interval: 10s
start_period: 40s
start_period: 60s

mongodb-replica-init:
image: mongo:7
Expand All @@ -33,20 +28,20 @@ services:
entrypoint: >
sh -c "
echo 'Waiting for MongoDB to be ready...' &&
until mongosh --host mongodb --username root --password mongodb_root_password --authenticationDatabase admin --eval 'db.runCommand({ping:1})' --quiet; do
until mongosh --host mongodb --eval 'db.runCommand({ping:1})' --quiet; do
echo 'MongoDB not ready, waiting...'
sleep 2
done &&
echo 'Initializing replica set...' &&
mongosh --host mongodb --username root --password mongodb_root_password --authenticationDatabase admin --eval '
mongosh --host mongodb --eval '
try {
result = rs.initiate({
rs.initiate({
_id: \"timer-rs\",
members: [
{ _id: 0, host: \"mongodb:27017\" }
]
});
print(\"Replica set initiation result:\", JSON.stringify(result));
print(\"Replica set initialized successfully\");
} catch (e) {
if (e.code === 23 || e.message.includes(\"already initialized\")) {
print(\"Replica set already initialized\");
Expand All @@ -56,12 +51,12 @@ services:
}
}
' &&
echo 'Waiting for replica set to be ready...' &&
until mongosh --host mongodb --username root --password mongodb_root_password --authenticationDatabase admin --eval 'rs.status().ok' --quiet; do
echo 'Waiting for replica set to become ready...' &&
until mongosh --host mongodb --eval 'rs.status().ok' --quiet; do
echo 'Replica set not ready, waiting...'
sleep 2
done &&
echo 'Replica set initialized successfully!'
echo 'MongoDB replica set setup completed successfully!'
"

mongodb-init:
Expand All @@ -75,31 +70,23 @@ services:
- timer-network
entrypoint: >
sh -c "
echo 'Waiting for replica set to be ready...' &&
until mongosh --host mongodb --username root --password mongodb_root_password --authenticationDatabase admin --eval 'rs.status().ok && db.runCommand({ping:1}).ok' --quiet; do
echo 'Replica set not ready, waiting...'
echo 'Waiting for MongoDB to be ready...' &&
until mongosh --host mongodb --eval 'db.runCommand({ping:1}).ok' --quiet; do
echo 'MongoDB not ready, waiting...'
sleep 2
done &&
echo 'Creating timer_service database and user...' &&
mongosh --host mongodb --username root --password mongodb_root_password --authenticationDatabase admin --eval '
mongosh --host mongodb --eval '
use timer_service;
try {
db.createUser({
user: \"timer_user\",
pwd: \"timer_password\",
roles: [{role: \"readWrite\", db: \"timer_service\"}]
});
print(\"User timer_user created successfully\");
} catch (e) {
if (e.code === 51003) {
print(\"User timer_user already exists\");
} else {
throw e;
}
}
db.createUser({
user: \"timer_user\",
pwd: \"timer_password\",
roles: [{role: \"readWrite\", db: \"timer_service\"}]
});
print(\"User timer_user created successfully\");
' &&
echo 'Executing schema with root user...' &&
mongosh --host mongodb --username root --password mongodb_root_password --authenticationDatabase admin timer_service < /schema/v1.js &&
echo 'Executing schema from v1.js...' &&
mongosh --host mongodb timer_service < /schema/v1.js &&
echo 'Schema execution completed successfully!'
"

Expand Down
4 changes: 4 additions & 0 deletions docs/design/api-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ This document describes the design decisions and rationale behind the Distribute

### Callback Response Protocol

Return 4xx means invalid timer and no retry.
Return 200 with ok == true means success and no retry.
Otherwise will retry, if retryPolicy is provided on timer creation.

**CallbackResponse Schema**:
```json
{
Expand Down
15 changes: 6 additions & 9 deletions server/databases/mongodb/mongodb_test_util.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ const (
testHost = "localhost"
testPort = 27017
testDatabase = "timer_service_test"
testUsername = "root"
testPassword = "mongodb_root_password"
testAuthDatabase = "admin"
testUsername = "" // No authentication in test setup
testPassword = "" // No authentication in test setup
testAuthDatabase = "" // No authentication in test setup
)

func getTestHost() string {
Expand Down Expand Up @@ -54,9 +54,6 @@ func executeSchemaFileWithMongosh() error {

// Use docker exec to run mongosh inside the MongoDB container
cmd := exec.Command("docker", "exec", "timer-service-mongodb-dev", "mongosh",
"--username", testUsername,
"--password", testPassword,
"--authenticationDatabase", testAuthDatabase,
testDatabase,
"--eval", string(contentBytes))

Expand Down Expand Up @@ -106,9 +103,9 @@ func setupTestStore(t *testing.T) (*MongoDBTimerStore, func()) {
}

func createTestDatabase() error {
// Connect as root user with direct connection for localhost testing
uri := fmt.Sprintf("mongodb://%s:%s@%s:%d/?authSource=%s&directConnection=true",
testUsername, testPassword, getTestHost(), testPort, testAuthDatabase)
// Connect without authentication for development/CI setup
uri := fmt.Sprintf("mongodb://%s:%d/?directConnection=true",
getTestHost(), testPort)

client, err := mongo.Connect(context.Background(), options.Client().ApplyURI(uri))
if err != nil {
Expand Down
22 changes: 18 additions & 4 deletions server/databases/mongodb/mongodb_timer_store_impl.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,28 @@ type MongoDBTimerStore struct {
func NewMongoDBTimerStore(config *config.MongoDBConnectConfig) (databases.TimerStore, error) {
// Build connection URI - use replica set config only for non-localhost connections
var uri string

// Check if authentication is configured
hasAuth := config.Username != "" && config.Password != "" && config.AuthDatabase != ""

if config.Host == "localhost" || config.Host == "127.0.0.1" {
// For localhost connections (testing), use direct connection to bypass replica set discovery
uri = fmt.Sprintf("mongodb://%s:%s@%s:%d/%s?authSource=%s&directConnection=true",
config.Username, config.Password, config.Host, config.Port, config.Database, config.AuthDatabase)
if hasAuth {
uri = fmt.Sprintf("mongodb://%s:%s@%s:%d/%s?authSource=%s&directConnection=true",
config.Username, config.Password, config.Host, config.Port, config.Database, config.AuthDatabase)
} else {
uri = fmt.Sprintf("mongodb://%s:%d/%s?directConnection=true",
config.Host, config.Port, config.Database)
}
} else {
// For production, use replica set configuration for transactions
uri = fmt.Sprintf("mongodb://%s:%s@%s:%d/%s?authSource=%s&replicaSet=timer-rs&readConcern=majority&w=majority",
config.Username, config.Password, config.Host, config.Port, config.Database, config.AuthDatabase)
if hasAuth {
uri = fmt.Sprintf("mongodb://%s:%s@%s:%d/%s?authSource=%s&replicaSet=timer-rs&readConcern=majority&w=majority",
config.Username, config.Password, config.Host, config.Port, config.Database, config.AuthDatabase)
} else {
uri = fmt.Sprintf("mongodb://%s:%d/%s?replicaSet=timer-rs&readConcern=majority&w=majority",
config.Host, config.Port, config.Database)
}
}

// Configure client options
Expand Down