Compare commits
2 Commits
42fec5708a
...
43ae10d7dd
| Author | SHA1 | Date | |
|---|---|---|---|
| 43ae10d7dd | |||
| 064ae104f7 |
@@ -0,0 +1,19 @@
|
||||
node_modules
|
||||
npm-debug.log
|
||||
.git
|
||||
.gitignore
|
||||
README.md
|
||||
.env
|
||||
.nyc_output
|
||||
coverage
|
||||
.DS_Store
|
||||
*.log
|
||||
logs/*
|
||||
tickets/*
|
||||
.vscode
|
||||
.idea
|
||||
*.md
|
||||
!design.md
|
||||
Dockerfile
|
||||
docker-compose.yml
|
||||
prometheus.yml
|
||||
@@ -1,21 +0,0 @@
|
||||
# Server Configuration
|
||||
PORT=3049
|
||||
NODE_ENV=development
|
||||
|
||||
# Redis Configuration
|
||||
REDIS_URL=redis://localhost:6379
|
||||
REDIS_HOST=localhost
|
||||
REDIS_PORT=6379
|
||||
|
||||
# Event Configuration
|
||||
DEFAULT_TICKETS_PER_EVENT=10000
|
||||
|
||||
# Logging Configuration
|
||||
LOG_LEVEL=info
|
||||
LOG_FILE=logs/app.log
|
||||
|
||||
# PDF Configuration
|
||||
PDF_OUTPUT_DIR=./tickets
|
||||
|
||||
# Metrics Configuration
|
||||
METRICS_PORT=9090
|
||||
+4
-1
@@ -1,3 +1,6 @@
|
||||
/node_modules
|
||||
/dist
|
||||
/package-lock.json
|
||||
/package-lock.json
|
||||
/tickets
|
||||
/logs
|
||||
.env
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
# Use official Node.js runtime as base image
|
||||
FROM node:18-alpine
|
||||
|
||||
# Set working directory in container
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package files
|
||||
COPY package*.json ./
|
||||
|
||||
# Install dependencies
|
||||
RUN npm ci --only=production
|
||||
|
||||
# Copy application code
|
||||
COPY . .
|
||||
|
||||
# Create necessary directories
|
||||
RUN mkdir -p logs tickets
|
||||
|
||||
# Create non-root user for security
|
||||
RUN addgroup -g 1001 -S nodejs
|
||||
RUN adduser -S nodejs -u 1001
|
||||
|
||||
# Change ownership of app directory
|
||||
RUN chown -R nodejs:nodejs /app
|
||||
|
||||
# Switch to non-root user
|
||||
USER nodejs
|
||||
|
||||
# Expose port
|
||||
EXPOSE 3049
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||
CMD node -e "require('http').get('http://localhost:3049/health', (res) => { process.exit(res.statusCode === 200 ? 0 : 1) }).on('error', () => process.exit(1))"
|
||||
|
||||
# Start the application
|
||||
CMD ["npm", "start"]
|
||||
@@ -42,34 +42,128 @@ Your task is to extract the high-throughput ticket purchasing component and exte
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Node.js (v14+ recommended)
|
||||
- npm
|
||||
- Redis (installed locally or via Docker, as per the provided docker-compose configuration)
|
||||
- Node.js (v18+ recommended)
|
||||
- npm or yarn
|
||||
- Docker and Docker Compose
|
||||
- Git
|
||||
|
||||
### Setup
|
||||
### Quick Start
|
||||
|
||||
1. Clone the repository.
|
||||
2. Install dependencies:
|
||||
1. **Clone the repository**
|
||||
```bash
|
||||
git clone <repository-url>
|
||||
cd module4_backend_project
|
||||
```
|
||||
|
||||
2. **Install dependencies**
|
||||
```bash
|
||||
npm install
|
||||
3. (Optional) Copy the environment variable template:
|
||||
```
|
||||
|
||||
3. **Set up environment**
|
||||
```bash
|
||||
cp .env.example .env
|
||||
4. Seed the Redis store with tickets for multiple events. You might modify the seeding script to handle multiple event keys (e.g., `event:1:tickets`, `event:2:tickets`, etc.).
|
||||
5. Start the application:
|
||||
npm start
|
||||
# Edit .env file if needed
|
||||
```
|
||||
|
||||
4. **Start with Docker (Recommended)**
|
||||
```bash
|
||||
# Start core services (Redis + App)
|
||||
docker-compose up -d
|
||||
|
||||
# Or start with monitoring (Prometheus + Grafana)
|
||||
docker-compose --profile monitoring up -d
|
||||
```
|
||||
|
||||
5. **Seed the database**
|
||||
```bash
|
||||
# Seed 5 events with 10,000 tickets each
|
||||
npm run seed
|
||||
|
||||
# Custom seeding: 3 events with 5,000 tickets each
|
||||
npm run seed 3 5000
|
||||
```
|
||||
|
||||
### Manual Setup (Development)
|
||||
|
||||
If you prefer to run components separately:
|
||||
|
||||
1. **Start Redis**
|
||||
```bash
|
||||
docker-compose up -d redis
|
||||
```
|
||||
|
||||
2. **Start the application**
|
||||
```bash
|
||||
npm run dev # Development with auto-reload
|
||||
# or
|
||||
npm start # Production mode
|
||||
```
|
||||
|
||||
### API Endpoints
|
||||
|
||||
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 |
|
||||
|
||||
### Load Testing
|
||||
|
||||
Simulate high load using a tool like [autocannon](https://github.com/mcollina/autocannon) or [wrk](https://github.com/wg/wrk). For example, to simulate 5000 concurrent connections on event 1:
|
||||
The system includes a comprehensive load testing framework:
|
||||
|
||||
npx autocannon -c 5000 -d 30 http://localhost:3049/buy/1
|
||||
```bash
|
||||
# Run full test suite (5000+ concurrent connections)
|
||||
npm run test:load -- --full
|
||||
|
||||
### Metrics
|
||||
# Test specific event
|
||||
npm run test:load -- --event 1 --connections 5000 --duration 30
|
||||
|
||||
Access real-time service metrics at:
|
||||
# Multi-event concurrent testing
|
||||
npm run test:load -- --multi --events 1,2,3 --connections 6000
|
||||
|
||||
http://localhost:3049/metrics
|
||||
# Custom load test
|
||||
node tests/load-test.js --event 2 --connections 1000 --duration 10
|
||||
```
|
||||
|
||||
These metrics should include data on tickets sold, remaining tickets per event, and any instances where the fallback mechanism was activated.
|
||||
### Monitoring & Metrics
|
||||
|
||||
#### Application Metrics
|
||||
Access real-time service metrics at: http://localhost:3049/metrics
|
||||
|
||||
#### Prometheus (if enabled)
|
||||
Prometheus dashboard: http://localhost:9090
|
||||
|
||||
#### Grafana (if enabled)
|
||||
Grafana dashboard: http://localhost:3000
|
||||
- Username: `admin`
|
||||
- Password: `admin`
|
||||
|
||||
### Docker Commands
|
||||
|
||||
```bash
|
||||
# Start core services
|
||||
docker-compose up -d
|
||||
|
||||
# Start with monitoring
|
||||
docker-compose --profile monitoring up -d
|
||||
|
||||
# View logs
|
||||
docker-compose logs -f app
|
||||
|
||||
# Stop services
|
||||
docker-compose down
|
||||
|
||||
# Rebuild and restart
|
||||
docker-compose up -d --build
|
||||
```
|
||||
|
||||
## Evaluation Criteria
|
||||
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
const redis = require('redis');
|
||||
require('dotenv').config();
|
||||
|
||||
async function debugEvents() {
|
||||
const client = redis.createClient({
|
||||
url: process.env.REDIS_URL || 'redis://localhost:6379'
|
||||
});
|
||||
|
||||
try {
|
||||
await client.connect();
|
||||
console.log('✅ Connected to Redis');
|
||||
|
||||
// Check what event keys exist
|
||||
const eventKeys = await client.keys('event:*');
|
||||
console.log('\n📋 Found Redis keys:', eventKeys);
|
||||
|
||||
// Check global stats
|
||||
const globalStats = await client.hGetAll('global:stats');
|
||||
console.log('\n🌍 Global stats:', globalStats);
|
||||
|
||||
// Check each event
|
||||
const metaKeys = eventKeys.filter(key => key.includes(':meta'));
|
||||
console.log('\n🎫 Event Details:');
|
||||
|
||||
for (const metaKey of metaKeys) {
|
||||
const eventId = metaKey.match(/event:(\d+):meta/)[1];
|
||||
const ticketKey = `event:${eventId}:tickets`;
|
||||
|
||||
const meta = await client.hGetAll(metaKey);
|
||||
const ticketCount = await client.lLen(ticketKey);
|
||||
|
||||
console.log(`\n Event ${eventId}:`);
|
||||
console.log(` Name: ${meta.name}`);
|
||||
console.log(` Total Tickets: ${meta.totalTickets}`);
|
||||
console.log(` Sold Tickets: ${meta.soldTickets}`);
|
||||
console.log(` Remaining: ${ticketCount}`);
|
||||
console.log(` Created: ${meta.createdAt}`);
|
||||
}
|
||||
|
||||
// Test if we can check existence of event 5
|
||||
const event5Exists = await client.exists('event:5:meta');
|
||||
console.log(`\n🔍 Event 5 exists: ${event5Exists ? 'YES' : 'NO'}`);
|
||||
|
||||
if (event5Exists) {
|
||||
const event5Meta = await client.hGetAll('event:5:meta');
|
||||
const event5Tickets = await client.lLen('event:5:tickets');
|
||||
console.log('📊 Event 5 details:', event5Meta);
|
||||
console.log(`🎫 Event 5 remaining tickets: ${event5Tickets}`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Error:', error);
|
||||
} finally {
|
||||
await client.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
debugEvents();
|
||||
@@ -0,0 +1,329 @@
|
||||
# Ticket Scaling Microservice - Design Document
|
||||
|
||||
## Table of Contents
|
||||
1. [Architecture Overview](#architecture-overview)
|
||||
2. [System Components](#system-components)
|
||||
3. [Scalability Strategies](#scalability-strategies)
|
||||
4. [Atomic Operations](#atomic-operations)
|
||||
5. [Fallback Mechanisms](#fallback-mechanisms)
|
||||
6. [Performance Optimizations](#performance-optimizations)
|
||||
7. [Monitoring & Observability](#monitoring--observability)
|
||||
8. [Security Considerations](#security-considerations)
|
||||
9. [Deployment Strategy](#deployment-strategy)
|
||||
10. [Future Enhancements](#future-enhancements)
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
### High-Level Architecture
|
||||
```
|
||||
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
||||
│ Load Balancer │ │ Prometheus │ │ Grafana │
|
||||
│ (Optional) │ │ Monitoring │ │ Dashboard │
|
||||
└─────────────────┘ └─────────────────┘ └─────────────────┘
|
||||
│ │ │
|
||||
│ │ │
|
||||
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
||||
│ │ │ │ │ │
|
||||
│ Ticket Service │◄───┤ Redis │ │ In-Memory │
|
||||
│ (Node.js/ │ │ Primary Store │ │ Fallback Store │
|
||||
│ Express) │ │ │ │ │
|
||||
│ │ │ │ │ │
|
||||
└─────────────────┘ └─────────────────┘ └─────────────────┘
|
||||
│
|
||||
│
|
||||
┌─────────────────┐
|
||||
│ PDF Generator │
|
||||
│ (PDFKit) │
|
||||
└─────────────────┘
|
||||
```
|
||||
|
||||
### 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
|
||||
4. **Observability**: Comprehensive logging and metrics
|
||||
5. **Performance**: Optimized for high-throughput scenarios
|
||||
|
||||
## System Components
|
||||
|
||||
### 1. Core Application (server.js)
|
||||
- **Technology**: Node.js with Express framework
|
||||
- **Responsibilities**:
|
||||
- HTTP request handling
|
||||
- Business logic orchestration
|
||||
- Error handling and logging
|
||||
- PDF generation coordination
|
||||
|
||||
### 2. Redis Client (redis-client.js)
|
||||
- **Technology**: Redis with Lua scripting
|
||||
- **Responsibilities**:
|
||||
- Atomic ticket operations
|
||||
- Event metadata management
|
||||
- Connection health monitoring
|
||||
- Script execution
|
||||
|
||||
### 3. Fallback Store (fallback-store.js)
|
||||
- **Technology**: In-memory JavaScript Map
|
||||
- **Responsibilities**:
|
||||
- Emergency ticket storage
|
||||
- Temporary operation continuity
|
||||
- Graceful degradation
|
||||
|
||||
### 4. PDF Generator (pdf-generator.js)
|
||||
- **Technology**: PDFKit library
|
||||
- **Responsibilities**:
|
||||
- Professional ticket generation
|
||||
- File management
|
||||
- Cleanup operations
|
||||
|
||||
### 5. Logging System (logger.js)
|
||||
- **Technology**: Winston logging framework
|
||||
- **Responsibilities**:
|
||||
- Structured logging
|
||||
- Request tracking
|
||||
- Error reporting
|
||||
- Performance metrics
|
||||
|
||||
## Scalability Strategies
|
||||
|
||||
### Horizontal Scaling
|
||||
```
|
||||
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
||||
│ Instance 1 │ │ Instance 2 │ │ Instance N │
|
||||
│ Port: 3049 │ │ Port: 3050 │ │ Port: 305X │
|
||||
└─────────────────┘ └─────────────────┘ └─────────────────┘
|
||||
│ │ │
|
||||
└───────────────────────┼───────────────────────┘
|
||||
│
|
||||
┌─────────────────┐
|
||||
│ Shared Redis │
|
||||
│ Cluster │
|
||||
└─────────────────┘
|
||||
```
|
||||
|
||||
**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 │
|
||||
│ (Read/Write) │───▶│ (Read Only) │ │ (Read Only) │
|
||||
└─────────────────┘ └─────────────────┘ └─────────────────┘
|
||||
```
|
||||
|
||||
**Strategies**:
|
||||
- Redis clustering for horizontal scaling
|
||||
- Read replicas for metrics/stats queries
|
||||
- Sharding by event ID for massive scale
|
||||
|
||||
## Atomic Operations
|
||||
|
||||
### Lua Script Design
|
||||
Our core purchase operation uses a Redis Lua script to ensure atomicity:
|
||||
|
||||
```lua
|
||||
-- Atomic ticket purchase script
|
||||
local ticketKey = KEYS[1] -- event:X:tickets
|
||||
local metaKey = KEYS[2] -- event:X:meta
|
||||
local globalKey = KEYS[3] -- global:stats
|
||||
|
||||
-- Atomic operations:
|
||||
1. Check event exists
|
||||
2. Pop ticket from list
|
||||
3. Update sold count
|
||||
4. Update global stats
|
||||
5. Store purchase record
|
||||
```
|
||||
|
||||
**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
|
||||
|
||||
## 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 │
|
||||
└─────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐ ┌─────────────────┐
|
||||
│ Try Redis │───▶│ Redis Success │
|
||||
│ Operation │ │ Return Result │
|
||||
└─────────────────┘ └─────────────────┘
|
||||
│
|
||||
▼ (On Failure)
|
||||
┌─────────────────┐ ┌─────────────────┐
|
||||
│ Activate │───▶│ In-Memory │
|
||||
│ Fallback Store │ │ Operation │
|
||||
└─────────────────┘ └─────────────────┘
|
||||
```
|
||||
|
||||
### Fallback Limitations
|
||||
- **Non-Persistent**: Data lost on restart
|
||||
- **Single Instance**: No cross-instance synchronization
|
||||
- **Capacity Limited**: Memory constraints
|
||||
- **Warning Logs**: Clear indication of degraded mode
|
||||
|
||||
## 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
|
||||
4. **Error Isolation**: PDF failures don't affect purchases
|
||||
|
||||
## Monitoring & Observability
|
||||
|
||||
### Metrics Collection
|
||||
```json
|
||||
{
|
||||
"global": {
|
||||
"totalEvents": 5,
|
||||
"totalTickets": 50000,
|
||||
"totalSold": 1250
|
||||
},
|
||||
"events": [
|
||||
{
|
||||
"eventId": "1",
|
||||
"soldTickets": 250,
|
||||
"remainingTickets": 9750
|
||||
}
|
||||
],
|
||||
"system": {
|
||||
"usingFallback": false,
|
||||
"redisConnected": true,
|
||||
"uptime": 3600,
|
||||
"memoryUsage": {...}
|
||||
},
|
||||
"pdf": {
|
||||
"totalTickets": 1250,
|
||||
"totalSizeMB": "15.6"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 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
|
||||
- **Resource Usage**: Memory and CPU monitoring
|
||||
|
||||
## 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
|
||||
|
||||
## Deployment Strategy
|
||||
|
||||
### Development Environment
|
||||
```bash
|
||||
# Local development
|
||||
npm install
|
||||
npm run docker:up # Start Redis
|
||||
npm run seed # Seed events
|
||||
npm run dev # Start with nodemon
|
||||
```
|
||||
|
||||
### Production Environment
|
||||
```bash
|
||||
# Docker deployment
|
||||
docker-compose up -d # Core services
|
||||
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
|
||||
- **Resource Limits**: CPU and memory constraints
|
||||
|
||||
## 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
|
||||
4. **Audit Trails**: Comprehensive logging
|
||||
|
||||
## Conclusion
|
||||
|
||||
This design provides a robust, scalable foundation for high-volume ticket sales with the following key strengths:
|
||||
|
||||
- **Atomic Operations**: Guaranteed consistency under load
|
||||
- **High Availability**: Graceful degradation capabilities
|
||||
- **Observability**: Comprehensive monitoring and logging
|
||||
- **Scalability**: Horizontal and vertical scaling support
|
||||
- **Performance**: Optimized for high-throughput scenarios
|
||||
|
||||
The architecture successfully handles the challenge requirements of processing thousands of concurrent requests while maintaining data integrity and system reliability.
|
||||
+71
-1
@@ -1,10 +1,80 @@
|
||||
version: "3"
|
||||
version: "3.8"
|
||||
services:
|
||||
redis:
|
||||
image: redis:alpine
|
||||
container_name: ticket-redis
|
||||
ports:
|
||||
- "6379:6379"
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
command: redis-server --appendonly yes
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 10s
|
||||
timeout: 3s
|
||||
retries: 3
|
||||
networks:
|
||||
- ticket-network
|
||||
|
||||
app:
|
||||
build: .
|
||||
container_name: ticket-microservice
|
||||
ports:
|
||||
- "3049:3049"
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- REDIS_URL=redis://redis:6379
|
||||
- PORT=3049
|
||||
- LOG_LEVEL=info
|
||||
depends_on:
|
||||
redis:
|
||||
condition: service_healthy
|
||||
volumes:
|
||||
- ./logs:/app/logs
|
||||
- ./tickets:/app/tickets
|
||||
networks:
|
||||
- ticket-network
|
||||
restart: unless-stopped
|
||||
|
||||
prometheus:
|
||||
image: prom/prometheus:latest
|
||||
container_name: ticket-prometheus
|
||||
ports:
|
||||
- "9090:9090"
|
||||
volumes:
|
||||
- ./prometheus.yml:/etc/prometheus/prometheus.yml
|
||||
- prometheus_data:/prometheus
|
||||
command:
|
||||
- '--config.file=/etc/prometheus/prometheus.yml'
|
||||
- '--storage.tsdb.path=/prometheus'
|
||||
- '--web.console.libraries=/etc/prometheus/console_libraries'
|
||||
- '--web.console.templates=/etc/prometheus/consoles'
|
||||
- '--storage.tsdb.retention.time=200h'
|
||||
- '--web.enable-lifecycle'
|
||||
networks:
|
||||
- ticket-network
|
||||
profiles:
|
||||
- monitoring
|
||||
|
||||
grafana:
|
||||
image: grafana/grafana:latest
|
||||
container_name: ticket-grafana
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
- GF_SECURITY_ADMIN_PASSWORD=admin
|
||||
volumes:
|
||||
- grafana_data:/var/lib/grafana
|
||||
networks:
|
||||
- ticket-network
|
||||
profiles:
|
||||
- monitoring
|
||||
|
||||
volumes:
|
||||
redis_data:
|
||||
prometheus_data:
|
||||
grafana_data:
|
||||
|
||||
networks:
|
||||
ticket-network:
|
||||
driver: bridge
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
global:
|
||||
scrape_interval: 15s
|
||||
evaluation_interval: 15s
|
||||
|
||||
rule_files:
|
||||
# - "first_rules.yml"
|
||||
# - "second_rules.yml"
|
||||
|
||||
scrape_configs:
|
||||
- job_name: 'prometheus'
|
||||
static_configs:
|
||||
- targets: ['localhost:9090']
|
||||
|
||||
- job_name: 'ticket-microservice'
|
||||
static_configs:
|
||||
- targets: ['app:3049']
|
||||
metrics_path: '/metrics'
|
||||
scrape_interval: 5s
|
||||
scrape_timeout: 3s
|
||||
@@ -32,13 +32,14 @@ redis.call('HINCRBY', globalKey, 'totalSold', 1)
|
||||
|
||||
-- Store purchase record
|
||||
local purchaseKey = 'purchase:' .. purchaseId
|
||||
redis.call('HSET', purchaseKey, {
|
||||
local eventIdFromKey = string.match(ticketKey, 'event:(%d+):tickets')
|
||||
redis.call('HSET', purchaseKey,
|
||||
'ticketId', ticket,
|
||||
'eventId', string.match(ticketKey, 'event:(%d+):tickets'),
|
||||
'eventId', eventIdFromKey,
|
||||
'purchaseId', purchaseId,
|
||||
'timestamp', timestamp,
|
||||
'status', 'completed'
|
||||
})
|
||||
)
|
||||
|
||||
-- Set expiration for purchase record (24 hours)
|
||||
redis.call('EXPIRE', purchaseKey, 86400)
|
||||
|
||||
+97
-77
@@ -1,11 +1,11 @@
|
||||
const PDFDocument = require('pdfkit');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const logger = require('./logger');
|
||||
const PDFDocument = require("pdfkit");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const logger = require("./logger");
|
||||
|
||||
class PDFGenerator {
|
||||
constructor() {
|
||||
this.outputDir = process.env.PDF_OUTPUT_DIR || './tickets';
|
||||
this.outputDir = process.env.PDF_OUTPUT_DIR || "./tickets";
|
||||
this.ensureOutputDirectory();
|
||||
}
|
||||
|
||||
@@ -26,13 +26,13 @@ class PDFGenerator {
|
||||
eventName,
|
||||
eventDescription,
|
||||
timestamp,
|
||||
soldCount
|
||||
soldCount,
|
||||
} = ticketData;
|
||||
|
||||
// Create PDF document
|
||||
const doc = new PDFDocument({
|
||||
size: 'A4',
|
||||
margin: 50
|
||||
size: "A4",
|
||||
margin: 50,
|
||||
});
|
||||
|
||||
// Generate filename
|
||||
@@ -44,97 +44,114 @@ class PDFGenerator {
|
||||
doc.pipe(stream);
|
||||
|
||||
// Header
|
||||
doc.fontSize(24)
|
||||
.fillColor('#2c3e50')
|
||||
.text('🎫 TICKET RECEIPT', 50, 50, { align: 'center' });
|
||||
doc
|
||||
.fontSize(24)
|
||||
.fillColor("#2c3e50")
|
||||
.text("TICKET RECEIPT", 50, 50, { align: "center" });
|
||||
|
||||
// Divider line
|
||||
doc.moveTo(50, 90)
|
||||
.lineTo(545, 90)
|
||||
.strokeColor('#3498db')
|
||||
.lineWidth(2)
|
||||
.stroke();
|
||||
doc
|
||||
.moveTo(50, 90)
|
||||
.lineTo(545, 90)
|
||||
.strokeColor("#3498db")
|
||||
.lineWidth(2)
|
||||
.stroke();
|
||||
|
||||
// Event Information
|
||||
doc.fontSize(18)
|
||||
.fillColor('#2c3e50')
|
||||
.text('Event Information', 50, 120);
|
||||
doc
|
||||
.fontSize(18)
|
||||
.fillColor("#2c3e50")
|
||||
.text("Event Information", 50, 120);
|
||||
|
||||
doc.fontSize(12)
|
||||
.fillColor('#34495e')
|
||||
.text(`Event Name: ${eventName || `Event ${eventId}`}`, 50, 150)
|
||||
.text(`Event ID: ${eventId}`, 50, 170)
|
||||
.text(`Description: ${eventDescription || 'No description available'}`, 50, 190);
|
||||
doc
|
||||
.fontSize(12)
|
||||
.fillColor("#34495e")
|
||||
.text(`Event Name: ${eventName || `Event ${eventId}`}`, 50, 150)
|
||||
.text(`Event ID: ${eventId}`, 50, 170)
|
||||
.text(
|
||||
`Description: ${eventDescription || "No description available"}`,
|
||||
50,
|
||||
190
|
||||
);
|
||||
|
||||
// Ticket Information
|
||||
doc.fontSize(18)
|
||||
.fillColor('#2c3e50')
|
||||
.text('Ticket Details', 50, 230);
|
||||
doc.fontSize(18).fillColor("#2c3e50").text("Ticket Details", 50, 230);
|
||||
|
||||
doc.fontSize(12)
|
||||
.fillColor('#34495e')
|
||||
.text(`Ticket ID: ${ticketId}`, 50, 260)
|
||||
.text(`Purchase ID: ${purchaseId}`, 50, 280)
|
||||
.text(`Purchase Date: ${new Date(timestamp).toLocaleString()}`, 50, 300)
|
||||
.text(`Ticket Number: #${soldCount}`, 50, 320);
|
||||
doc
|
||||
.fontSize(12)
|
||||
.fillColor("#34495e")
|
||||
.text(`Ticket ID: ${ticketId}`, 50, 260)
|
||||
.text(`Purchase ID: ${purchaseId}`, 50, 280)
|
||||
.text(
|
||||
`Purchase Date: ${new Date(timestamp).toLocaleString()}`,
|
||||
50,
|
||||
300
|
||||
)
|
||||
.text(`Ticket Number: #${soldCount}`, 50, 320);
|
||||
|
||||
// QR Code placeholder (you could integrate a QR code library here)
|
||||
doc.rect(400, 250, 100, 100)
|
||||
.strokeColor('#bdc3c7')
|
||||
.lineWidth(1)
|
||||
.stroke();
|
||||
doc
|
||||
.rect(400, 250, 100, 100)
|
||||
.strokeColor("#bdc3c7")
|
||||
.lineWidth(1)
|
||||
.stroke();
|
||||
|
||||
doc.fontSize(10)
|
||||
.fillColor('#7f8c8d')
|
||||
.text('QR Code', 430, 305, { align: 'center' });
|
||||
doc
|
||||
.fontSize(10)
|
||||
.fillColor("#7f8c8d")
|
||||
.text("QR Code", 430, 305, { align: "center" });
|
||||
|
||||
// Terms and Conditions
|
||||
doc.fontSize(14)
|
||||
.fillColor('#2c3e50')
|
||||
.text('Terms & Conditions', 50, 380);
|
||||
doc
|
||||
.fontSize(14)
|
||||
.fillColor("#2c3e50")
|
||||
.text("Terms & Conditions", 50, 380);
|
||||
|
||||
doc.fontSize(10)
|
||||
.fillColor('#7f8c8d')
|
||||
.text('• This ticket is non-refundable and non-transferable', 50, 410)
|
||||
.text('• Please arrive 30 minutes before the event starts', 50, 425)
|
||||
.text('• Valid photo ID required for entry', 50, 440)
|
||||
.text('• This ticket is valid only for the specified event and date', 50, 455);
|
||||
doc
|
||||
.fontSize(10)
|
||||
.fillColor("#7f8c8d")
|
||||
.text("• This ticket is non-refundable and non-transferable", 50, 410)
|
||||
.text("• Please arrive 30 minutes before the event starts", 50, 425)
|
||||
.text("• Valid photo ID required for entry", 50, 440)
|
||||
.text(
|
||||
"• This ticket is valid only for the specified event and date",
|
||||
50,
|
||||
455
|
||||
);
|
||||
|
||||
// Footer
|
||||
doc.fontSize(8)
|
||||
.fillColor('#95a5a6')
|
||||
.text(`Generated on ${new Date().toLocaleString()}`, 50, 520)
|
||||
.text('Powered by Ticket Microservice', 50, 535)
|
||||
.text(`System ID: ${process.env.NODE_ENV || 'development'}`, 50, 550);
|
||||
doc
|
||||
.fontSize(8)
|
||||
.fillColor("#95a5a6")
|
||||
.text(`Generated on ${new Date().toLocaleString()}`, 50, 520)
|
||||
.text("Powered by Ticket Microservice", 50, 535)
|
||||
.text(`System ID: ${process.env.NODE_ENV || "development"}`, 50, 550);
|
||||
|
||||
// Security watermark
|
||||
doc.fontSize(60)
|
||||
.fillColor('#ecf0f1')
|
||||
.text('VALID', 200, 300, {
|
||||
rotate: -45,
|
||||
opacity: 0.1
|
||||
});
|
||||
doc.fontSize(60).fillColor("#ecf0f1").text("VALID", 200, 300, {
|
||||
rotate: -45,
|
||||
opacity: 0.1,
|
||||
});
|
||||
|
||||
// Finalize PDF
|
||||
doc.end();
|
||||
|
||||
stream.on('finish', () => {
|
||||
stream.on("finish", () => {
|
||||
logger.info(`PDF ticket generated: ${filename}`);
|
||||
resolve({
|
||||
success: true,
|
||||
filename,
|
||||
filepath,
|
||||
size: fs.statSync(filepath).size
|
||||
size: fs.statSync(filepath).size,
|
||||
});
|
||||
});
|
||||
|
||||
stream.on('error', (error) => {
|
||||
logger.error('Error writing PDF file:', error);
|
||||
stream.on("error", (error) => {
|
||||
logger.error("Error writing PDF file:", error);
|
||||
reject(error);
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Error generating PDF:', error);
|
||||
logger.error("Error generating PDF:", error);
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
@@ -142,17 +159,20 @@ class PDFGenerator {
|
||||
|
||||
async generateBulkTicketsPDF(tickets) {
|
||||
const results = [];
|
||||
|
||||
|
||||
for (const ticket of tickets) {
|
||||
try {
|
||||
const result = await this.generateTicketPDF(ticket);
|
||||
results.push(result);
|
||||
} catch (error) {
|
||||
logger.error(`Failed to generate PDF for ticket ${ticket.ticketId}:`, error);
|
||||
logger.error(
|
||||
`Failed to generate PDF for ticket ${ticket.ticketId}:`,
|
||||
error
|
||||
);
|
||||
results.push({
|
||||
success: false,
|
||||
ticketId: ticket.ticketId,
|
||||
error: error.message
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -191,7 +211,7 @@ class PDFGenerator {
|
||||
let deletedCount = 0;
|
||||
|
||||
for (const file of files) {
|
||||
if (file.endsWith('.pdf')) {
|
||||
if (file.endsWith(".pdf")) {
|
||||
const filepath = path.join(this.outputDir, file);
|
||||
const stats = fs.statSync(filepath);
|
||||
const ageHours = (now - stats.mtime.getTime()) / (1000 * 60 * 60);
|
||||
@@ -209,7 +229,7 @@ class PDFGenerator {
|
||||
|
||||
return deletedCount;
|
||||
} catch (error) {
|
||||
logger.error('Error cleaning up old tickets:', error);
|
||||
logger.error("Error cleaning up old tickets:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -217,8 +237,8 @@ class PDFGenerator {
|
||||
getStats() {
|
||||
try {
|
||||
const files = fs.readdirSync(this.outputDir);
|
||||
const pdfFiles = files.filter(f => f.endsWith('.pdf'));
|
||||
|
||||
const pdfFiles = files.filter((f) => f.endsWith(".pdf"));
|
||||
|
||||
let totalSize = 0;
|
||||
for (const file of pdfFiles) {
|
||||
const filepath = path.join(this.outputDir, file);
|
||||
@@ -229,16 +249,16 @@ class PDFGenerator {
|
||||
totalTickets: pdfFiles.length,
|
||||
totalSizeBytes: totalSize,
|
||||
totalSizeMB: (totalSize / (1024 * 1024)).toFixed(2),
|
||||
outputDirectory: this.outputDir
|
||||
outputDirectory: this.outputDir,
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('Error getting PDF stats:', error);
|
||||
logger.error("Error getting PDF stats:", error);
|
||||
return {
|
||||
totalTickets: 0,
|
||||
totalSizeBytes: 0,
|
||||
totalSizeMB: '0.00',
|
||||
totalSizeMB: "0.00",
|
||||
outputDirectory: this.outputDir,
|
||||
error: error.message
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,12 +70,20 @@ class RedisClient {
|
||||
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'];
|
||||
}
|
||||
|
||||
const keys = [
|
||||
`event:${eventId}:tickets`,
|
||||
`event:${eventId}:meta`,
|
||||
'global:stats'
|
||||
];
|
||||
const args = [timestamp, purchaseId];
|
||||
// Ensure all arguments are strings as required by Redis Lua
|
||||
const args = [String(timestamp), String(purchaseId)];
|
||||
|
||||
try {
|
||||
const result = await this.client.eval(
|
||||
@@ -85,6 +93,8 @@ class RedisClient {
|
||||
return result;
|
||||
} catch (error) {
|
||||
logger.error('Error executing purchase ticket script:', error);
|
||||
logger.error('Script keys:', keys);
|
||||
logger.error('Script args:', args);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user