Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Scale Redis Caching #90

Merged
merged 2 commits into from
Nov 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion backend/.env.example
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
GOOGLE_MAPS_API_KEY=
GOOGLE_MAPS_API_KEY=
REDIS_PORT=
32 changes: 32 additions & 0 deletions backend/src/redis.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// redis.js
import { createClient } from 'redis';
import dotenv from 'dotenv';

dotenv.config(); // Load environment variables

const redisClient = createClient({
socket: {
host: process.env.REDIS_HOST || 'localhost', // Default to localhost
port: parseInt(process.env.REDIS_PORT, 10) || 6379, // Default to port 6379
},
});

// Event listeners for Redis client
redisClient.on('error', (err) => {
console.error('Redis Client Error', err);
});

redisClient.on('connect', () => {
console.log('Connected to Redis');
});

// Connect to Redis
const connectRedis = async () => {
await redisClient.connect();
};

connectRedis();

export const getRedisClient = () => {
return redisClient;
};
20 changes: 19 additions & 1 deletion backend/src/resolvers/TripResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { Trip } from '../entity/Ride';
import axios from 'axios';
import { validateOrReject, IsNotEmpty, Length } from 'class-validator';
import sanitizeHtml from 'sanitize-html';
import { getRedisClient } from '../redis';
; // Ensure this imports your Redis client

class TripInput {
@IsNotEmpty()
Expand Down Expand Up @@ -37,6 +39,17 @@ export class TripResolver {
// Validate inputs
await validateOrReject(tripInput);

// Construct cache key
const cacheKey = `trip:${origin}:${destination}`;
const redisClient = getRedisClient(); // Get the Redis client instance

// Check if the route is already cached
const cachedTrip = await redisClient.get(cacheKey);
if (cachedTrip) {
console.log('Returning cached trip data');
return JSON.parse(cachedTrip); // Return cached data if available
}

const googleMapsApiKey = process.env.GOOGLE_MAPS_API_KEY!;
const url = `https://maps.googleapis.com/maps/api/directions/json?origin=${origin}&destination=${destination}&departure_time=now&key=${googleMapsApiKey}&traffic_model=best_guess`;

Expand All @@ -58,8 +71,13 @@ export class TripResolver {
trip.destination = destination;
trip.trafficDuration = trafficDuration;
trip.distance = distance;

await trip.save();

// Store the trip data in Redis with an expiration time of 1 hour
await redisClient.set(cacheKey, JSON.stringify(trip), 'EX', 3600);
console.log('Trip data cached successfully');

return trip;
}
}
}
29 changes: 23 additions & 6 deletions backend/src/resolvers/UserResolver.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
import { Resolver, Query, Mutation, Arg } from 'type-graphql';
import { User } from '../entity/User';
import { UserRepository } from '../repositories/UserRepository'; // Example of UserRepository; adjust as per your setup
import { UserRepository } from '../repositories/UserRepository';
import { validateOrReject } from 'class-validator';
import sanitizeHtml from 'sanitize-html';
import { v4 as uuidv4 } from 'uuid';
import jwt from 'jsonwebtoken';
import { RefreshToken } from '../entity/RefreshTokens';
import { JWT_SECRET, JWT_EXPIRATION, JWT_ALGORITHM } from '../constants';
import { getRepository } from 'typeorm';

import { getRedisClient } from '../redis'; // Import the Redis client

