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
|
- Docker and Docker Compose
|
||||||
- Git
|
- 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
|
### Quick Start
|
||||||
|
|
||||||
1. **Clone the repository**
|
1. **Clone the repository**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone <repository-url>
|
git clone <repository-url>
|
||||||
cd module4_backend_project
|
cd module4_backend_project
|
||||||
```
|
```
|
||||||
|
|
||||||
2. **Install dependencies**
|
2. **Install dependencies**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install
|
npm install
|
||||||
```
|
```
|
||||||
|
|
||||||
3. **Set up environment**
|
3. **Set up environment**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cp .env.example .env
|
cp env.example .env
|
||||||
# Edit .env file if needed
|
# 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)**
|
4. **Start with Docker (Recommended)**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Start core services (Redis + App)
|
# Start core services (Redis + App)
|
||||||
docker-compose up -d
|
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
|
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**
|
5. **Seed the database**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Seed 5 events with 10,000 tickets each
|
# Seed 5 events with 10,000 tickets each
|
||||||
npm run seed
|
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:
|
If you prefer to run components separately:
|
||||||
|
|
||||||
1. **Start Redis**
|
1. **Create necessary directories**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p logs tickets
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Start Redis**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker-compose up -d redis
|
docker-compose up -d redis
|
||||||
```
|
```
|
||||||
|
|
||||||
2. **Start the application**
|
3. **Start the application**
|
||||||
```bash
|
```bash
|
||||||
npm run dev # Development with auto-reload
|
npm run dev # Development with auto-reload
|
||||||
# or
|
# or
|
||||||
@@ -104,22 +137,23 @@ If you prefer to run components separately:
|
|||||||
|
|
||||||
Once running, the following endpoints are available:
|
Once running, the following endpoints are available:
|
||||||
|
|
||||||
| Method | Endpoint | Description |
|
| Method | Endpoint | Description |
|
||||||
|--------|----------|-------------|
|
| ------ | ------------------------ | --------------------------------------- |
|
||||||
| GET | `/health` | System health check |
|
| GET | `/health` | System health check |
|
||||||
| GET | `/events` | List all events with statistics |
|
| GET | `/events` | List all events with statistics |
|
||||||
| GET | `/events/:eventId` | Get specific event details |
|
| GET | `/events/:eventId` | Get specific event details |
|
||||||
| POST | `/buy/:eventId` | Purchase a ticket for an event |
|
| POST | `/buy/:eventId` | Purchase a ticket for an event |
|
||||||
| GET | `/tickets/:purchaseId` | Download ticket PDF |
|
| GET | `/tickets/:purchaseId` | Download ticket PDF |
|
||||||
| GET | `/metrics` | Real-time system metrics |
|
| GET | `/metrics` | Real-time system metrics |
|
||||||
| GET | `/admin/pdf-stats` | PDF management statistics |
|
| GET | `/admin/pdf-stats` | PDF management statistics |
|
||||||
| POST | `/admin/cleanup-tickets` | Cleanup old ticket files |
|
| POST | `/admin/cleanup-tickets` | Cleanup old ticket files |
|
||||||
|
| POST | `/admin/seed-fallback` | Manually seed fallback store from Redis |
|
||||||
|
|
||||||
### Load Testing
|
### Load Testing
|
||||||
|
|
||||||
The system includes a comprehensive load testing framework:
|
The system includes a comprehensive load testing framework:
|
||||||
|
|
||||||
```bash
|
````bash
|
||||||
# Run full test suite (5000+ concurrent connections)
|
# Run full test suite (5000+ concurrent connections)
|
||||||
npm run test:load -- --full
|
npm run test:load -- --full
|
||||||
|
|
||||||
@@ -131,7 +165,9 @@ npm run test:load -- --multi --events 1,2,3 --connections 6000
|
|||||||
|
|
||||||
# Custom load test
|
# Custom load test
|
||||||
node tests/load-test.js --event 2 --connections 1000 --duration 10
|
node tests/load-test.js --event 2 --connections 1000 --duration 10
|
||||||
```
|
|
||||||
|
# Test fallback store functionality
|
||||||
|
npm run test:fallback
|
||||||
|
|
||||||
### Monitoring & Metrics
|
### Monitoring & Metrics
|
||||||
|
|
||||||
@@ -146,6 +182,22 @@ Grafana dashboard: http://localhost:3000
|
|||||||
- Username: `admin`
|
- Username: `admin`
|
||||||
- Password: `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
|
### Docker Commands
|
||||||
|
|
||||||
```bash
|
```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.
|
- **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.
|
- **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
|
## Final Challenges
|
||||||
|
|
||||||
- Enhance your docker-compose setup to include a Prometheus container for live monitoring.
|
- Enhance your docker-compose setup to include a Prometheus container for live monitoring.
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
# Ticket Scaling Microservice - Design Document
|
# Ticket Scaling Microservice - Design Document
|
||||||
|
|
||||||
## Table of Contents
|
## Table of Contents
|
||||||
|
|
||||||
1. [Architecture Overview](#architecture-overview)
|
1. [Architecture Overview](#architecture-overview)
|
||||||
2. [System Components](#system-components)
|
2. [System Components](#system-components)
|
||||||
3. [Scalability Strategies](#scalability-strategies)
|
3. [Scalability Strategies](#scalability-strategies)
|
||||||
@@ -15,6 +16,7 @@
|
|||||||
## Architecture Overview
|
## Architecture Overview
|
||||||
|
|
||||||
### High-Level Architecture
|
### High-Level Architecture
|
||||||
|
|
||||||
```
|
```
|
||||||
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
||||||
│ Load Balancer │ │ Prometheus │ │ Grafana │
|
│ Load Balancer │ │ Prometheus │ │ Grafana │
|
||||||
@@ -38,6 +40,7 @@
|
|||||||
```
|
```
|
||||||
|
|
||||||
### Design Principles
|
### Design Principles
|
||||||
|
|
||||||
1. **High Availability**: Fallback mechanisms ensure service continuity
|
1. **High Availability**: Fallback mechanisms ensure service continuity
|
||||||
2. **Atomic Operations**: Redis Lua scripts prevent race conditions
|
2. **Atomic Operations**: Redis Lua scripts prevent race conditions
|
||||||
3. **Horizontal Scalability**: Stateless design enables easy scaling
|
3. **Horizontal Scalability**: Stateless design enables easy scaling
|
||||||
@@ -47,6 +50,7 @@
|
|||||||
## System Components
|
## System Components
|
||||||
|
|
||||||
### 1. Core Application (server.js)
|
### 1. Core Application (server.js)
|
||||||
|
|
||||||
- **Technology**: Node.js with Express framework
|
- **Technology**: Node.js with Express framework
|
||||||
- **Responsibilities**:
|
- **Responsibilities**:
|
||||||
- HTTP request handling
|
- HTTP request handling
|
||||||
@@ -55,6 +59,7 @@
|
|||||||
- PDF generation coordination
|
- PDF generation coordination
|
||||||
|
|
||||||
### 2. Redis Client (redis-client.js)
|
### 2. Redis Client (redis-client.js)
|
||||||
|
|
||||||
- **Technology**: Redis with Lua scripting
|
- **Technology**: Redis with Lua scripting
|
||||||
- **Responsibilities**:
|
- **Responsibilities**:
|
||||||
- Atomic ticket operations
|
- Atomic ticket operations
|
||||||
@@ -63,6 +68,7 @@
|
|||||||
- Script execution
|
- Script execution
|
||||||
|
|
||||||
### 3. Fallback Store (fallback-store.js)
|
### 3. Fallback Store (fallback-store.js)
|
||||||
|
|
||||||
- **Technology**: In-memory JavaScript Map
|
- **Technology**: In-memory JavaScript Map
|
||||||
- **Responsibilities**:
|
- **Responsibilities**:
|
||||||
- Emergency ticket storage
|
- Emergency ticket storage
|
||||||
@@ -70,6 +76,7 @@
|
|||||||
- Graceful degradation
|
- Graceful degradation
|
||||||
|
|
||||||
### 4. PDF Generator (pdf-generator.js)
|
### 4. PDF Generator (pdf-generator.js)
|
||||||
|
|
||||||
- **Technology**: PDFKit library
|
- **Technology**: PDFKit library
|
||||||
- **Responsibilities**:
|
- **Responsibilities**:
|
||||||
- Professional ticket generation
|
- Professional ticket generation
|
||||||
@@ -77,6 +84,7 @@
|
|||||||
- Cleanup operations
|
- Cleanup operations
|
||||||
|
|
||||||
### 5. Logging System (logger.js)
|
### 5. Logging System (logger.js)
|
||||||
|
|
||||||
- **Technology**: Winston logging framework
|
- **Technology**: Winston logging framework
|
||||||
- **Responsibilities**:
|
- **Responsibilities**:
|
||||||
- Structured logging
|
- Structured logging
|
||||||
@@ -87,6 +95,7 @@
|
|||||||
## Scalability Strategies
|
## Scalability Strategies
|
||||||
|
|
||||||
### Horizontal Scaling
|
### Horizontal Scaling
|
||||||
|
|
||||||
```
|
```
|
||||||
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
||||||
│ Instance 1 │ │ Instance 2 │ │ Instance N │
|
│ Instance 1 │ │ Instance 2 │ │ Instance N │
|
||||||
@@ -102,17 +111,20 @@
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Key Features**:
|
**Key Features**:
|
||||||
|
|
||||||
- Stateless application design
|
- Stateless application design
|
||||||
- Shared Redis backend
|
- Shared Redis backend
|
||||||
- Load balancer distribution
|
- Load balancer distribution
|
||||||
- Independent scaling
|
- Independent scaling
|
||||||
|
|
||||||
### Vertical Scaling
|
### Vertical Scaling
|
||||||
|
|
||||||
- **CPU**: Multi-core utilization through Node.js cluster mode
|
- **CPU**: Multi-core utilization through Node.js cluster mode
|
||||||
- **Memory**: Configurable heap sizes for high-throughput
|
- **Memory**: Configurable heap sizes for high-throughput
|
||||||
- **I/O**: Async operations prevent blocking
|
- **I/O**: Async operations prevent blocking
|
||||||
|
|
||||||
### Database Scaling
|
### Database Scaling
|
||||||
|
|
||||||
```
|
```
|
||||||
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
||||||
│ Redis Master │ │ Redis Replica │ │ Redis Replica │
|
│ Redis Master │ │ Redis Replica │ │ Redis Replica │
|
||||||
@@ -121,6 +133,7 @@
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Strategies**:
|
**Strategies**:
|
||||||
|
|
||||||
- Redis clustering for horizontal scaling
|
- Redis clustering for horizontal scaling
|
||||||
- Read replicas for metrics/stats queries
|
- Read replicas for metrics/stats queries
|
||||||
- Sharding by event ID for massive scale
|
- Sharding by event ID for massive scale
|
||||||
@@ -128,6 +141,7 @@
|
|||||||
## Atomic Operations
|
## Atomic Operations
|
||||||
|
|
||||||
### Lua Script Design
|
### Lua Script Design
|
||||||
|
|
||||||
Our core purchase operation uses a Redis Lua script to ensure atomicity:
|
Our core purchase operation uses a Redis Lua script to ensure atomicity:
|
||||||
|
|
||||||
```lua
|
```lua
|
||||||
@@ -145,12 +159,14 @@ local globalKey = KEYS[3] -- global:stats
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Benefits**:
|
**Benefits**:
|
||||||
|
|
||||||
- **Race Condition Prevention**: All operations execute atomically
|
- **Race Condition Prevention**: All operations execute atomically
|
||||||
- **Consistency**: No partial state updates
|
- **Consistency**: No partial state updates
|
||||||
- **Performance**: Single round-trip to Redis
|
- **Performance**: Single round-trip to Redis
|
||||||
- **Reliability**: All-or-nothing execution
|
- **Reliability**: All-or-nothing execution
|
||||||
|
|
||||||
### Concurrency Handling
|
### Concurrency Handling
|
||||||
|
|
||||||
- **Optimistic Locking**: Lua scripts handle concurrent access
|
- **Optimistic Locking**: Lua scripts handle concurrent access
|
||||||
- **Queue Management**: Redis lists provide FIFO ticket distribution
|
- **Queue Management**: Redis lists provide FIFO ticket distribution
|
||||||
- **Connection Pooling**: Efficient Redis connection reuse
|
- **Connection Pooling**: Efficient Redis connection reuse
|
||||||
@@ -158,11 +174,13 @@ local globalKey = KEYS[3] -- global:stats
|
|||||||
## Fallback Mechanisms
|
## Fallback Mechanisms
|
||||||
|
|
||||||
### Activation Triggers
|
### Activation Triggers
|
||||||
|
|
||||||
1. **Redis Connection Failure**: Network issues or Redis downtime
|
1. **Redis Connection Failure**: Network issues or Redis downtime
|
||||||
2. **Script Execution Errors**: Lua script failures
|
2. **Script Execution Errors**: Lua script failures
|
||||||
3. **Timeout Scenarios**: Slow Redis responses
|
3. **Timeout Scenarios**: Slow Redis responses
|
||||||
|
|
||||||
### Fallback Architecture
|
### Fallback Architecture
|
||||||
|
|
||||||
```
|
```
|
||||||
┌─────────────────┐
|
┌─────────────────┐
|
||||||
│ Request Comes │
|
│ 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
|
### Fallback Limitations
|
||||||
- **Non-Persistent**: Data lost on restart
|
|
||||||
|
- **Non-Persistent**: Data lost on restart (mitigated by automatic reseeding)
|
||||||
- **Single Instance**: No cross-instance synchronization
|
- **Single Instance**: No cross-instance synchronization
|
||||||
- **Capacity Limited**: Memory constraints
|
- **Capacity Limited**: Memory constraints
|
||||||
- **Warning Logs**: Clear indication of degraded mode
|
- **Warning Logs**: Clear indication of degraded mode
|
||||||
@@ -190,18 +216,21 @@ local globalKey = KEYS[3] -- global:stats
|
|||||||
## Performance Optimizations
|
## Performance Optimizations
|
||||||
|
|
||||||
### Application Level
|
### Application Level
|
||||||
|
|
||||||
1. **Async Operations**: Non-blocking I/O throughout
|
1. **Async Operations**: Non-blocking I/O throughout
|
||||||
2. **Connection Pooling**: Reuse Redis connections
|
2. **Connection Pooling**: Reuse Redis connections
|
||||||
3. **Batch Operations**: Bulk ticket seeding
|
3. **Batch Operations**: Bulk ticket seeding
|
||||||
4. **Caching**: Event metadata caching
|
4. **Caching**: Event metadata caching
|
||||||
|
|
||||||
### Redis Optimizations
|
### Redis Optimizations
|
||||||
|
|
||||||
1. **Lua Scripts**: Reduced network round-trips
|
1. **Lua Scripts**: Reduced network round-trips
|
||||||
2. **Pipeline Operations**: Batch commands
|
2. **Pipeline Operations**: Batch commands
|
||||||
3. **Memory Management**: Efficient data structures
|
3. **Memory Management**: Efficient data structures
|
||||||
4. **Persistence**: AOF for durability
|
4. **Persistence**: AOF for durability
|
||||||
|
|
||||||
### PDF Generation
|
### PDF Generation
|
||||||
|
|
||||||
1. **Async Generation**: Non-blocking PDF creation
|
1. **Async Generation**: Non-blocking PDF creation
|
||||||
2. **Stream Processing**: Memory-efficient file handling
|
2. **Stream Processing**: Memory-efficient file handling
|
||||||
3. **Cleanup Jobs**: Automatic old file removal
|
3. **Cleanup Jobs**: Automatic old file removal
|
||||||
@@ -210,6 +239,7 @@ local globalKey = KEYS[3] -- global:stats
|
|||||||
## Monitoring & Observability
|
## Monitoring & Observability
|
||||||
|
|
||||||
### Metrics Collection
|
### Metrics Collection
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"global": {
|
"global": {
|
||||||
@@ -238,12 +268,14 @@ local globalKey = KEYS[3] -- global:stats
|
|||||||
```
|
```
|
||||||
|
|
||||||
### Logging Strategy
|
### Logging Strategy
|
||||||
|
|
||||||
- **Structured Logging**: JSON format for parsing
|
- **Structured Logging**: JSON format for parsing
|
||||||
- **Request Tracking**: Unique IDs for tracing
|
- **Request Tracking**: Unique IDs for tracing
|
||||||
- **Performance Metrics**: Response times and throughput
|
- **Performance Metrics**: Response times and throughput
|
||||||
- **Error Categorization**: Different log levels
|
- **Error Categorization**: Different log levels
|
||||||
|
|
||||||
### Health Checks
|
### Health Checks
|
||||||
|
|
||||||
- **Application Health**: `/health` endpoint
|
- **Application Health**: `/health` endpoint
|
||||||
- **Redis Connectivity**: Connection status
|
- **Redis Connectivity**: Connection status
|
||||||
- **Fallback Status**: Degraded mode indication
|
- **Fallback Status**: Degraded mode indication
|
||||||
@@ -252,16 +284,19 @@ local globalKey = KEYS[3] -- global:stats
|
|||||||
## Security Considerations
|
## Security Considerations
|
||||||
|
|
||||||
### Input Validation
|
### Input Validation
|
||||||
|
|
||||||
- **Event ID Validation**: Numeric constraints
|
- **Event ID Validation**: Numeric constraints
|
||||||
- **Request Rate Limiting**: DDoS protection
|
- **Request Rate Limiting**: DDoS protection
|
||||||
- **Parameter Sanitization**: Injection prevention
|
- **Parameter Sanitization**: Injection prevention
|
||||||
|
|
||||||
### Container Security
|
### Container Security
|
||||||
|
|
||||||
- **Non-Root User**: Principle of least privilege
|
- **Non-Root User**: Principle of least privilege
|
||||||
- **Minimal Base Image**: Alpine Linux for smaller attack surface
|
- **Minimal Base Image**: Alpine Linux for smaller attack surface
|
||||||
- **Health Checks**: Container monitoring
|
- **Health Checks**: Container monitoring
|
||||||
|
|
||||||
### Data Protection
|
### Data Protection
|
||||||
|
|
||||||
- **No Sensitive Data**: Tickets are identifiers only
|
- **No Sensitive Data**: Tickets are identifiers only
|
||||||
- **Audit Logging**: Purchase tracking
|
- **Audit Logging**: Purchase tracking
|
||||||
- **Secure Defaults**: Production-ready configuration
|
- **Secure Defaults**: Production-ready configuration
|
||||||
@@ -269,6 +304,7 @@ local globalKey = KEYS[3] -- global:stats
|
|||||||
## Deployment Strategy
|
## Deployment Strategy
|
||||||
|
|
||||||
### Development Environment
|
### Development Environment
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Local development
|
# Local development
|
||||||
npm install
|
npm install
|
||||||
@@ -278,6 +314,7 @@ npm run dev # Start with nodemon
|
|||||||
```
|
```
|
||||||
|
|
||||||
### Production Environment
|
### Production Environment
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Docker deployment
|
# Docker deployment
|
||||||
docker-compose up -d # Core services
|
docker-compose up -d # Core services
|
||||||
@@ -285,6 +322,7 @@ docker-compose --profile monitoring up # With monitoring
|
|||||||
```
|
```
|
||||||
|
|
||||||
### Container Orchestration
|
### Container Orchestration
|
||||||
|
|
||||||
- **Docker Compose**: Local and small deployments
|
- **Docker Compose**: Local and small deployments
|
||||||
- **Kubernetes**: Large-scale deployments
|
- **Kubernetes**: Large-scale deployments
|
||||||
- **Health Checks**: Automatic restart on failure
|
- **Health Checks**: Automatic restart on failure
|
||||||
@@ -293,24 +331,28 @@ docker-compose --profile monitoring up # With monitoring
|
|||||||
## Future Enhancements
|
## Future Enhancements
|
||||||
|
|
||||||
### Performance Improvements
|
### Performance Improvements
|
||||||
|
|
||||||
1. **Redis Clustering**: Horizontal database scaling
|
1. **Redis Clustering**: Horizontal database scaling
|
||||||
2. **CDN Integration**: PDF delivery optimization
|
2. **CDN Integration**: PDF delivery optimization
|
||||||
3. **Caching Layer**: Application-level caching
|
3. **Caching Layer**: Application-level caching
|
||||||
4. **Connection Optimization**: Advanced pooling
|
4. **Connection Optimization**: Advanced pooling
|
||||||
|
|
||||||
### Feature Additions
|
### Feature Additions
|
||||||
|
|
||||||
1. **QR Code Generation**: Enhanced ticket security
|
1. **QR Code Generation**: Enhanced ticket security
|
||||||
2. **Email Integration**: Automatic ticket delivery
|
2. **Email Integration**: Automatic ticket delivery
|
||||||
3. **Payment Processing**: Complete purchase flow
|
3. **Payment Processing**: Complete purchase flow
|
||||||
4. **Event Management**: Dynamic event creation
|
4. **Event Management**: Dynamic event creation
|
||||||
|
|
||||||
### Monitoring Enhancements
|
### Monitoring Enhancements
|
||||||
|
|
||||||
1. **Distributed Tracing**: Request flow tracking
|
1. **Distributed Tracing**: Request flow tracking
|
||||||
2. **Custom Dashboards**: Business metrics visualization
|
2. **Custom Dashboards**: Business metrics visualization
|
||||||
3. **Alerting**: Proactive issue detection
|
3. **Alerting**: Proactive issue detection
|
||||||
4. **Performance Profiling**: Bottleneck identification
|
4. **Performance Profiling**: Bottleneck identification
|
||||||
|
|
||||||
### Security Hardening
|
### Security Hardening
|
||||||
|
|
||||||
1. **Authentication**: API key management
|
1. **Authentication**: API key management
|
||||||
2. **Rate Limiting**: Advanced throttling
|
2. **Rate Limiting**: Advanced throttling
|
||||||
3. **Encryption**: Data in transit protection
|
3. **Encryption**: Data in transit protection
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
"seed": "node seed.js",
|
"seed": "node seed.js",
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
"test:load": "node tests/load-test.js",
|
"test:load": "node tests/load-test.js",
|
||||||
|
"test:fallback": "node test-fallback.js",
|
||||||
"docker:up": "docker-compose up -d",
|
"docker:up": "docker-compose up -d",
|
||||||
"docker:down": "docker-compose down"
|
"docker:down": "docker-compose down"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
const redis = require("redis");
|
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
|
// Configuration for seeding
|
||||||
const config = {
|
const config = {
|
||||||
numEvents: parseInt(process.argv[2]) || 5, // Number of events to create
|
numEvents: parseInt(process.argv[2]) || 5, // Number of events to create
|
||||||
ticketsPerEvent: parseInt(process.argv[3]) || 10000, // Tickets per event
|
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 });
|
const client = redis.createClient({ url: config.redisUrl });
|
||||||
@@ -13,10 +16,12 @@ const client = redis.createClient({ url: config.redisUrl });
|
|||||||
async function seedTickets() {
|
async function seedTickets() {
|
||||||
try {
|
try {
|
||||||
await client.connect();
|
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
|
// Clear existing event data
|
||||||
const existingKeys = await client.keys('event:*');
|
const existingKeys = await client.keys("event:*");
|
||||||
if (existingKeys.length > 0) {
|
if (existingKeys.length > 0) {
|
||||||
await client.del(existingKeys);
|
await client.del(existingKeys);
|
||||||
console.log(`Cleared ${existingKeys.length} existing event keys.`);
|
console.log(`Cleared ${existingKeys.length} existing event keys.`);
|
||||||
@@ -37,27 +42,39 @@ async function seedTickets() {
|
|||||||
await client.rPush(eventKey, tickets);
|
await client.rPush(eventKey, tickets);
|
||||||
|
|
||||||
// Store event metadata
|
// Store event metadata
|
||||||
await client.hSet(metaKey, {
|
const metadata = {
|
||||||
eventId: eventId,
|
eventId: eventId,
|
||||||
totalTickets: config.ticketsPerEvent,
|
totalTickets: config.ticketsPerEvent,
|
||||||
soldTickets: 0,
|
soldTickets: 0,
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
name: `Event ${eventId}`,
|
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
|
// Store global stats
|
||||||
await client.hSet('global:stats', {
|
await client.hSet("global:stats", {
|
||||||
totalEvents: config.numEvents,
|
totalEvents: config.numEvents,
|
||||||
totalTickets: config.numEvents * config.ticketsPerEvent,
|
totalTickets: config.numEvents * config.ticketsPerEvent,
|
||||||
totalSold: 0,
|
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);
|
process.exit(0);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Error during seed:", 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();
|
seedTickets();
|
||||||
|
|||||||
@@ -1,58 +1,56 @@
|
|||||||
const express = require('express');
|
const express = require("express");
|
||||||
const { v4: uuidv4 } = require('uuid');
|
const { v4: uuidv4 } = require("uuid");
|
||||||
require('dotenv').config();
|
require("dotenv").config();
|
||||||
|
|
||||||
// Import utilities
|
// Import utilities
|
||||||
const redisClient = require('./src/utils/redis-client');
|
const redisClient = require("./src/utils/redis-client");
|
||||||
const fallbackStore = require('./src/utils/fallback-store');
|
const fallbackStore = require("./src/utils/fallback-store");
|
||||||
const logger = require('./src/utils/logger');
|
const logger = require("./src/utils/logger");
|
||||||
const pdfGenerator = require('./src/utils/pdf-generator');
|
const pdfGenerator = require("./src/utils/pdf-generator");
|
||||||
|
const metrics = require("./src/utils/metrics");
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const port = process.env.PORT || 3049;
|
const port = process.env.PORT || 3049;
|
||||||
|
|
||||||
// Middleware
|
// Middleware
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
app.use(express.static('public'));
|
app.use(express.static("public"));
|
||||||
|
|
||||||
// Request logging middleware
|
// Request logging middleware
|
||||||
app.use((req, res, next) => {
|
app.use((req, res, next) => {
|
||||||
const start = Date.now();
|
const start = Date.now();
|
||||||
res.on('finish', () => {
|
res.on("finish", () => {
|
||||||
const responseTime = Date.now() - start;
|
const responseTime = Date.now() - start;
|
||||||
logger.logRequest(req, res, responseTime);
|
logger.logRequest(req, res, responseTime);
|
||||||
});
|
});
|
||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Global error handler
|
// Prometheus metrics middleware
|
||||||
app.use((err, req, res, next) => {
|
app.use(metrics.metricsMiddleware);
|
||||||
logger.error('Unhandled error:', err);
|
|
||||||
res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
message: 'Internal Server Error',
|
|
||||||
error: process.env.NODE_ENV === 'development' ? err.message : undefined
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Health check endpoint
|
// Health check endpoint
|
||||||
app.get('/health', async (req, res) => {
|
app.get("/health", async (req, res) => {
|
||||||
const redisHealthy = redisClient.isHealthy();
|
const redisHealthy = redisClient.isHealthy();
|
||||||
const fallbackActive = fallbackStore.isActive;
|
const fallbackActive = fallbackStore.isActive;
|
||||||
|
|
||||||
|
// Update metrics
|
||||||
|
metrics.updateRedisStatus(redisHealthy);
|
||||||
|
metrics.updateFallbackStatus(fallbackActive);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
status: 'ok',
|
status: "ok",
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
redis: {
|
redis: {
|
||||||
connected: redisHealthy,
|
connected: redisHealthy,
|
||||||
fallbackActive: fallbackActive
|
fallbackActive: fallbackActive,
|
||||||
},
|
},
|
||||||
uptime: process.uptime()
|
uptime: process.uptime(),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get all events endpoint
|
// Get all events endpoint
|
||||||
app.get('/events', async (req, res) => {
|
app.get("/events", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
let events;
|
let events;
|
||||||
|
|
||||||
@@ -65,19 +63,19 @@ app.get('/events', async (req, res) => {
|
|||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
events,
|
events,
|
||||||
usingFallback: fallbackStore.isActive
|
usingFallback: fallbackStore.isActive,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error fetching events:', error);
|
logger.error("Error fetching events:", error);
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: 'Failed to fetch events'
|
message: "Failed to fetch events",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get specific event stats
|
// Get specific event stats
|
||||||
app.get('/events/:eventId', async (req, res) => {
|
app.get("/events/:eventId", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const eventId = req.params.eventId;
|
const eventId = req.params.eventId;
|
||||||
let eventStats;
|
let eventStats;
|
||||||
@@ -91,26 +89,26 @@ app.get('/events/:eventId', async (req, res) => {
|
|||||||
if (!eventStats) {
|
if (!eventStats) {
|
||||||
return res.status(404).json({
|
return res.status(404).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: 'Event not found'
|
message: "Event not found",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
event: eventStats,
|
event: eventStats,
|
||||||
usingFallback: fallbackStore.isActive
|
usingFallback: fallbackStore.isActive,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error fetching event stats:', error);
|
logger.error("Error fetching event stats:", error);
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: 'Failed to fetch event stats'
|
message: "Failed to fetch event stats",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Purchase ticket endpoint (multi-event)
|
// Purchase ticket endpoint (multi-event)
|
||||||
app.post('/buy/:eventId', async (req, res) => {
|
app.post("/buy/:eventId", async (req, res) => {
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
const eventId = req.params.eventId;
|
const eventId = req.params.eventId;
|
||||||
const purchaseId = uuidv4();
|
const purchaseId = uuidv4();
|
||||||
@@ -122,7 +120,11 @@ app.post('/buy/:eventId', async (req, res) => {
|
|||||||
// Try Redis first
|
// Try Redis first
|
||||||
if (redisClient.isHealthy()) {
|
if (redisClient.isHealthy()) {
|
||||||
try {
|
try {
|
||||||
const luaResult = await redisClient.purchaseTicket(eventId, purchaseId, timestamp);
|
const luaResult = await redisClient.purchaseTicket(
|
||||||
|
eventId,
|
||||||
|
purchaseId,
|
||||||
|
timestamp
|
||||||
|
);
|
||||||
|
|
||||||
if (luaResult[0]) {
|
if (luaResult[0]) {
|
||||||
// Success - generate PDF ticket
|
// Success - generate PDF ticket
|
||||||
@@ -130,36 +132,52 @@ app.post('/buy/:eventId', async (req, res) => {
|
|||||||
// Get event details for PDF
|
// Get event details for PDF
|
||||||
const eventStats = await redisClient.getEventStats(eventId);
|
const eventStats = await redisClient.getEventStats(eventId);
|
||||||
|
|
||||||
|
const pdfStartTime = Date.now();
|
||||||
const pdfResult = await pdfGenerator.generateTicketPDF({
|
const pdfResult = await pdfGenerator.generateTicketPDF({
|
||||||
ticketId: luaResult[0],
|
ticketId: luaResult[0],
|
||||||
eventId,
|
eventId,
|
||||||
purchaseId,
|
purchaseId,
|
||||||
eventName: eventStats?.name || `Event ${eventId}`,
|
eventName: eventStats?.name || `Event ${eventId}`,
|
||||||
eventDescription: eventStats?.description || 'Event description not available',
|
eventDescription:
|
||||||
|
eventStats?.description || "Event description not available",
|
||||||
timestamp,
|
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 = {
|
result = {
|
||||||
success: true,
|
success: true,
|
||||||
ticket: luaResult[0],
|
ticket: luaResult[0],
|
||||||
purchaseId,
|
purchaseId,
|
||||||
eventId,
|
eventId,
|
||||||
soldCount: luaResult[2],
|
soldCount: luaResult[2],
|
||||||
message: 'Ticket purchased successfully!',
|
message: "Ticket purchased successfully!",
|
||||||
usingFallback: false,
|
usingFallback: false,
|
||||||
pdf: {
|
pdf: {
|
||||||
generated: pdfResult.success,
|
generated: pdfResult.success,
|
||||||
filename: pdfResult.filename,
|
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.logPurchase(eventId, luaResult[0], purchaseId, true);
|
||||||
logger.info(`PDF ticket generated for purchase ${purchaseId}`);
|
logger.info(`PDF ticket generated for purchase ${purchaseId}`);
|
||||||
|
|
||||||
} catch (pdfError) {
|
} catch (pdfError) {
|
||||||
logger.error('PDF generation failed:', pdfError);
|
logger.error("PDF generation failed:", pdfError);
|
||||||
|
|
||||||
// Still return success for ticket purchase, but note PDF failure
|
// Still return success for ticket purchase, but note PDF failure
|
||||||
result = {
|
result = {
|
||||||
@@ -168,64 +186,86 @@ app.post('/buy/:eventId', async (req, res) => {
|
|||||||
purchaseId,
|
purchaseId,
|
||||||
eventId,
|
eventId,
|
||||||
soldCount: luaResult[2],
|
soldCount: luaResult[2],
|
||||||
message: 'Ticket purchased successfully! (PDF generation failed)',
|
message: "Ticket purchased successfully! (PDF generation failed)",
|
||||||
usingFallback: false,
|
usingFallback: false,
|
||||||
pdf: {
|
pdf: {
|
||||||
generated: false,
|
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);
|
logger.logPurchase(eventId, luaResult[0], purchaseId, true);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Failed - handle specific error
|
// Failed - handle specific error
|
||||||
const errorCode = luaResult[1];
|
const errorCode = luaResult[1];
|
||||||
let statusCode = 400;
|
let statusCode = 400;
|
||||||
let message = 'Purchase failed';
|
let message = "Purchase failed";
|
||||||
|
|
||||||
switch (errorCode) {
|
switch (errorCode) {
|
||||||
case 'EVENT_NOT_FOUND':
|
case "EVENT_NOT_FOUND":
|
||||||
statusCode = 404;
|
statusCode = 404;
|
||||||
message = 'Event not found';
|
message = "Event not found";
|
||||||
break;
|
break;
|
||||||
case 'NO_TICKETS_AVAILABLE':
|
case "NO_TICKETS_AVAILABLE":
|
||||||
statusCode = 409;
|
statusCode = 409;
|
||||||
message = 'No tickets available for this event';
|
message = "No tickets available for this event";
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Record metrics for failed purchase
|
||||||
|
metrics.recordTicketSale(eventId, "failed");
|
||||||
|
|
||||||
logger.logPurchase(eventId, null, purchaseId, false, errorCode);
|
logger.logPurchase(eventId, null, purchaseId, false, errorCode);
|
||||||
return res.status(statusCode).json({
|
return res.status(statusCode).json({
|
||||||
success: false,
|
success: false,
|
||||||
message,
|
message,
|
||||||
errorCode,
|
errorCode,
|
||||||
eventId,
|
eventId,
|
||||||
purchaseId
|
purchaseId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (redisError) {
|
} catch (redisError) {
|
||||||
logger.error('Redis purchase failed, attempting fallback:', redisError);
|
logger.error("Redis purchase failed, attempting fallback:", redisError);
|
||||||
// Activate fallback if not already active
|
// Activate fallback if not already active
|
||||||
if (!fallbackStore.isActive) {
|
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
|
throw redisError; // Will be caught by outer try-catch for fallback
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Redis not available');
|
throw new Error("Redis not available");
|
||||||
}
|
}
|
||||||
|
|
||||||
const responseTime = Date.now() - startTime;
|
const responseTime = Date.now() - startTime;
|
||||||
result.responseTime = `${responseTime}ms`;
|
result.responseTime = `${responseTime}ms`;
|
||||||
|
|
||||||
res.json(result);
|
res.json(result);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Fallback to in-memory store
|
// Fallback to in-memory store
|
||||||
try {
|
try {
|
||||||
if (!fallbackStore.isActive) {
|
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);
|
const fallbackResult = fallbackStore.purchaseTicket(eventId, purchaseId);
|
||||||
@@ -240,9 +280,10 @@ app.post('/buy/:eventId', async (req, res) => {
|
|||||||
eventId,
|
eventId,
|
||||||
purchaseId,
|
purchaseId,
|
||||||
eventName: eventStats?.name || `Event ${eventId}`,
|
eventName: eventStats?.name || `Event ${eventId}`,
|
||||||
eventDescription: eventStats?.description || 'Event description not available',
|
eventDescription:
|
||||||
|
eventStats?.description || "Event description not available",
|
||||||
timestamp,
|
timestamp,
|
||||||
soldCount: fallbackResult.soldCount
|
soldCount: fallbackResult.soldCount,
|
||||||
});
|
});
|
||||||
|
|
||||||
const responseTime = Date.now() - startTime;
|
const responseTime = Date.now() - startTime;
|
||||||
@@ -252,20 +293,29 @@ app.post('/buy/:eventId', async (req, res) => {
|
|||||||
purchaseId,
|
purchaseId,
|
||||||
eventId,
|
eventId,
|
||||||
soldCount: fallbackResult.soldCount,
|
soldCount: fallbackResult.soldCount,
|
||||||
message: 'Ticket purchased successfully (fallback mode)!',
|
message: "Ticket purchased successfully (fallback mode)!",
|
||||||
usingFallback: true,
|
usingFallback: true,
|
||||||
responseTime: `${responseTime}ms`,
|
responseTime: `${responseTime}ms`,
|
||||||
pdf: {
|
pdf: {
|
||||||
generated: pdfResult.success,
|
generated: pdfResult.success,
|
||||||
filename: pdfResult.filename,
|
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) {
|
} 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;
|
const responseTime = Date.now() - startTime;
|
||||||
res.json({
|
res.json({
|
||||||
@@ -274,105 +324,126 @@ app.post('/buy/:eventId', async (req, res) => {
|
|||||||
purchaseId,
|
purchaseId,
|
||||||
eventId,
|
eventId,
|
||||||
soldCount: fallbackResult.soldCount,
|
soldCount: fallbackResult.soldCount,
|
||||||
message: 'Ticket purchased successfully (fallback mode, PDF generation failed)!',
|
message:
|
||||||
|
"Ticket purchased successfully (fallback mode, PDF generation failed)!",
|
||||||
usingFallback: true,
|
usingFallback: true,
|
||||||
responseTime: `${responseTime}ms`,
|
responseTime: `${responseTime}ms`,
|
||||||
pdf: {
|
pdf: {
|
||||||
generated: false,
|
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 {
|
} else {
|
||||||
let statusCode = 400;
|
let statusCode = 400;
|
||||||
let message = 'Purchase failed';
|
let message = "Purchase failed";
|
||||||
|
|
||||||
switch (fallbackResult.error) {
|
switch (fallbackResult.error) {
|
||||||
case 'EVENT_NOT_FOUND':
|
case "EVENT_NOT_FOUND":
|
||||||
statusCode = 404;
|
statusCode = 404;
|
||||||
message = 'Event not found';
|
message = "Event not found";
|
||||||
break;
|
break;
|
||||||
case 'NO_TICKETS_AVAILABLE':
|
case "NO_TICKETS_AVAILABLE":
|
||||||
statusCode = 409;
|
statusCode = 409;
|
||||||
message = 'No tickets available for this event';
|
message = "No tickets available for this event";
|
||||||
break;
|
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({
|
res.status(statusCode).json({
|
||||||
success: false,
|
success: false,
|
||||||
message,
|
message,
|
||||||
errorCode: fallbackResult.error,
|
errorCode: fallbackResult.error,
|
||||||
eventId,
|
eventId,
|
||||||
purchaseId,
|
purchaseId,
|
||||||
usingFallback: true
|
usingFallback: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (fallbackError) {
|
} 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);
|
logger.logPurchase(eventId, null, purchaseId, false, fallbackError);
|
||||||
|
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: 'System temporarily unavailable',
|
message: "System temporarily unavailable",
|
||||||
eventId,
|
eventId,
|
||||||
purchaseId
|
purchaseId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Download ticket PDF endpoint
|
// Download ticket PDF endpoint
|
||||||
app.get('/tickets/:purchaseId', async (req, res) => {
|
app.get("/tickets/:purchaseId", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const purchaseId = req.params.purchaseId;
|
const purchaseId = req.params.purchaseId;
|
||||||
|
|
||||||
if (!pdfGenerator.ticketExists(purchaseId)) {
|
if (!pdfGenerator.ticketExists(purchaseId)) {
|
||||||
return res.status(404).json({
|
return res.status(404).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: 'Ticket not found'
|
message: "Ticket not found",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const filepath = pdfGenerator.getTicketPath(purchaseId);
|
const filepath = pdfGenerator.getTicketPath(purchaseId);
|
||||||
const filename = `ticket-${purchaseId}.pdf`;
|
const filename = `ticket-${purchaseId}.pdf`;
|
||||||
|
|
||||||
res.setHeader('Content-Type', 'application/pdf');
|
res.setHeader("Content-Type", "application/pdf");
|
||||||
res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
|
res.setHeader("Content-Disposition", `attachment; filename="${filename}"`);
|
||||||
|
|
||||||
const fileStream = require('fs').createReadStream(filepath);
|
const fileStream = require("fs").createReadStream(filepath);
|
||||||
fileStream.pipe(res);
|
fileStream.pipe(res);
|
||||||
|
|
||||||
logger.info(`PDF ticket downloaded: ${purchaseId}`);
|
logger.info(`PDF ticket downloaded: ${purchaseId}`);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error downloading ticket:', error);
|
logger.error("Error downloading ticket:", error);
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: 'Failed to download ticket'
|
message: "Failed to download ticket",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// PDF management endpoint
|
// PDF management endpoint
|
||||||
app.get('/admin/pdf-stats', async (req, res) => {
|
app.get("/admin/pdf-stats", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const stats = pdfGenerator.getStats();
|
const stats = pdfGenerator.getStats();
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
stats
|
stats,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error getting PDF stats:', error);
|
logger.error("Error getting PDF stats:", error);
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: 'Failed to get PDF statistics'
|
message: "Failed to get PDF statistics",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Cleanup old tickets endpoint
|
// Cleanup old tickets endpoint
|
||||||
app.post('/admin/cleanup-tickets', async (req, res) => {
|
app.post("/admin/cleanup-tickets", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const maxAgeHours = req.body.maxAgeHours || 24;
|
const maxAgeHours = req.body.maxAgeHours || 24;
|
||||||
const deletedCount = await pdfGenerator.cleanupOldTickets(maxAgeHours);
|
const deletedCount = await pdfGenerator.cleanupOldTickets(maxAgeHours);
|
||||||
@@ -380,19 +451,88 @@ app.post('/admin/cleanup-tickets', async (req, res) => {
|
|||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
message: `Cleaned up ${deletedCount} old tickets`,
|
message: `Cleaned up ${deletedCount} old tickets`,
|
||||||
deletedCount
|
deletedCount,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error cleaning up tickets:', error);
|
logger.error("Error cleaning up tickets:", error);
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
success: false,
|
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)
|
// Metrics endpoint (Prometheus compatible)
|
||||||
app.get('/metrics', async (req, res) => {
|
app.get("/metrics", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
let globalStats, events;
|
let globalStats, events;
|
||||||
|
|
||||||
@@ -416,17 +556,17 @@ app.get('/metrics', async (req, res) => {
|
|||||||
usingFallback: fallbackStore.isActive,
|
usingFallback: fallbackStore.isActive,
|
||||||
redisConnected: redisClient.isHealthy(),
|
redisConnected: redisClient.isHealthy(),
|
||||||
uptime: process.uptime(),
|
uptime: process.uptime(),
|
||||||
memoryUsage: process.memoryUsage()
|
memoryUsage: process.memoryUsage(),
|
||||||
},
|
},
|
||||||
pdf: pdfStats
|
pdf: pdfStats,
|
||||||
};
|
};
|
||||||
|
|
||||||
res.json(metrics);
|
res.json(metrics);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error generating metrics:', error);
|
logger.error("Error generating metrics:", error);
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: 'Failed to generate metrics'
|
message: "Failed to generate metrics",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -436,7 +576,10 @@ async function initializeServer() {
|
|||||||
try {
|
try {
|
||||||
// Connect to Redis
|
// Connect to Redis
|
||||||
await redisClient.connect();
|
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
|
// Start server
|
||||||
app.listen(port, () => {
|
app.listen(port, () => {
|
||||||
@@ -445,39 +588,108 @@ async function initializeServer() {
|
|||||||
logger.info(`📈 Metrics: http://localhost:${port}/metrics`);
|
logger.info(`📈 Metrics: http://localhost:${port}/metrics`);
|
||||||
logger.info(`🎫 Events: http://localhost:${port}/events`);
|
logger.info(`🎫 Events: http://localhost:${port}/events`);
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to initialize server:', error);
|
logger.error("Failed to initialize server:", error);
|
||||||
logger.warn('Starting server with fallback store only');
|
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, () => {
|
app.listen(port, () => {
|
||||||
logger.warn(`⚠️ Server running in FALLBACK MODE on port ${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
|
// Graceful shutdown
|
||||||
process.on('SIGINT', async () => {
|
process.on("SIGINT", async () => {
|
||||||
logger.info('Received SIGINT, shutting down gracefully...');
|
logger.info("Received SIGINT, shutting down gracefully...");
|
||||||
try {
|
try {
|
||||||
await redisClient.disconnect();
|
await redisClient.disconnect();
|
||||||
logger.info('Redis disconnected');
|
logger.info("Redis disconnected");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error disconnecting Redis:', error);
|
logger.error("Error disconnecting Redis:", error);
|
||||||
}
|
}
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
process.on('SIGTERM', async () => {
|
process.on("SIGTERM", async () => {
|
||||||
logger.info('Received SIGTERM, shutting down gracefully...');
|
logger.info("Received SIGTERM, shutting down gracefully...");
|
||||||
try {
|
try {
|
||||||
await redisClient.disconnect();
|
await redisClient.disconnect();
|
||||||
logger.info('Redis disconnected');
|
logger.info("Redis disconnected");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error disconnecting Redis:', error);
|
logger.error("Error disconnecting Redis:", error);
|
||||||
}
|
}
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
});
|
});
|
||||||
|
|||||||
+87
-14
@@ -1,4 +1,4 @@
|
|||||||
const logger = require('./logger');
|
const logger = require("./logger");
|
||||||
|
|
||||||
class FallbackStore {
|
class FallbackStore {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -7,20 +7,30 @@ class FallbackStore {
|
|||||||
totalEvents: 0,
|
totalEvents: 0,
|
||||||
totalTickets: 0,
|
totalTickets: 0,
|
||||||
totalSold: 0,
|
totalSold: 0,
|
||||||
lastSeeded: null
|
lastSeeded: null,
|
||||||
};
|
};
|
||||||
this.isActive = false;
|
this.isActive = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
activate(reason) {
|
activate(reason) {
|
||||||
this.isActive = true;
|
this.isActive = true;
|
||||||
logger.logFallback('In-Memory Store Activated', reason);
|
logger.logFallback("In-Memory Store Activated", reason);
|
||||||
logger.warn('⚠️ FALLBACK MODE: Using in-memory store - data will not persist!');
|
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() {
|
deactivate() {
|
||||||
this.isActive = false;
|
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) {
|
seedEvent(eventId, tickets, metadata) {
|
||||||
@@ -29,13 +39,15 @@ class FallbackStore {
|
|||||||
this.events.set(eventId, {
|
this.events.set(eventId, {
|
||||||
tickets: [...tickets], // Create a copy
|
tickets: [...tickets], // Create a copy
|
||||||
metadata: { ...metadata },
|
metadata: { ...metadata },
|
||||||
soldTickets: 0
|
soldTickets: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.globalStats.totalEvents++;
|
this.globalStats.totalEvents++;
|
||||||
this.globalStats.totalTickets += tickets.length;
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,11 +56,11 @@ class FallbackStore {
|
|||||||
|
|
||||||
const event = this.events.get(eventId);
|
const event = this.events.get(eventId);
|
||||||
if (!event) {
|
if (!event) {
|
||||||
return { success: false, error: 'EVENT_NOT_FOUND' };
|
return { success: false, error: "EVENT_NOT_FOUND" };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.tickets.length === 0) {
|
if (event.tickets.length === 0) {
|
||||||
return { success: false, error: 'NO_TICKETS_AVAILABLE' };
|
return { success: false, error: "NO_TICKETS_AVAILABLE" };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Atomically remove a ticket
|
// Atomically remove a ticket
|
||||||
@@ -63,7 +75,7 @@ class FallbackStore {
|
|||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
ticket,
|
ticket,
|
||||||
soldCount: event.soldTickets
|
soldCount: event.soldTickets,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,7 +93,7 @@ class FallbackStore {
|
|||||||
soldTickets: event.soldTickets,
|
soldTickets: event.soldTickets,
|
||||||
remainingTickets: event.tickets.length,
|
remainingTickets: event.tickets.length,
|
||||||
createdAt: event.metadata.createdAt,
|
createdAt: event.metadata.createdAt,
|
||||||
lastSoldAt: event.metadata.lastSoldAt || 'never'
|
lastSoldAt: event.metadata.lastSoldAt || "never",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,9 +125,9 @@ class FallbackStore {
|
|||||||
totalEvents: 0,
|
totalEvents: 0,
|
||||||
totalTickets: 0,
|
totalTickets: 0,
|
||||||
totalSold: 0,
|
totalSold: 0,
|
||||||
lastSeeded: null
|
lastSeeded: null,
|
||||||
};
|
};
|
||||||
logger.info('Fallback store cleared');
|
logger.info("Fallback store cleared");
|
||||||
}
|
}
|
||||||
|
|
||||||
getStatus() {
|
getStatus() {
|
||||||
@@ -123,9 +135,70 @@ class FallbackStore {
|
|||||||
active: this.isActive,
|
active: this.isActive,
|
||||||
eventsCount: this.events.size,
|
eventsCount: this.events.size,
|
||||||
totalTickets: this.globalStats.totalTickets,
|
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();
|
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 redis = require("redis");
|
||||||
const fs = require('fs');
|
const fs = require("fs");
|
||||||
const path = require('path');
|
const path = require("path");
|
||||||
const logger = require('./logger');
|
const logger = require("./logger");
|
||||||
|
|
||||||
class RedisClient {
|
class RedisClient {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -13,28 +13,28 @@ class RedisClient {
|
|||||||
|
|
||||||
async connect() {
|
async connect() {
|
||||||
try {
|
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 = redis.createClient({ url: redisUrl });
|
||||||
|
|
||||||
this.client.on('error', (err) => {
|
this.client.on("error", (err) => {
|
||||||
logger.error('Redis Client Error:', err);
|
logger.error("Redis Client Error:", err);
|
||||||
this.isConnected = false;
|
this.isConnected = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
this.client.on('connect', () => {
|
this.client.on("connect", () => {
|
||||||
logger.info('Redis client connected');
|
logger.info("Redis client connected");
|
||||||
this.isConnected = true;
|
this.isConnected = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
this.client.on('disconnect', () => {
|
this.client.on("disconnect", () => {
|
||||||
logger.warn('Redis client disconnected');
|
logger.warn("Redis client disconnected");
|
||||||
this.isConnected = false;
|
this.isConnected = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.client.connect();
|
await this.client.connect();
|
||||||
return this.client;
|
return this.client;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to connect to Redis:', error);
|
logger.error("Failed to connect to Redis:", error);
|
||||||
this.isConnected = false;
|
this.isConnected = false;
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
@@ -42,78 +42,74 @@ class RedisClient {
|
|||||||
|
|
||||||
loadLuaScripts() {
|
loadLuaScripts() {
|
||||||
try {
|
try {
|
||||||
const luaDir = path.join(__dirname, '../lua');
|
const luaDir = path.join(__dirname, "../lua");
|
||||||
|
|
||||||
// Load purchase ticket script
|
// Load purchase ticket script
|
||||||
const purchaseScript = fs.readFileSync(
|
const purchaseScript = fs.readFileSync(
|
||||||
path.join(luaDir, 'purchase-ticket.lua'),
|
path.join(luaDir, "purchase-ticket.lua"),
|
||||||
'utf8'
|
"utf8"
|
||||||
);
|
);
|
||||||
this.luaScripts.purchaseTicket = purchaseScript;
|
this.luaScripts.purchaseTicket = purchaseScript;
|
||||||
|
|
||||||
// Load event stats script
|
// Load event stats script
|
||||||
const statsScript = fs.readFileSync(
|
const statsScript = fs.readFileSync(
|
||||||
path.join(luaDir, 'get-event-stats.lua'),
|
path.join(luaDir, "get-event-stats.lua"),
|
||||||
'utf8'
|
"utf8"
|
||||||
);
|
);
|
||||||
this.luaScripts.getEventStats = statsScript;
|
this.luaScripts.getEventStats = statsScript;
|
||||||
|
|
||||||
logger.info('Lua scripts loaded successfully');
|
logger.info("Lua scripts loaded successfully");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to load Lua scripts:', error);
|
logger.error("Failed to load Lua scripts:", error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async purchaseTicket(eventId, purchaseId, timestamp) {
|
async purchaseTicket(eventId, purchaseId, timestamp) {
|
||||||
if (!this.isConnected) {
|
if (!this.isConnected) {
|
||||||
throw new Error('Redis not connected');
|
throw new Error("Redis not connected");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate event exists before attempting purchase
|
// Validate event exists before attempting purchase
|
||||||
const eventExists = await this.client.exists(`event:${eventId}:meta`);
|
const eventExists = await this.client.exists(`event:${eventId}:meta`);
|
||||||
if (!eventExists) {
|
if (!eventExists) {
|
||||||
logger.warn(`Event ${eventId} does not exist`);
|
logger.warn(`Event ${eventId} does not exist`);
|
||||||
return [null, 'EVENT_NOT_FOUND'];
|
return [null, "EVENT_NOT_FOUND"];
|
||||||
}
|
}
|
||||||
|
|
||||||
const keys = [
|
const keys = [
|
||||||
`event:${eventId}:tickets`,
|
`event:${eventId}:tickets`,
|
||||||
`event:${eventId}:meta`,
|
`event:${eventId}:meta`,
|
||||||
'global:stats'
|
"global:stats",
|
||||||
];
|
];
|
||||||
// Ensure all arguments are strings as required by Redis Lua
|
// Ensure all arguments are strings as required by Redis Lua
|
||||||
const args = [String(timestamp), String(purchaseId)];
|
const args = [String(timestamp), String(purchaseId)];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await this.client.eval(
|
const result = await this.client.eval(this.luaScripts.purchaseTicket, {
|
||||||
this.luaScripts.purchaseTicket,
|
keys,
|
||||||
{ keys, arguments: args }
|
arguments: args,
|
||||||
);
|
});
|
||||||
return result;
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error executing purchase ticket script:', error);
|
logger.error("Error executing purchase ticket script:", error);
|
||||||
logger.error('Script keys:', keys);
|
logger.error("Script keys:", keys);
|
||||||
logger.error('Script args:', args);
|
logger.error("Script args:", args);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getEventStats(eventId) {
|
async getEventStats(eventId) {
|
||||||
if (!this.isConnected) {
|
if (!this.isConnected) {
|
||||||
throw new Error('Redis not connected');
|
throw new Error("Redis not connected");
|
||||||
}
|
}
|
||||||
|
|
||||||
const keys = [
|
const keys = [`event:${eventId}:meta`, `event:${eventId}:tickets`];
|
||||||
`event:${eventId}:meta`,
|
|
||||||
`event:${eventId}:tickets`
|
|
||||||
];
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await this.client.eval(
|
const result = await this.client.eval(this.luaScripts.getEventStats, {
|
||||||
this.luaScripts.getEventStats,
|
keys,
|
||||||
{ keys }
|
});
|
||||||
);
|
|
||||||
|
|
||||||
if (!result) return null;
|
if (!result) return null;
|
||||||
|
|
||||||
@@ -125,21 +121,21 @@ class RedisClient {
|
|||||||
soldTickets: parseInt(result[4]),
|
soldTickets: parseInt(result[4]),
|
||||||
remainingTickets: parseInt(result[5]),
|
remainingTickets: parseInt(result[5]),
|
||||||
createdAt: result[6],
|
createdAt: result[6],
|
||||||
lastSoldAt: result[7]
|
lastSoldAt: result[7],
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error executing get event stats script:', error);
|
logger.error("Error executing get event stats script:", error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAllEvents() {
|
async getAllEvents() {
|
||||||
if (!this.isConnected) {
|
if (!this.isConnected) {
|
||||||
throw new Error('Redis not connected');
|
throw new Error("Redis not connected");
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const eventKeys = await this.client.keys('event:*:meta');
|
const eventKeys = await this.client.keys("event:*:meta");
|
||||||
const events = [];
|
const events = [];
|
||||||
|
|
||||||
for (const key of eventKeys) {
|
for (const key of eventKeys) {
|
||||||
@@ -152,30 +148,48 @@ class RedisClient {
|
|||||||
|
|
||||||
return events;
|
return events;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error getting all events:', error);
|
logger.error("Error getting all events:", error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getGlobalStats() {
|
async getGlobalStats() {
|
||||||
if (!this.isConnected) {
|
if (!this.isConnected) {
|
||||||
throw new Error('Redis not connected');
|
throw new Error("Redis not connected");
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const stats = await this.client.hGetAll('global:stats');
|
const stats = await this.client.hGetAll("global:stats");
|
||||||
return {
|
return {
|
||||||
totalEvents: parseInt(stats.totalEvents) || 0,
|
totalEvents: parseInt(stats.totalEvents) || 0,
|
||||||
totalTickets: parseInt(stats.totalTickets) || 0,
|
totalTickets: parseInt(stats.totalTickets) || 0,
|
||||||
totalSold: parseInt(stats.totalSold) || 0,
|
totalSold: parseInt(stats.totalSold) || 0,
|
||||||
lastSeeded: stats.lastSeeded || null
|
lastSeeded: stats.lastSeeded || null,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error getting global stats:', error);
|
logger.error("Error getting global stats:", error);
|
||||||
throw 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() {
|
getClient() {
|
||||||
return this.client;
|
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