Feat: add prometheus compatible endpoint, seed fallback store and add env.example

This commit is contained in:
Ayobami
2025-08-13 22:41:37 +01:00
parent 3f8f456eef
commit da78487047
10 changed files with 987 additions and 268 deletions
+34
View File
@@ -0,0 +1,34 @@
# Environment Configuration for Ticket Microservice
# Server Configuration
PORT=3049
NODE_ENV=development
# Redis Configuration
REDIS_URL=redis://localhost:6379
# Logging Configuration
LOG_LEVEL=info
LOG_FILE=logs/app.log
# PDF Configuration
PDF_OUTPUT_DIR=tickets
PDF_CLEANUP_MAX_AGE_HOURS=24
# Load Testing Configuration
TEST_URL=http://localhost:3049
# Prometheus Configuration (if using monitoring profile)
PROMETHEUS_PORT=9090
GRAFANA_PORT=3000
# Optional: Custom Redis Configuration
# REDIS_HOST=localhost
# REDIS_PORT=6379
# REDIS_PASSWORD=
# REDIS_DB=0
# Optional: Performance Tuning
# MAX_CONCURRENT_REQUESTS=1000
# REQUEST_TIMEOUT_MS=30000
# PDF_GENERATION_TIMEOUT_MS=10000
+105 -17
View File
@@ -47,39 +47,65 @@ Your task is to extract the high-throughput ticket purchasing component and exte
- Docker and Docker Compose
- Git
### Environment Variables
The following environment variables can be configured in your `.env` file:
| Variable | Default | Description |
| --------------------------- | ------------------------ | ---------------------------------------- |
| `PORT` | `3049` | Server port number |
| `NODE_ENV` | `development` | Environment mode |
| `REDIS_URL` | `redis://localhost:6379` | Redis connection string |
| `LOG_LEVEL` | `info` | Logging level (error, warn, info, debug) |
| `LOG_FILE` | `logs/app.log` | Log file path |
| `PDF_OUTPUT_DIR` | `tickets` | Directory for generated PDF tickets |
| `PDF_CLEANUP_MAX_AGE_HOURS` | `24` | Maximum age for PDF cleanup |
| `TEST_URL` | `http://localhost:3049` | Base URL for load testing |
### Quick Start
1. **Clone the repository**
```bash
git clone <repository-url>
cd module4_backend_project
```
2. **Install dependencies**
```bash
npm install
```
3. **Set up environment**
```bash
cp .env.example .env
cp env.example .env
# Edit .env file if needed
```
> **Note**: The `env.example` file contains default configuration values. Copy it to `.env` and modify as needed for your environment.
> **Important**: Create the `logs` directory if you want to use file logging: `mkdir logs`
4. **Start with Docker (Recommended)**
```bash
# Start core services (Redis + App)
docker-compose up -d
# Or start with monitoring (Prometheus + Grafana)
docker-compose --profile monitoring up -d
```
> **Note**: For Docker deployment, you can also set environment variables directly in `docker-compose.yml` or use the `.env` file for local development.
5. **Seed the database**
```bash
# Seed 5 events with 10,000 tickets each
npm run seed
# Custom seeding: 3 events with 5,000 tickets each
npm run seed 3 5000
```
@@ -88,12 +114,19 @@ Your task is to extract the high-throughput ticket purchasing component and exte
If you prefer to run components separately:
1. **Start Redis**
1. **Create necessary directories**
```bash
mkdir -p logs tickets
```
2. **Start Redis**
```bash
docker-compose up -d redis
```
2. **Start the application**
3. **Start the application**
```bash
npm run dev # Development with auto-reload
# or
@@ -104,22 +137,23 @@ If you prefer to run components separately:
Once running, the following endpoints are available:
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/health` | System health check |
| GET | `/events` | List all events with statistics |
| GET | `/events/:eventId` | Get specific event details |
| POST | `/buy/:eventId` | Purchase a ticket for an event |
| GET | `/tickets/:purchaseId` | Download ticket PDF |
| GET | `/metrics` | Real-time system metrics |
| GET | `/admin/pdf-stats` | PDF management statistics |
| POST | `/admin/cleanup-tickets` | Cleanup old ticket files |
| Method | Endpoint | Description |
| ------ | ------------------------ | --------------------------------------- |
| GET | `/health` | System health check |
| GET | `/events` | List all events with statistics |
| GET | `/events/:eventId` | Get specific event details |
| POST | `/buy/:eventId` | Purchase a ticket for an event |
| GET | `/tickets/:purchaseId` | Download ticket PDF |
| GET | `/metrics` | Real-time system metrics |
| GET | `/admin/pdf-stats` | PDF management statistics |
| POST | `/admin/cleanup-tickets` | Cleanup old ticket files |
| POST | `/admin/seed-fallback` | Manually seed fallback store from Redis |
### Load Testing
The system includes a comprehensive load testing framework:
```bash
````bash
# Run full test suite (5000+ concurrent connections)
npm run test:load -- --full
@@ -131,7 +165,9 @@ npm run test:load -- --multi --events 1,2,3 --connections 6000
# Custom load test
node tests/load-test.js --event 2 --connections 1000 --duration 10
```
# Test fallback store functionality
npm run test:fallback
### Monitoring & Metrics
@@ -146,6 +182,22 @@ Grafana dashboard: http://localhost:3000
- Username: `admin`
- Password: `admin`
### Fallback Store Management
The system includes a robust fallback mechanism that automatically activates when Redis is unavailable:
- **Automatic Seeding**: The fallback store is automatically seeded during server startup and when activated
- **Data Synchronization**: When Redis becomes available again, the fallback store can be manually synced
- **Manual Seeding**: Use `/admin/seed-fallback` to manually populate the fallback store from Redis data
```bash
# Manually seed fallback store from Redis
curl -X POST http://localhost:3049/admin/seed-fallback
# Check fallback store status
curl http://localhost:3049/health
````
### Docker Commands
```bash
@@ -174,6 +226,42 @@ docker-compose up -d --build
- **Logging & Metrics:** Proper logging of operations and a functional metrics endpoint suitable for Prometheus scraping.
- **Design Rationale:** The design document (`design.md`) should clearly articulate your architectural decisions, potential bottlenecks, and design solutions.
## Troubleshooting
### Common Issues
1. **Redis Connection Failed**
- Ensure Redis is running: `docker-compose up -d redis`
- Check Redis URL in `.env` file
- Verify Redis port (default: 6379) is not blocked
2. **Port Already in Use**
- Change `PORT` in `.env` file
- Kill process using the port: `lsof -ti:3049 | xargs kill -9`
3. **Missing Dependencies**
- Run `npm install` to install all dependencies
- Ensure Node.js version is 18+ (check with `node --version`)
4. **Permission Denied for Logs/Tickets**
- Create directories with proper permissions: `mkdir -p logs tickets`
- Check file permissions: `chmod 755 logs tickets`
5. **Fallback Store Not Working**
- Ensure Redis is seeded: `npm run seed`
- Check fallback store status: `npm run test:fallback`
- Manually seed fallback: `curl -X POST http://localhost:3049/admin/seed-fallback`
### Getting Help
- Check application logs in the `logs` directory
- Verify Redis connection: `curl http://localhost:3049/health`
- Test fallback store: `npm run test:fallback`
## Final Challenges
- Enhance your docker-compose setup to include a Prometheus container for live monitoring.
+43 -1
View File
@@ -1,6 +1,7 @@
# Ticket Scaling Microservice - Design Document
## Table of Contents
1. [Architecture Overview](#architecture-overview)
2. [System Components](#system-components)
3. [Scalability Strategies](#scalability-strategies)
@@ -15,6 +16,7 @@
## Architecture Overview
### High-Level Architecture
```
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Load Balancer │ │ Prometheus │ │ Grafana │
@@ -38,6 +40,7 @@
```
### Design Principles
1. **High Availability**: Fallback mechanisms ensure service continuity
2. **Atomic Operations**: Redis Lua scripts prevent race conditions
3. **Horizontal Scalability**: Stateless design enables easy scaling
@@ -47,6 +50,7 @@
## System Components
### 1. Core Application (server.js)
- **Technology**: Node.js with Express framework
- **Responsibilities**:
- HTTP request handling
@@ -55,6 +59,7 @@
- PDF generation coordination
### 2. Redis Client (redis-client.js)
- **Technology**: Redis with Lua scripting
- **Responsibilities**:
- Atomic ticket operations
@@ -63,6 +68,7 @@
- Script execution
### 3. Fallback Store (fallback-store.js)
- **Technology**: In-memory JavaScript Map
- **Responsibilities**:
- Emergency ticket storage
@@ -70,6 +76,7 @@
- Graceful degradation
### 4. PDF Generator (pdf-generator.js)
- **Technology**: PDFKit library
- **Responsibilities**:
- Professional ticket generation
@@ -77,6 +84,7 @@
- Cleanup operations
### 5. Logging System (logger.js)
- **Technology**: Winston logging framework
- **Responsibilities**:
- Structured logging
@@ -87,6 +95,7 @@
## Scalability Strategies
### Horizontal Scaling
```
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Instance 1 │ │ Instance 2 │ │ Instance N │
@@ -102,17 +111,20 @@
```
**Key Features**:
- Stateless application design
- Shared Redis backend
- Load balancer distribution
- Independent scaling
### Vertical Scaling
- **CPU**: Multi-core utilization through Node.js cluster mode
- **Memory**: Configurable heap sizes for high-throughput
- **I/O**: Async operations prevent blocking
### Database Scaling
```
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Redis Master │ │ Redis Replica │ │ Redis Replica │
@@ -121,6 +133,7 @@
```
**Strategies**:
- Redis clustering for horizontal scaling
- Read replicas for metrics/stats queries
- Sharding by event ID for massive scale
@@ -128,6 +141,7 @@
## Atomic Operations
### Lua Script Design
Our core purchase operation uses a Redis Lua script to ensure atomicity:
```lua
@@ -145,12 +159,14 @@ local globalKey = KEYS[3] -- global:stats
```
**Benefits**:
- **Race Condition Prevention**: All operations execute atomically
- **Consistency**: No partial state updates
- **Performance**: Single round-trip to Redis
- **Reliability**: All-or-nothing execution
### Concurrency Handling
- **Optimistic Locking**: Lua scripts handle concurrent access
- **Queue Management**: Redis lists provide FIFO ticket distribution
- **Connection Pooling**: Efficient Redis connection reuse
@@ -158,11 +174,13 @@ local globalKey = KEYS[3] -- global:stats
## Fallback Mechanisms
### Activation Triggers
1. **Redis Connection Failure**: Network issues or Redis downtime
2. **Script Execution Errors**: Lua script failures
3. **Timeout Scenarios**: Slow Redis responses
### Fallback Architecture
```
┌─────────────────┐
│ Request Comes │
@@ -181,8 +199,16 @@ local globalKey = KEYS[3] -- global:stats
└─────────────────┘ └─────────────────┘
```
### Fallback Store Improvements
- **Automatic Seeding**: Fallback store is seeded during server startup and when activated
- **Data Synchronization**: Automatic attempt to sync with Redis data when activated
- **Manual Seeding**: Admin endpoint to manually populate fallback store from Redis
- **Resilient Operation**: Continues functioning even when Redis is completely unavailable
### Fallback Limitations
- **Non-Persistent**: Data lost on restart
- **Non-Persistent**: Data lost on restart (mitigated by automatic reseeding)
- **Single Instance**: No cross-instance synchronization
- **Capacity Limited**: Memory constraints
- **Warning Logs**: Clear indication of degraded mode
@@ -190,18 +216,21 @@ local globalKey = KEYS[3] -- global:stats
## Performance Optimizations
### Application Level
1. **Async Operations**: Non-blocking I/O throughout
2. **Connection Pooling**: Reuse Redis connections
3. **Batch Operations**: Bulk ticket seeding
4. **Caching**: Event metadata caching
### Redis Optimizations
1. **Lua Scripts**: Reduced network round-trips
2. **Pipeline Operations**: Batch commands
3. **Memory Management**: Efficient data structures
4. **Persistence**: AOF for durability
### PDF Generation
1. **Async Generation**: Non-blocking PDF creation
2. **Stream Processing**: Memory-efficient file handling
3. **Cleanup Jobs**: Automatic old file removal
@@ -210,6 +239,7 @@ local globalKey = KEYS[3] -- global:stats
## Monitoring & Observability
### Metrics Collection
```json
{
"global": {
@@ -238,12 +268,14 @@ local globalKey = KEYS[3] -- global:stats
```
### Logging Strategy
- **Structured Logging**: JSON format for parsing
- **Request Tracking**: Unique IDs for tracing
- **Performance Metrics**: Response times and throughput
- **Error Categorization**: Different log levels
### Health Checks
- **Application Health**: `/health` endpoint
- **Redis Connectivity**: Connection status
- **Fallback Status**: Degraded mode indication
@@ -252,16 +284,19 @@ local globalKey = KEYS[3] -- global:stats
## Security Considerations
### Input Validation
- **Event ID Validation**: Numeric constraints
- **Request Rate Limiting**: DDoS protection
- **Parameter Sanitization**: Injection prevention
### Container Security
- **Non-Root User**: Principle of least privilege
- **Minimal Base Image**: Alpine Linux for smaller attack surface
- **Health Checks**: Container monitoring
### Data Protection
- **No Sensitive Data**: Tickets are identifiers only
- **Audit Logging**: Purchase tracking
- **Secure Defaults**: Production-ready configuration
@@ -269,6 +304,7 @@ local globalKey = KEYS[3] -- global:stats
## Deployment Strategy
### Development Environment
```bash
# Local development
npm install
@@ -278,6 +314,7 @@ npm run dev # Start with nodemon
```
### Production Environment
```bash
# Docker deployment
docker-compose up -d # Core services
@@ -285,6 +322,7 @@ docker-compose --profile monitoring up # With monitoring
```
### Container Orchestration
- **Docker Compose**: Local and small deployments
- **Kubernetes**: Large-scale deployments
- **Health Checks**: Automatic restart on failure
@@ -293,24 +331,28 @@ docker-compose --profile monitoring up # With monitoring
## Future Enhancements
### Performance Improvements
1. **Redis Clustering**: Horizontal database scaling
2. **CDN Integration**: PDF delivery optimization
3. **Caching Layer**: Application-level caching
4. **Connection Optimization**: Advanced pooling
### Feature Additions
1. **QR Code Generation**: Enhanced ticket security
2. **Email Integration**: Automatic ticket delivery
3. **Payment Processing**: Complete purchase flow
4. **Event Management**: Dynamic event creation
### Monitoring Enhancements
1. **Distributed Tracing**: Request flow tracking
2. **Custom Dashboards**: Business metrics visualization
3. **Alerting**: Proactive issue detection
4. **Performance Profiling**: Bottleneck identification
### Security Hardening
1. **Authentication**: API key management
2. **Rate Limiting**: Advanced throttling
3. **Encryption**: Data in transit protection
+1
View File
@@ -9,6 +9,7 @@
"seed": "node seed.js",
"test": "jest",
"test:load": "node tests/load-test.js",
"test:fallback": "node test-fallback.js",
"docker:up": "docker-compose up -d",
"docker:down": "docker-compose down"
},
+75 -19
View File
@@ -1,11 +1,14 @@
const redis = require("redis");
require('dotenv').config();
require("dotenv").config();
// Import fallback store for seeding
const fallbackStore = require("./src/utils/fallback-store");
// Configuration for seeding
const config = {
numEvents: parseInt(process.argv[2]) || 5, // Number of events to create
ticketsPerEvent: parseInt(process.argv[3]) || 10000, // Tickets per event
redisUrl: process.env.REDIS_URL || "redis://localhost:6379"
redisUrl: process.env.REDIS_URL || "redis://localhost:6379",
};
const client = redis.createClient({ url: config.redisUrl });
@@ -13,51 +16,65 @@ const client = redis.createClient({ url: config.redisUrl });
async function seedTickets() {
try {
await client.connect();
console.log(`Seeding ${config.numEvents} events with ${config.ticketsPerEvent} tickets each...`);
console.log(
`Seeding ${config.numEvents} events with ${config.ticketsPerEvent} tickets each...`
);
// Clear existing event data
const existingKeys = await client.keys('event:*');
const existingKeys = await client.keys("event:*");
if (existingKeys.length > 0) {
await client.del(existingKeys);
console.log(`Cleared ${existingKeys.length} existing event keys.`);
}
// Seed multiple events
for (let eventId = 1; eventId <= config.numEvents; eventId++) {
const eventKey = `event:${eventId}:tickets`;
const metaKey = `event:${eventId}:meta`;
// Generate tickets for this event
const tickets = [];
for (let i = 1; i <= config.ticketsPerEvent; i++) {
tickets.push(`ticket-${eventId}-${i}`);
}
// Store tickets in Redis list
await client.rPush(eventKey, tickets);
// Store event metadata
await client.hSet(metaKey, {
const metadata = {
eventId: eventId,
totalTickets: config.ticketsPerEvent,
soldTickets: 0,
createdAt: new Date().toISOString(),
name: `Event ${eventId}`,
description: `Sample event ${eventId} for load testing`
});
console.log(`✓ Event ${eventId}: ${config.ticketsPerEvent} tickets seeded`);
description: `Sample event ${eventId} for load testing`,
};
await client.hSet(metaKey, metadata);
console.log(
`✓ Event ${eventId}: ${config.ticketsPerEvent} tickets seeded`
);
}
// Store global stats
await client.hSet('global:stats', {
await client.hSet("global:stats", {
totalEvents: config.numEvents,
totalTickets: config.numEvents * config.ticketsPerEvent,
totalSold: 0,
lastSeeded: new Date().toISOString()
lastSeeded: new Date().toISOString(),
});
console.log(`\n🎉 Successfully seeded ${config.numEvents} events with ${config.numEvents * config.ticketsPerEvent} total tickets!`);
// Also seed the fallback store
await seedFallbackStore();
console.log(
`\n🎉 Successfully seeded ${config.numEvents} events with ${
config.numEvents * config.ticketsPerEvent
} total tickets!`
);
console.log(`📦 Fallback store also seeded for resilience`);
process.exit(0);
} catch (err) {
console.error("Error during seed:", err);
@@ -65,4 +82,43 @@ async function seedTickets() {
}
}
async function seedFallbackStore() {
try {
console.log("\n🌐 Seeding fallback store...");
// Activate fallback store temporarily for seeding
fallbackStore.activate("Seeding fallback store during initialization");
// Seed the same events in fallback store
for (let eventId = 1; eventId <= config.numEvents; eventId++) {
const tickets = [];
for (let i = 1; i <= config.ticketsPerEvent; i++) {
tickets.push(`ticket-${eventId}-${i}`);
}
const metadata = {
eventId: eventId,
totalTickets: config.ticketsPerEvent,
soldTickets: 0,
createdAt: new Date().toISOString(),
name: `Event ${eventId}`,
description: `Sample event ${eventId} for load testing`,
};
fallbackStore.seedEvent(eventId, tickets, metadata);
}
// Update global stats in fallback store
fallbackStore.globalStats.lastSeeded = new Date().toISOString();
console.log(`✓ Fallback store seeded with ${config.numEvents} events`);
// Deactivate fallback store after seeding (will be activated when needed)
fallbackStore.deactivate();
} catch (error) {
console.error("Error seeding fallback store:", error);
// Don't fail the entire seeding process if fallback seeding fails
}
}
seedTickets();
+376 -164
View File
@@ -1,166 +1,184 @@
const express = require('express');
const { v4: uuidv4 } = require('uuid');
require('dotenv').config();
const express = require("express");
const { v4: uuidv4 } = require("uuid");
require("dotenv").config();
// Import utilities
const redisClient = require('./src/utils/redis-client');
const fallbackStore = require('./src/utils/fallback-store');
const logger = require('./src/utils/logger');
const pdfGenerator = require('./src/utils/pdf-generator');
const redisClient = require("./src/utils/redis-client");
const fallbackStore = require("./src/utils/fallback-store");
const logger = require("./src/utils/logger");
const pdfGenerator = require("./src/utils/pdf-generator");
const metrics = require("./src/utils/metrics");
const app = express();
const port = process.env.PORT || 3049;
// Middleware
app.use(express.json());
app.use(express.static('public'));
app.use(express.static("public"));
// Request logging middleware
app.use((req, res, next) => {
const start = Date.now();
res.on('finish', () => {
res.on("finish", () => {
const responseTime = Date.now() - start;
logger.logRequest(req, res, responseTime);
});
next();
});
// Global error handler
app.use((err, req, res, next) => {
logger.error('Unhandled error:', err);
res.status(500).json({
success: false,
message: 'Internal Server Error',
error: process.env.NODE_ENV === 'development' ? err.message : undefined
});
});
// Prometheus metrics middleware
app.use(metrics.metricsMiddleware);
// Health check endpoint
app.get('/health', async (req, res) => {
app.get("/health", async (req, res) => {
const redisHealthy = redisClient.isHealthy();
const fallbackActive = fallbackStore.isActive;
// Update metrics
metrics.updateRedisStatus(redisHealthy);
metrics.updateFallbackStatus(fallbackActive);
res.json({
status: 'ok',
status: "ok",
timestamp: new Date().toISOString(),
redis: {
connected: redisHealthy,
fallbackActive: fallbackActive
fallbackActive: fallbackActive,
},
uptime: process.uptime()
uptime: process.uptime(),
});
});
// Get all events endpoint
app.get('/events', async (req, res) => {
app.get("/events", async (req, res) => {
try {
let events;
if (redisClient.isHealthy()) {
events = await redisClient.getAllEvents();
} else {
events = fallbackStore.getAllEvents();
}
res.json({
success: true,
events,
usingFallback: fallbackStore.isActive
usingFallback: fallbackStore.isActive,
});
} catch (error) {
logger.error('Error fetching events:', error);
res.status(500).json({
success: false,
message: 'Failed to fetch events'
logger.error("Error fetching events:", error);
res.status(500).json({
success: false,
message: "Failed to fetch events",
});
}
});
// Get specific event stats
app.get('/events/:eventId', async (req, res) => {
app.get("/events/:eventId", async (req, res) => {
try {
const eventId = req.params.eventId;
let eventStats;
if (redisClient.isHealthy()) {
eventStats = await redisClient.getEventStats(eventId);
} else {
eventStats = fallbackStore.getEventStats(eventId);
}
if (!eventStats) {
return res.status(404).json({
success: false,
message: 'Event not found'
return res.status(404).json({
success: false,
message: "Event not found",
});
}
res.json({
success: true,
event: eventStats,
usingFallback: fallbackStore.isActive
usingFallback: fallbackStore.isActive,
});
} catch (error) {
logger.error('Error fetching event stats:', error);
res.status(500).json({
success: false,
message: 'Failed to fetch event stats'
logger.error("Error fetching event stats:", error);
res.status(500).json({
success: false,
message: "Failed to fetch event stats",
});
}
});
// Purchase ticket endpoint (multi-event)
app.post('/buy/:eventId', async (req, res) => {
app.post("/buy/:eventId", async (req, res) => {
const startTime = Date.now();
const eventId = req.params.eventId;
const purchaseId = uuidv4();
const timestamp = new Date().toISOString();
try {
let result;
// Try Redis first
if (redisClient.isHealthy()) {
try {
const luaResult = await redisClient.purchaseTicket(eventId, purchaseId, timestamp);
const luaResult = await redisClient.purchaseTicket(
eventId,
purchaseId,
timestamp
);
if (luaResult[0]) {
// Success - generate PDF ticket
try {
// Get event details for PDF
const eventStats = await redisClient.getEventStats(eventId);
const pdfStartTime = Date.now();
const pdfResult = await pdfGenerator.generateTicketPDF({
ticketId: luaResult[0],
eventId,
purchaseId,
eventName: eventStats?.name || `Event ${eventId}`,
eventDescription: eventStats?.description || 'Event description not available',
eventDescription:
eventStats?.description || "Event description not available",
timestamp,
soldCount: luaResult[2]
soldCount: luaResult[2],
});
// Record PDF generation metrics
const pdfDuration = (Date.now() - pdfStartTime) / 1000;
metrics.recordPDFGeneration(
pdfResult.success ? "success" : "failed",
pdfDuration
);
result = {
success: true,
ticket: luaResult[0],
purchaseId,
eventId,
soldCount: luaResult[2],
message: 'Ticket purchased successfully!',
message: "Ticket purchased successfully!",
usingFallback: false,
pdf: {
generated: pdfResult.success,
filename: pdfResult.filename,
downloadUrl: `/tickets/${purchaseId}`
}
downloadUrl: `/tickets/${purchaseId}`,
},
};
// Record metrics for successful purchase
metrics.recordTicketSale(eventId, "success");
metrics.updateTicketMetrics(
eventId,
luaResult[2],
luaResult[3] || 0
);
logger.logPurchase(eventId, luaResult[0], purchaseId, true);
logger.info(`PDF ticket generated for purchase ${purchaseId}`);
} catch (pdfError) {
logger.error('PDF generation failed:', pdfError);
logger.error("PDF generation failed:", pdfError);
// Still return success for ticket purchase, but note PDF failure
result = {
success: true,
@@ -168,83 +186,106 @@ app.post('/buy/:eventId', async (req, res) => {
purchaseId,
eventId,
soldCount: luaResult[2],
message: 'Ticket purchased successfully! (PDF generation failed)',
message: "Ticket purchased successfully! (PDF generation failed)",
usingFallback: false,
pdf: {
generated: false,
error: 'PDF generation failed'
}
error: "PDF generation failed",
},
};
// Record metrics for successful purchase (even with PDF failure)
metrics.recordTicketSale(eventId, "success");
metrics.updateTicketMetrics(
eventId,
luaResult[2],
luaResult[3] || 0
);
logger.logPurchase(eventId, luaResult[0], purchaseId, true);
}
} else {
// Failed - handle specific error
const errorCode = luaResult[1];
let statusCode = 400;
let message = 'Purchase failed';
let message = "Purchase failed";
switch (errorCode) {
case 'EVENT_NOT_FOUND':
case "EVENT_NOT_FOUND":
statusCode = 404;
message = 'Event not found';
message = "Event not found";
break;
case 'NO_TICKETS_AVAILABLE':
case "NO_TICKETS_AVAILABLE":
statusCode = 409;
message = 'No tickets available for this event';
message = "No tickets available for this event";
break;
}
// Record metrics for failed purchase
metrics.recordTicketSale(eventId, "failed");
logger.logPurchase(eventId, null, purchaseId, false, errorCode);
return res.status(statusCode).json({
success: false,
return res.status(statusCode).json({
success: false,
message,
errorCode,
eventId,
purchaseId
purchaseId,
});
}
} catch (redisError) {
logger.error('Redis purchase failed, attempting fallback:', redisError);
logger.error("Redis purchase failed, attempting fallback:", redisError);
// Activate fallback if not already active
if (!fallbackStore.isActive) {
fallbackStore.activate('Redis purchase operation failed');
fallbackStore.activate("Redis purchase operation failed");
// Try to sync with Redis data if possible
setTimeout(() => {
if (fallbackStore.isActive && fallbackStore.events.size === 0) {
fallbackStore.attemptReseed();
}
}, 100);
}
throw redisError; // Will be caught by outer try-catch for fallback
}
} else {
throw new Error('Redis not available');
throw new Error("Redis not available");
}
const responseTime = Date.now() - startTime;
result.responseTime = `${responseTime}ms`;
res.json(result);
} catch (error) {
// Fallback to in-memory store
try {
if (!fallbackStore.isActive) {
fallbackStore.activate('Redis connection failed during purchase');
fallbackStore.activate("Redis connection failed during purchase");
// Try to sync with Redis data if possible
setTimeout(() => {
if (fallbackStore.isActive && fallbackStore.events.size === 0) {
fallbackStore.attemptReseed();
}
}, 100);
}
const fallbackResult = fallbackStore.purchaseTicket(eventId, purchaseId);
if (fallbackResult.success) {
// Generate PDF for fallback purchase
try {
const eventStats = fallbackStore.getEventStats(eventId);
const pdfResult = await pdfGenerator.generateTicketPDF({
ticketId: fallbackResult.ticket,
eventId,
purchaseId,
eventName: eventStats?.name || `Event ${eventId}`,
eventDescription: eventStats?.description || 'Event description not available',
eventDescription:
eventStats?.description || "Event description not available",
timestamp,
soldCount: fallbackResult.soldCount
soldCount: fallbackResult.soldCount,
});
const responseTime = Date.now() - startTime;
res.json({
success: true,
@@ -252,21 +293,30 @@ app.post('/buy/:eventId', async (req, res) => {
purchaseId,
eventId,
soldCount: fallbackResult.soldCount,
message: 'Ticket purchased successfully (fallback mode)!',
message: "Ticket purchased successfully (fallback mode)!",
usingFallback: true,
responseTime: `${responseTime}ms`,
pdf: {
generated: pdfResult.success,
filename: pdfResult.filename,
downloadUrl: `/tickets/${purchaseId}`
}
downloadUrl: `/tickets/${purchaseId}`,
},
});
logger.info(`PDF ticket generated for fallback purchase ${purchaseId}`);
// Record metrics for successful fallback purchase
metrics.recordTicketSale(eventId, "success_fallback");
metrics.updateTicketMetrics(
eventId,
fallbackResult.soldCount,
fallbackResult.remainingTickets || 0
);
logger.info(
`PDF ticket generated for fallback purchase ${purchaseId}`
);
} catch (pdfError) {
logger.error('PDF generation failed in fallback mode:', pdfError);
logger.error("PDF generation failed in fallback mode:", pdfError);
const responseTime = Date.now() - startTime;
res.json({
success: true,
@@ -274,128 +324,218 @@ app.post('/buy/:eventId', async (req, res) => {
purchaseId,
eventId,
soldCount: fallbackResult.soldCount,
message: 'Ticket purchased successfully (fallback mode, PDF generation failed)!',
message:
"Ticket purchased successfully (fallback mode, PDF generation failed)!",
usingFallback: true,
responseTime: `${responseTime}ms`,
pdf: {
generated: false,
error: 'PDF generation failed'
}
error: "PDF generation failed",
},
});
// Record metrics for successful fallback purchase (even with PDF failure)
metrics.recordTicketSale(eventId, "success_fallback");
metrics.updateTicketMetrics(
eventId,
fallbackResult.soldCount,
fallbackResult.remainingTickets || 0
);
}
} else {
let statusCode = 400;
let message = 'Purchase failed';
let message = "Purchase failed";
switch (fallbackResult.error) {
case 'EVENT_NOT_FOUND':
case "EVENT_NOT_FOUND":
statusCode = 404;
message = 'Event not found';
message = "Event not found";
break;
case 'NO_TICKETS_AVAILABLE':
case "NO_TICKETS_AVAILABLE":
statusCode = 409;
message = 'No tickets available for this event';
message = "No tickets available for this event";
break;
}
logger.logPurchase(eventId, null, purchaseId, false, fallbackResult.error);
res.status(statusCode).json({
success: false,
// Record metrics for failed fallback purchase
metrics.recordTicketSale(eventId, "failed_fallback");
logger.logPurchase(
eventId,
null,
purchaseId,
false,
fallbackResult.error
);
res.status(statusCode).json({
success: false,
message,
errorCode: fallbackResult.error,
eventId,
purchaseId,
usingFallback: true
usingFallback: true,
});
}
} catch (fallbackError) {
logger.error('Both Redis and fallback failed:', fallbackError);
logger.error("Both Redis and fallback failed:", fallbackError);
// Record metrics for system failure
metrics.recordTicketSale(eventId, "system_error");
logger.logPurchase(eventId, null, purchaseId, false, fallbackError);
res.status(500).json({
success: false,
message: 'System temporarily unavailable',
res.status(500).json({
success: false,
message: "System temporarily unavailable",
eventId,
purchaseId
purchaseId,
});
}
}
});
// Download ticket PDF endpoint
app.get('/tickets/:purchaseId', async (req, res) => {
app.get("/tickets/:purchaseId", async (req, res) => {
try {
const purchaseId = req.params.purchaseId;
if (!pdfGenerator.ticketExists(purchaseId)) {
return res.status(404).json({
success: false,
message: 'Ticket not found'
message: "Ticket not found",
});
}
const filepath = pdfGenerator.getTicketPath(purchaseId);
const filename = `ticket-${purchaseId}.pdf`;
res.setHeader('Content-Type', 'application/pdf');
res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
const fileStream = require('fs').createReadStream(filepath);
res.setHeader("Content-Type", "application/pdf");
res.setHeader("Content-Disposition", `attachment; filename="${filename}"`);
const fileStream = require("fs").createReadStream(filepath);
fileStream.pipe(res);
logger.info(`PDF ticket downloaded: ${purchaseId}`);
} catch (error) {
logger.error('Error downloading ticket:', error);
logger.error("Error downloading ticket:", error);
res.status(500).json({
success: false,
message: 'Failed to download ticket'
message: "Failed to download ticket",
});
}
});
// PDF management endpoint
app.get('/admin/pdf-stats', async (req, res) => {
app.get("/admin/pdf-stats", async (req, res) => {
try {
const stats = pdfGenerator.getStats();
res.json({
success: true,
stats
stats,
});
} catch (error) {
logger.error('Error getting PDF stats:', error);
logger.error("Error getting PDF stats:", error);
res.status(500).json({
success: false,
message: 'Failed to get PDF statistics'
message: "Failed to get PDF statistics",
});
}
});
// Cleanup old tickets endpoint
app.post('/admin/cleanup-tickets', async (req, res) => {
app.post("/admin/cleanup-tickets", async (req, res) => {
try {
const maxAgeHours = req.body.maxAgeHours || 24;
const deletedCount = await pdfGenerator.cleanupOldTickets(maxAgeHours);
res.json({
success: true,
message: `Cleaned up ${deletedCount} old tickets`,
deletedCount
deletedCount,
});
} catch (error) {
logger.error('Error cleaning up tickets:', error);
logger.error("Error cleaning up tickets:", error);
res.status(500).json({
success: false,
message: 'Failed to cleanup tickets'
message: "Failed to cleanup tickets",
});
}
});
// Seed fallback store endpoint
app.post("/admin/seed-fallback", async (req, res) => {
try {
if (redisClient.isHealthy()) {
// Activate fallback store temporarily for seeding
fallbackStore.activate("Manual seeding from admin endpoint");
// Get all events from Redis and seed fallback store
const events = await redisClient.getAllEvents();
const globalStats = await redisClient.getGlobalStats();
for (const event of events) {
// Get remaining tickets for this event
const remainingTickets = await redisClient.getRemainingTickets(
event.eventId
);
// Create metadata object
const metadata = {
eventId: event.eventId,
totalTickets: event.totalTickets,
soldTickets: event.soldTickets,
createdAt: event.createdAt,
name: event.name,
description: event.description,
lastSoldAt: event.lastSoldAt,
};
// Seed the event in fallback store
fallbackStore.seedEvent(event.eventId, remainingTickets, metadata);
// Update sold tickets count
const fallbackEvent = fallbackStore.events.get(event.eventId);
if (fallbackEvent) {
fallbackEvent.soldTickets = event.soldTickets;
}
}
// Update global stats
if (globalStats) {
fallbackStore.globalStats.totalSold = globalStats.totalSold;
fallbackStore.globalStats.lastSeeded = new Date().toISOString();
}
// Deactivate fallback store (will be activated when needed)
fallbackStore.deactivate();
res.json({
success: true,
message: `Fallback store seeded with ${events.length} events`,
eventsCount: events.length,
totalTickets: globalStats?.totalTickets || 0,
totalSold: globalStats?.totalSold || 0,
});
} else {
res.status(503).json({
success: false,
message: "Redis not available - cannot seed fallback store",
});
}
} catch (error) {
logger.error("Error seeding fallback store:", error);
res.status(500).json({
success: false,
message: "Failed to seed fallback store",
});
}
});
// Metrics endpoint (Prometheus compatible)
app.get('/metrics', async (req, res) => {
app.get("/metrics", async (req, res) => {
try {
let globalStats, events;
if (redisClient.isHealthy()) {
globalStats = await redisClient.getGlobalStats();
events = await redisClient.getAllEvents();
@@ -403,10 +543,10 @@ app.get('/metrics', async (req, res) => {
globalStats = fallbackStore.getGlobalStats();
events = fallbackStore.getAllEvents();
}
// Get PDF stats
const pdfStats = pdfGenerator.getStats();
// Calculate metrics
const metrics = {
timestamp: new Date().toISOString(),
@@ -416,17 +556,17 @@ app.get('/metrics', async (req, res) => {
usingFallback: fallbackStore.isActive,
redisConnected: redisClient.isHealthy(),
uptime: process.uptime(),
memoryUsage: process.memoryUsage()
memoryUsage: process.memoryUsage(),
},
pdf: pdfStats
pdf: pdfStats,
};
res.json(metrics);
} catch (error) {
logger.error('Error generating metrics:', error);
res.status(500).json({
success: false,
message: 'Failed to generate metrics'
logger.error("Error generating metrics:", error);
res.status(500).json({
success: false,
message: "Failed to generate metrics",
});
}
});
@@ -436,8 +576,11 @@ async function initializeServer() {
try {
// Connect to Redis
await redisClient.connect();
logger.info('Redis connected successfully');
logger.info("Redis connected successfully");
// Ensure fallback store is seeded with current Redis data
await ensureFallbackStoreSeeded();
// Start server
app.listen(port, () => {
logger.info(`🚀 Ticket Microservice running on port ${port}`);
@@ -445,39 +588,108 @@ async function initializeServer() {
logger.info(`📈 Metrics: http://localhost:${port}/metrics`);
logger.info(`🎫 Events: http://localhost:${port}/events`);
});
} catch (error) {
logger.error('Failed to initialize server:', error);
logger.warn('Starting server with fallback store only');
fallbackStore.activate('Redis connection failed at startup');
logger.error("Failed to initialize server:", error);
logger.warn("Starting server with fallback store only");
fallbackStore.activate("Redis connection failed at startup");
app.listen(port, () => {
logger.warn(`⚠️ Server running in FALLBACK MODE on port ${port}`);
logger.warn('Redis connection failed - using in-memory store');
logger.warn("Redis connection failed - using in-memory store");
});
}
}
// Ensure fallback store is seeded with current Redis data
async function ensureFallbackStoreSeeded() {
try {
// Check if fallback store needs seeding
if (fallbackStore.events.size === 0) {
logger.info(
"Fallback store is empty, seeding with current Redis data..."
);
// Temporarily activate fallback store for seeding
fallbackStore.activate("Seeding during server initialization");
// Get all events from Redis and seed fallback store
const events = await redisClient.getAllEvents();
const globalStats = await redisClient.getGlobalStats();
for (const event of events) {
// Get remaining tickets for this event
const remainingTickets = await redisClient.getRemainingTickets(
event.eventId
);
// Create metadata object
const metadata = {
eventId: event.eventId,
totalTickets: event.totalTickets,
soldTickets: event.soldTickets,
createdAt: event.createdAt,
name: event.name,
description: event.description,
lastSoldAt: event.lastSoldAt,
};
// Seed the event in fallback store
fallbackStore.seedEvent(event.eventId, remainingTickets, metadata);
// Update sold tickets count
const fallbackEvent = fallbackStore.events.get(event.eventId);
if (fallbackEvent) {
fallbackEvent.soldTickets = event.soldTickets;
}
}
// Update global stats
if (globalStats) {
fallbackStore.globalStats.totalSold = globalStats.totalSold;
fallbackStore.globalStats.lastSeeded = new Date().toISOString();
}
logger.info(`Fallback store seeded with ${events.length} events`);
// Deactivate fallback store (will be activated when needed)
fallbackStore.deactivate();
}
} catch (error) {
logger.error("Error seeding fallback store during initialization:", error);
// Don't fail server startup if fallback seeding fails
}
}
// Global error handler
app.use((err, req, res, next) => {
logger.error("Unhandled error:", err);
res.status(500).json({
success: false,
message: "Internal Server Error",
error: process.env.NODE_ENV === "development" ? err.message : undefined,
});
});
// Graceful shutdown
process.on('SIGINT', async () => {
logger.info('Received SIGINT, shutting down gracefully...');
process.on("SIGINT", async () => {
logger.info("Received SIGINT, shutting down gracefully...");
try {
await redisClient.disconnect();
logger.info('Redis disconnected');
logger.info("Redis disconnected");
} catch (error) {
logger.error('Error disconnecting Redis:', error);
logger.error("Error disconnecting Redis:", error);
}
process.exit(0);
});
process.on('SIGTERM', async () => {
logger.info('Received SIGTERM, shutting down gracefully...');
process.on("SIGTERM", async () => {
logger.info("Received SIGTERM, shutting down gracefully...");
try {
await redisClient.disconnect();
logger.info('Redis disconnected');
logger.info("Redis disconnected");
} catch (error) {
logger.error('Error disconnecting Redis:', error);
logger.error("Error disconnecting Redis:", error);
}
process.exit(0);
});
+90 -17
View File
@@ -1,4 +1,4 @@
const logger = require('./logger');
const logger = require("./logger");
class FallbackStore {
constructor() {
@@ -7,20 +7,30 @@ class FallbackStore {
totalEvents: 0,
totalTickets: 0,
totalSold: 0,
lastSeeded: null
lastSeeded: null,
};
this.isActive = false;
}
activate(reason) {
this.isActive = true;
logger.logFallback('In-Memory Store Activated', reason);
logger.warn('⚠️ FALLBACK MODE: Using in-memory store - data will not persist!');
logger.logFallback("In-Memory Store Activated", reason);
logger.warn(
"⚠️ FALLBACK MODE: Using in-memory store - data will not persist!"
);
// Check if fallback store needs seeding
if (this.events.size === 0) {
logger.warn(
"⚠️ Fallback store is empty - attempting to seed from Redis..."
);
this.attemptReseed();
}
}
deactivate() {
this.isActive = false;
logger.info('In-Memory Store Deactivated - Redis connection restored');
logger.info("In-Memory Store Deactivated - Redis connection restored");
}
seedEvent(eventId, tickets, metadata) {
@@ -29,13 +39,15 @@ class FallbackStore {
this.events.set(eventId, {
tickets: [...tickets], // Create a copy
metadata: { ...metadata },
soldTickets: 0
soldTickets: 0,
});
this.globalStats.totalEvents++;
this.globalStats.totalTickets += tickets.length;
logger.info(`Fallback: Seeded event ${eventId} with ${tickets.length} tickets`);
logger.info(
`Fallback: Seeded event ${eventId} with ${tickets.length} tickets`
);
return true;
}
@@ -44,26 +56,26 @@ class FallbackStore {
const event = this.events.get(eventId);
if (!event) {
return { success: false, error: 'EVENT_NOT_FOUND' };
return { success: false, error: "EVENT_NOT_FOUND" };
}
if (event.tickets.length === 0) {
return { success: false, error: 'NO_TICKETS_AVAILABLE' };
return { success: false, error: "NO_TICKETS_AVAILABLE" };
}
// Atomically remove a ticket
const ticket = event.tickets.shift();
event.soldTickets++;
event.metadata.lastSoldAt = new Date().toISOString();
this.globalStats.totalSold++;
logger.logPurchase(eventId, ticket, purchaseId, true);
return {
success: true,
ticket,
soldCount: event.soldTickets
soldCount: event.soldTickets,
};
}
@@ -81,7 +93,7 @@ class FallbackStore {
soldTickets: event.soldTickets,
remainingTickets: event.tickets.length,
createdAt: event.metadata.createdAt,
lastSoldAt: event.metadata.lastSoldAt || 'never'
lastSoldAt: event.metadata.lastSoldAt || "never",
};
}
@@ -113,9 +125,9 @@ class FallbackStore {
totalEvents: 0,
totalTickets: 0,
totalSold: 0,
lastSeeded: null
lastSeeded: null,
};
logger.info('Fallback store cleared');
logger.info("Fallback store cleared");
}
getStatus() {
@@ -123,9 +135,70 @@ class FallbackStore {
active: this.isActive,
eventsCount: this.events.size,
totalTickets: this.globalStats.totalTickets,
totalSold: this.globalStats.totalSold
totalSold: this.globalStats.totalSold,
};
}
async attemptReseed() {
try {
logger.info("Attempting to reseed fallback store from Redis...");
// Try to connect to Redis temporarily to get data
const redis = require("redis");
const client = redis.createClient({
url: process.env.REDIS_URL || "redis://localhost:6379",
});
await client.connect();
// Get all event keys
const eventKeys = await client.keys("event:*:meta");
for (const metaKey of eventKeys) {
const eventId = metaKey.split(":")[1];
const ticketKey = `event:${eventId}:tickets`;
try {
// Get event metadata
const metadata = await client.hGetAll(metaKey);
if (!metadata.eventId) continue;
// Get remaining tickets
const remainingTickets = await client.lRange(ticketKey, 0, -1);
// Get sold tickets count
const soldTickets = parseInt(metadata.soldTickets) || 0;
// Seed the event in fallback store
this.seedEvent(eventId, remainingTickets, metadata);
// Update sold tickets count
const event = this.events.get(eventId);
if (event) {
event.soldTickets = soldTickets;
this.globalStats.totalSold += soldTickets;
}
logger.info(
`Fallback: Reseeded event ${eventId} with ${remainingTickets.length} tickets (${soldTickets} already sold)`
);
} catch (eventError) {
logger.error(`Error reseeding event ${eventId}:`, eventError);
}
}
// Update global stats
this.globalStats.lastSeeded = new Date().toISOString();
await client.disconnect();
logger.info("Fallback store reseed completed");
} catch (error) {
logger.error("Failed to reseed fallback store from Redis:", error);
logger.warn(
"Fallback store will operate with existing data or empty state"
);
}
}
}
module.exports = new FallbackStore();
+134
View File
@@ -0,0 +1,134 @@
const promClient = require("prom-client");
// Create a Registry to register the metrics
const register = new promClient.Registry();
// Enable the collection of default metrics
promClient.collectDefaultMetrics({ register });
// Custom metrics for the ticket service
const httpRequestDurationMicroseconds = new promClient.Histogram({
name: "http_request_duration_seconds",
help: "Duration of HTTP requests in seconds",
labelNames: ["method", "route", "status_code"],
buckets: [0.1, 0.5, 1, 2, 5],
});
const httpRequestsTotal = new promClient.Counter({
name: "http_requests_total",
help: "Total number of HTTP requests",
labelNames: ["method", "route", "status_code"],
});
const ticketsSoldTotal = new promClient.Counter({
name: "tickets_sold_total",
help: "Total number of tickets sold",
labelNames: ["event_id", "status"],
});
const ticketsAvailable = new promClient.Gauge({
name: "tickets_available",
help: "Number of tickets available per event",
labelNames: ["event_id"],
});
const redisConnectionStatus = new promClient.Gauge({
name: "redis_connection_status",
help: "Redis connection status (1 = connected, 0 = disconnected)",
});
const fallbackStoreActive = new promClient.Gauge({
name: "fallback_store_active",
help: "Fallback store activation status (1 = active, 0 = inactive)",
});
const pdfGenerationTotal = new promClient.Counter({
name: "pdf_generation_total",
help: "Total number of PDFs generated",
labelNames: ["status"],
});
const pdfGenerationDuration = new promClient.Histogram({
name: "pdf_generation_duration_seconds",
help: "Duration of PDF generation in seconds",
buckets: [0.1, 0.5, 1, 2, 5],
});
// Register all metrics
register.registerMetric(httpRequestDurationMicroseconds);
register.registerMetric(httpRequestsTotal);
register.registerMetric(ticketsSoldTotal);
register.registerMetric(ticketsAvailable);
register.registerMetric(redisConnectionStatus);
register.registerMetric(fallbackStoreActive);
register.registerMetric(pdfGenerationTotal);
register.registerMetric(pdfGenerationDuration);
// Middleware to collect HTTP metrics
const metricsMiddleware = (req, res, next) => {
const start = Date.now();
// Override res.end to capture response status
const originalEnd = res.end;
res.end = function (...args) {
const duration = (Date.now() - start) / 1000; // Convert to seconds
const route = req.route ? req.route.path : req.path;
// Record metrics
httpRequestDurationMicroseconds
.labels(req.method, route, res.statusCode.toString())
.observe(duration);
httpRequestsTotal
.labels(req.method, route, res.statusCode.toString())
.inc();
originalEnd.apply(this, args);
};
next();
};
// Function to update ticket metrics
const updateTicketMetrics = (eventId, soldTickets, remainingTickets) => {
ticketsAvailable.labels(eventId.toString()).set(remainingTickets);
};
// Function to record ticket sale
const recordTicketSale = (eventId, status = "success") => {
ticketsSoldTotal.labels(eventId.toString(), status).inc();
};
// Function to update Redis connection status
const updateRedisStatus = (isConnected) => {
redisConnectionStatus.set(isConnected ? 1 : 0);
};
// Function to update fallback store status
const updateFallbackStatus = (isActive) => {
fallbackStoreActive.set(isActive ? 1 : 0);
};
// Function to record PDF generation
const recordPDFGeneration = (status = "success", duration = null) => {
pdfGenerationTotal.labels(status).inc();
if (duration !== null) {
pdfGenerationDuration.observe(duration);
}
};
// Function to get metrics in Prometheus format
const getMetrics = async () => {
return await register.metrics();
};
module.exports = {
register,
metricsMiddleware,
updateTicketMetrics,
recordTicketSale,
updateRedisStatus,
updateFallbackStatus,
recordPDFGeneration,
getMetrics,
};
+64 -50
View File
@@ -1,7 +1,7 @@
const redis = require('redis');
const fs = require('fs');
const path = require('path');
const logger = require('./logger');
const redis = require("redis");
const fs = require("fs");
const path = require("path");
const logger = require("./logger");
class RedisClient {
constructor() {
@@ -13,28 +13,28 @@ class RedisClient {
async connect() {
try {
const redisUrl = process.env.REDIS_URL || 'redis://localhost:6379';
const redisUrl = process.env.REDIS_URL || "redis://localhost:6379";
this.client = redis.createClient({ url: redisUrl });
this.client.on('error', (err) => {
logger.error('Redis Client Error:', err);
this.client.on("error", (err) => {
logger.error("Redis Client Error:", err);
this.isConnected = false;
});
this.client.on('connect', () => {
logger.info('Redis client connected');
this.client.on("connect", () => {
logger.info("Redis client connected");
this.isConnected = true;
});
this.client.on('disconnect', () => {
logger.warn('Redis client disconnected');
this.client.on("disconnect", () => {
logger.warn("Redis client disconnected");
this.isConnected = false;
});
await this.client.connect();
return this.client;
} catch (error) {
logger.error('Failed to connect to Redis:', error);
logger.error("Failed to connect to Redis:", error);
this.isConnected = false;
throw error;
}
@@ -42,79 +42,75 @@ class RedisClient {
loadLuaScripts() {
try {
const luaDir = path.join(__dirname, '../lua');
const luaDir = path.join(__dirname, "../lua");
// Load purchase ticket script
const purchaseScript = fs.readFileSync(
path.join(luaDir, 'purchase-ticket.lua'),
'utf8'
path.join(luaDir, "purchase-ticket.lua"),
"utf8"
);
this.luaScripts.purchaseTicket = purchaseScript;
// Load event stats script
const statsScript = fs.readFileSync(
path.join(luaDir, 'get-event-stats.lua'),
'utf8'
path.join(luaDir, "get-event-stats.lua"),
"utf8"
);
this.luaScripts.getEventStats = statsScript;
logger.info('Lua scripts loaded successfully');
logger.info("Lua scripts loaded successfully");
} catch (error) {
logger.error('Failed to load Lua scripts:', error);
logger.error("Failed to load Lua scripts:", error);
throw error;
}
}
async purchaseTicket(eventId, purchaseId, timestamp) {
if (!this.isConnected) {
throw new Error('Redis not connected');
throw new Error("Redis not connected");
}
// Validate event exists before attempting purchase
const eventExists = await this.client.exists(`event:${eventId}:meta`);
if (!eventExists) {
logger.warn(`Event ${eventId} does not exist`);
return [null, 'EVENT_NOT_FOUND'];
return [null, "EVENT_NOT_FOUND"];
}
const keys = [
`event:${eventId}:tickets`,
`event:${eventId}:meta`,
'global:stats'
"global:stats",
];
// Ensure all arguments are strings as required by Redis Lua
const args = [String(timestamp), String(purchaseId)];
try {
const result = await this.client.eval(
this.luaScripts.purchaseTicket,
{ keys, arguments: args }
);
const result = await this.client.eval(this.luaScripts.purchaseTicket, {
keys,
arguments: args,
});
return result;
} catch (error) {
logger.error('Error executing purchase ticket script:', error);
logger.error('Script keys:', keys);
logger.error('Script args:', args);
logger.error("Error executing purchase ticket script:", error);
logger.error("Script keys:", keys);
logger.error("Script args:", args);
throw error;
}
}
async getEventStats(eventId) {
if (!this.isConnected) {
throw new Error('Redis not connected');
throw new Error("Redis not connected");
}
const keys = [
`event:${eventId}:meta`,
`event:${eventId}:tickets`
];
const keys = [`event:${eventId}:meta`, `event:${eventId}:tickets`];
try {
const result = await this.client.eval(
this.luaScripts.getEventStats,
{ keys }
);
const result = await this.client.eval(this.luaScripts.getEventStats, {
keys,
});
if (!result) return null;
return {
@@ -125,21 +121,21 @@ class RedisClient {
soldTickets: parseInt(result[4]),
remainingTickets: parseInt(result[5]),
createdAt: result[6],
lastSoldAt: result[7]
lastSoldAt: result[7],
};
} catch (error) {
logger.error('Error executing get event stats script:', error);
logger.error("Error executing get event stats script:", error);
throw error;
}
}
async getAllEvents() {
if (!this.isConnected) {
throw new Error('Redis not connected');
throw new Error("Redis not connected");
}
try {
const eventKeys = await this.client.keys('event:*:meta');
const eventKeys = await this.client.keys("event:*:meta");
const events = [];
for (const key of eventKeys) {
@@ -152,30 +148,48 @@ class RedisClient {
return events;
} catch (error) {
logger.error('Error getting all events:', error);
logger.error("Error getting all events:", error);
throw error;
}
}
async getGlobalStats() {
if (!this.isConnected) {
throw new Error('Redis not connected');
throw new Error("Redis not connected");
}
try {
const stats = await this.client.hGetAll('global:stats');
const stats = await this.client.hGetAll("global:stats");
return {
totalEvents: parseInt(stats.totalEvents) || 0,
totalTickets: parseInt(stats.totalTickets) || 0,
totalSold: parseInt(stats.totalSold) || 0,
lastSeeded: stats.lastSeeded || null
lastSeeded: stats.lastSeeded || null,
};
} catch (error) {
logger.error('Error getting global stats:', error);
logger.error("Error getting global stats:", error);
throw error;
}
}
async getRemainingTickets(eventId) {
if (!this.isConnected) {
throw new Error("Redis not connected");
}
try {
const ticketKey = `event:${eventId}:tickets`;
const remainingTickets = await this.client.lRange(ticketKey, 0, -1);
return remainingTickets;
} catch (error) {
logger.error(
`Error getting remaining tickets for event ${eventId}:`,
error
);
return [];
}
}
getClient() {
return this.client;
}
+65
View File
@@ -0,0 +1,65 @@
const fallbackStore = require("./src/utils/fallback-store");
async function testFallbackStore() {
console.log("🧪 Testing Fallback Store Functionality\n");
// Test 1: Check initial state
console.log("1. Initial State:");
console.log(` - Active: ${fallbackStore.isActive}`);
console.log(` - Events Count: ${fallbackStore.events.size}`);
console.log(` - Total Tickets: ${fallbackStore.globalStats.totalTickets}`);
console.log(` - Total Sold: ${fallbackStore.globalStats.totalSold}\n`);
// Test 2: Activate fallback store
console.log("2. Activating Fallback Store...");
fallbackStore.activate("Test activation");
console.log(` - Active: ${fallbackStore.isActive}\n`);
// Test 3: Check if seeding is needed
console.log("3. Checking Seeding Status:");
if (fallbackStore.events.size === 0) {
console.log(" - Fallback store is empty, attempting to seed...");
await fallbackStore.attemptReseed();
} else {
console.log(" - Fallback store already has data");
}
console.log(` - Events Count: ${fallbackStore.events.size}\n`);
// Test 4: Test ticket purchase if events exist
if (fallbackStore.events.size > 0) {
console.log("4. Testing Ticket Purchase:");
const eventId = "1";
const purchaseId = "test-purchase-123";
const result = fallbackStore.purchaseTicket(eventId, purchaseId);
if (result.success) {
console.log(` - Purchase successful: ${result.ticket}`);
console.log(` - Sold count: ${result.soldCount}`);
} else {
console.log(` - Purchase failed: ${result.error}`);
}
console.log(
` - Remaining tickets: ${
fallbackStore.getEventStats(eventId)?.remainingTickets || 0
}\n`
);
}
// Test 5: Get status
console.log("5. Fallback Store Status:");
const status = fallbackStore.getStatus();
console.log(` - Active: ${status.active}`);
console.log(` - Events: ${status.eventsCount}`);
console.log(` - Total Tickets: ${status.totalTickets}`);
console.log(` - Total Sold: ${status.totalSold}\n`);
// Test 6: Deactivate
console.log("6. Deactivating Fallback Store...");
fallbackStore.deactivate();
console.log(` - Active: ${fallbackStore.isActive}\n`);
console.log("✅ Fallback Store Test Completed!");
}
// Run the test
testFallbackStore().catch(console.error);