@Resolver()
export class UserResolver {
private readonly userRepository: UserRepository;

constructor() {
this.userRepository = new UserRepository(); // Example of UserRepository instantiation; adjust as per your setup
this.userRepository = new UserRepository();
}

private generateUniqueId(): number {
Expand All @@ -24,7 +24,22 @@ export class UserResolver {

@Query(() => [User])
async users(): Promise<User[]> {
return this.userRepository.findAllUsers();
const redisClient = getRedisClient(); // Get Redis client
const cacheKey = 'users'; // Key for caching user data

// Try to get users from Redis cache
const cachedUsers = await redisClient.get(cacheKey);
if (cachedUsers) {
return JSON.parse(cachedUsers); // Return cached data if available
}

// If not in cache, fetch from database
const users = await this.userRepository.findAllUsers();

// Store users in Redis cache
await redisClient.set(cacheKey, JSON.stringify(users), 'EX', 3600); // Cache for 1 hour

return users;
}

@Mutation(() => String)
Expand All @@ -37,7 +52,7 @@ export class UserResolver {
email = sanitizeHtml(email);
password = sanitizeHtml(password);
const userId = this.generateUniqueId();
const user = new User(userId,name, email, password);
const user = new User(userId, name, email, password);
user.name = name;
user.email = email;
user.password = password;
Expand All @@ -52,12 +67,14 @@ export class UserResolver {
expiresIn: JWT_EXPIRATION,
});


const refreshToken = new RefreshToken();
refreshToken.token = uuidv4();
refreshToken.user = newUser;
await getRepository(RefreshToken).save(refreshToken);

// Invalidate the cache when a new user is created
const redisClient = getRedisClient();
await redisClient.del('users'); // Clear the users cache

return JSON.stringify({ token, refreshToken: refreshToken.token });
}
Expand Down
35 changes: 16 additions & 19 deletions backend/src/services/googleMapService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,24 @@ const GOOGLE_MAPS_API_KEY = process.env.GOOGLE_MAPS_API_KEY;

/**
* Fetch optimized route from Google Maps API with caching.
* @param {Object} start - Start coordinates.
* @param {Object} end - End coordinates.
* @param {Object} start - Start coordinates { lat: number, lng: number }.
* @param {Object} end - End coordinates { lat: number, lng: number }.
* @returns {Promise<Object>} - The optimized route and ETA.
*/
export const fetchOptimizedRoute = async (start, end) => {
const cacheKey = `route:${start.lat},${start.lng}:${end.lat},${end.lng}`;

try {
// Check for cached result
const cachedResult = await redis.get(cacheKey);
if (cachedResult) {
console.log('Cache hit for key:', cacheKey); // Log cache hits for monitoring
return JSON.parse(cachedResult);
}

I;
// Fetch from Google Maps API
const response = await axios.get(
`https://maps.googleapis.com/maps/api/directions/json?origin=${start.lat},${start.lng}&destination=${end.lat},${end.lng}&key=${GOOGLE_MAPS_API_KEY}`,
`https://maps.googleapis.com/maps/api/directions/json?origin=${start.lat},${start.lng}&destination=${end.lat},${end.lng}&key=${GOOGLE_MAPS_API_KEY}`
);

if (response.data.routes.length === 0) {
Expand All @@ -35,39 +37,34 @@ export const fetchOptimizedRoute = async (start, end) => {
const route = response.data.routes[0];
const eta = Math.ceil(route.legs[0].duration.value / 60); // ETA in minutes

await redis.set(
cacheKey,
JSON.stringify({ optimizedRoute: route, eta }),
'EX',
3600,
);
// Cache the result with an expiration time
await redis.set(cacheKey, JSON.stringify({ optimizedRoute: route, eta }), 'EX', 3600);

return {
optimizedRoute: route,
eta,
};
} catch (error: any) {
if (error instanceof axios.AxiosError) {
} catch (error) {
if (error.isAxiosError) {
console.error('Error making request to Google Maps API:', {
message: error.message,
config: error.config,
response: error.response
? {
status: error.response.status,
data: error.response.data,
}
: null,
response: error.response ? {
status: error.response.status,
data: error.response.data,
} : null,
});
throw new Error('Failed to fetch data from Google Maps API');
}

// Log Redis-specific errors without interrupting the workflow
if (error.message.includes('Redis')) {
console.error('Redis error:', {
message: error.message,
stack: error.stack,
});
}

throw error;
throw error; // Rethrow the error to be handled by the calling function
}
};
24 changes: 20 additions & 4 deletions backend/src/services/route.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import express from 'express';
import { z } from 'zod';
import { fetchOptimizedRoute } from './googleMapService';
import Redis from 'ioredis';

const router = express.Router();
const redis = new Redis();

const routeSchema = z.object({
start: z.object({
Expand All @@ -17,22 +19,36 @@ const routeSchema = z.object({

router.post('/optimize-route', async (req, res) => {
try {
// Validate request body against schema
routeSchema.parse(req.body);

const { start, end } = req.body;

// Create a unique cache key based on start and end coordinates
const cacheKey = `optimized-route:${start.lat},${start.lng}:${end.lat},${end.lng}`;

// Check if the result is already cached
const cachedResult = await redis.get(cacheKey);
if (cachedResult) {
console.log('Cache hit for key:', cacheKey); // Log cache hit for monitoring
return res.json(JSON.parse(cachedResult)); // Return cached result
}

// Fetch optimized route from the Google Maps service
const { optimizedRoute, eta } = await fetchOptimizedRoute(start, end);

// Cache the result with an expiration time of 1 hour
await redis.set(cacheKey, JSON.stringify({ optimizedRoute, eta }), 'EX', 3600);

// Send response
res.json({
optimizedRoute,
eta,
});
} catch (error: any) {
} catch (error : any) {
if (error instanceof z.ZodError) {
console.error('Validation error:', error.errors);
return res
.status(400)
.json({ error: 'Invalid input', details: error.errors });
return res.status(400).json({ error: 'Invalid input', details: error.errors });
}

if (error.message === 'No route found') {
Expand Down
6 changes: 6 additions & 0 deletions frontend/src/component/faqsection.jsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
// FaqSection.jsx

import React, { useState } from "react";
import Accordion from "@mui/material/Accordion";
import AccordionSummary from "@mui/material/AccordionSummary";
import AccordionDetails from "@mui/material/AccordionDetails";
import Typography from "@mui/material/Typography";
import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; // Import the arrow icon


// Import Header and Footer components
import Header from "./header"; // Adjust the path based on your folder structure
import Footer from "./footer";

const FaqSection = () => {

const [expanded, setExpanded] = useState(false);

const handleChange = (panel) => (event, isExpanded) => {
Expand Down Expand Up @@ -44,6 +47,7 @@ const FaqSection = () => {
</Typography>

{/* First FAQ */}

<Accordion
expanded={expanded === "panel1"}
onChange={handleChange("panel1")}
Expand Down Expand Up @@ -94,6 +98,7 @@ const FaqSection = () => {
</Accordion>

{/* Second FAQ */}

<Accordion
expanded={expanded === "panel2"}
onChange={handleChange("panel2")}
Expand Down Expand Up @@ -143,6 +148,7 @@ const FaqSection = () => {
</Accordion>

{/* Third FAQ */}

<Accordion
expanded={expanded === "panel3"}
onChange={handleChange("panel3")}
Expand Down
Loading