Feat: add prometheus compatible endpoint, seed fallback store and add env.example
This commit is contained in:
@@ -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
|
||||
@@ -47,26 +47,49 @@ 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
|
||||
@@ -75,7 +98,10 @@ Your task is to extract the high-throughput ticket purchasing component and exte
|
||||
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
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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,10 +16,12 @@ 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.`);
|
||||
@@ -37,27 +42,39 @@ async function seedTickets() {
|
||||
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`
|
||||
});
|
||||
description: `Sample event ${eventId} for load testing`,
|
||||
};
|
||||
|
||||
console.log(`✓ Event ${eventId}: ${config.ticketsPerEvent} tickets seeded`);
|
||||
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();
|
||||
|
||||
@@ -1,58 +1,56 @@
|
||||
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;
|
||||
|
||||
@@ -65,19 +63,19 @@ app.get('/events', async (req, res) => {
|
||||
res.json({
|
||||
success: true,
|
||||
events,
|
||||
usingFallback: fallbackStore.isActive
|
||||
usingFallback: fallbackStore.isActive,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Error fetching events:', error);
|
||||
logger.error("Error fetching events:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Failed to fetch events'
|
||||
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;
|
||||
@@ -91,26 +89,26 @@ app.get('/events/:eventId', async (req, res) => {
|
||||
if (!eventStats) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Event not found'
|
||||
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);
|
||||
logger.error("Error fetching event stats:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Failed to fetch event stats'
|
||||
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();
|
||||
@@ -122,7 +120,11 @@ app.post('/buy/:eventId', async (req, res) => {
|
||||
// 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
|
||||
@@ -130,36 +132,52 @@ app.post('/buy/:eventId', async (req, res) => {
|
||||
// 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 = {
|
||||
@@ -168,64 +186,86 @@ 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,
|
||||
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);
|
||||
@@ -240,9 +280,10 @@ app.post('/buy/:eventId', async (req, res) => {
|
||||
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;
|
||||
@@ -252,20 +293,29 @@ 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({
|
||||
@@ -274,105 +324,126 @@ 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);
|
||||
// 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',
|
||||
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}"`);
|
||||
res.setHeader("Content-Type", "application/pdf");
|
||||
res.setHeader("Content-Disposition", `attachment; filename="${filename}"`);
|
||||
|
||||
const fileStream = require('fs').createReadStream(filepath);
|
||||
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);
|
||||
@@ -380,19 +451,88 @@ app.post('/admin/cleanup-tickets', async (req, res) => {
|
||||
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;
|
||||
|
||||
@@ -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);
|
||||
logger.error("Error generating metrics:", error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Failed to generate metrics'
|
||||
message: "Failed to generate metrics",
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -436,7 +576,10 @@ 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, () => {
|
||||
@@ -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');
|
||||
logger.error("Failed to initialize server:", error);
|
||||
logger.warn("Starting server with fallback store only");
|
||||
|
||||
fallbackStore.activate('Redis connection failed at startup');
|
||||
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);
|
||||
});
|
||||
|
||||
+87
-14
@@ -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,11 +56,11 @@ 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
|
||||
@@ -63,7 +75,7 @@ class FallbackStore {
|
||||
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();
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
+61
-47
@@ -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,78 +42,74 @@ 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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
Reference in New Issue
Block a user