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

⚡️ perf: 피드 삽입 정렬 기능 제거 & 피드 뷰 구현 #311

Merged
merged 5 commits into from
Dec 31, 2024
Merged
Show file tree
Hide file tree
Changes from 4 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
2 changes: 1 addition & 1 deletion .github/workflows/deploy_server.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ jobs:
npm run build

cd ..
cd rss-notifier/
cd feed-crawler/

echo "PORT=${{ secrets.RSS_NOTIFIER_PORT }}" > .env
echo "DB_HOST=${{ secrets.RSS_NOTIFIER_DB_HOST }}" >> .env
Expand Down
6 changes: 3 additions & 3 deletions ecosystem.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ module.exports = {
},
},
{
name: "rss-notifier",
script: "./rss-notifier/dist/main.js",
name: "feed-crawler",
script: "./feed-crawler/dist/main.js",
instances: "1",
exec_mode: "fork",
cron_restart: `*/30 * * * *`, // 30분 마다 rss-notifier 재시작
cron_restart: `*/30 * * * *`, // 30분 마다 feed-crawler 재시작
autorestart: false,
watch: false,
env: {
Expand Down
2 changes: 1 addition & 1 deletion rss-notifier/.gitignore → feed-crawler/.gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/node_modules
.env
/dist
rss-notifier.log
feed-crawler.log

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import * as dotenv from "dotenv";
import { PoolConnection } from "mysql2/promise";

dotenv.config({
path: process.env.NODE_ENV === "production" ? "rss-notifier/.env" : ".env",
path: process.env.NODE_ENV === "production" ? "feed-crawler/.env" : ".env",
});

export const pool = mysql.createPool({
Expand Down Expand Up @@ -51,34 +51,42 @@ export const selectAllRss = async (): Promise<RssObj[]> => {
};

export const insertFeeds = async (resultData: FeedDetail[]) => {
let successCount = 0;
let lastFeedId: number;
const query = `
INSERT INTO feed (blog_id, created_at, title, path, thumbnail)
VALUES (?, ?, ?, ?, ?)
`;
for (const feed of resultData) {
try {
lastFeedId = (
await executeQuery<FeedDetail>(query, [
feed.blogId,
feed.pubDate,
feed.title,
feed.link,
feed.imageUrl,
])
)[INSERT_ID];
successCount++;
} catch (error) {
logger.error(`누락된 피드 데이터가 존재합니다. 에러 내용: ${error}`);
}
}
const connection = await pool.getConnection();
const insertPromises = resultData.map((feed) => {
return connection.query(query, [
feed.blogId,
feed.pubDate,
feed.title,
feed.link,
feed.imageUrl,
]);
});

const results = await Promise.allSettled(insertPromises);

const failedInserts = results.filter(
(result) => result.status === "rejected"
);
failedInserts.forEach((failed) => {
const error = (failed as PromiseRejectedResult).reason;
logger.error(`피드 삽입 실패. 에러 내용: ${error}`);
});
asn6878 marked this conversation as resolved.
Show resolved Hide resolved

const insertedIds = results
.filter((result) => result.status === "fulfilled")
.map(
(result) => (result as PromiseFulfilledResult<any>).value[0][INSERT_ID]
);
const successCount = insertedIds.length;

logger.info(
`${successCount}개의 피드 데이터가 성공적으로 데이터베이스에 삽입되었습니다.`
);
lastFeedId = lastFeedId - successCount + 1;
return lastFeedId;
return insertedIds;
};

export const deleteRecentFeed = async () => {
Expand Down Expand Up @@ -112,22 +120,20 @@ export const deleteRecentFeed = async () => {
logger.info(`Redis의 feed:recent:* 값이 정상적으로 삭제되었습니다.`);
};

export const setRecentFeedList = async (startId: number) => {
export const setRecentFeedList = async (feedIds: number[]) => {
const redis = await createRedisClient();
try {
const query = `SELECT f.id,
f.created_at AS createdAt,
f.title,
f.view_count AS viewCount,
f.path,
f.thumbnail,
f.blog_id AS blogId,
r.blog_platform AS blogPlatform,
r.name AS author
FROM feed f
JOIN rss_accept r ON f.blog_id = r.id
WHERE f.id >= ${startId}`;
const resultList = await executeQuery<RssObj>(query);
const query = `SELECT f.feed_id AS id ,
f.feed_created_at AS createdAt,
f.feed_title AS title,
f.feed_view_count AS viewCount,
f.feed_path AS path,
f.feed_thumbnail AS thumbnail,
f.blog_name AS blogName,
f.blog_platform AS blogPlatform
FROM feed_view f
WHERE f.feed_id IN (${feedIds.map(() => "?").join(", ")})`;
const resultList = await executeQuery<RssObj>(query, feedIds);
const pipeLine = redis.pipeline();
for (const feed of resultList) {
pipeLine.hset(`feed:recent:${feed.id}`, feed);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ const logger = winston.createLogger({
new winston.transports.File({
filename: `${
process.env.NODE_ENV === "production"
? "rss-notifier/logs/rss-notifier.log"
: "logs/rss-notifier.log"
? "feed-crawler/logs/feed-crawler.log"
: "logs/feed-crawler.log"
}`,
}),
],
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import logger from "./common/logger.js";
import "dotenv/config";
import "dotenv/config.js";
import {
selectAllRss,
insertFeeds,
Expand Down Expand Up @@ -125,38 +125,39 @@ const customUnescape = (text: string): string => {
return unescape(text);
};

export const performTask = async () => {
const feedGroupByRss = (rssObjects: RssObj[]) => {
const currentTime = new Date();
const rssObjects = await selectAllRss();

if (rssObjects.length === 0) {
logger.info("등록된 RSS가 없습니다.");
return;
}

let idx = 0;
const newFeeds = await Promise.all(
rssObjects.map(async (rssObj) => {
return Promise.all(
rssObjects.map(async (rssObj: RssObj) => {
logger.info(
`[${++idx}번째 rss [${rssObj.rssUrl}] 에서 데이터 조회하는 중...`
);
return await findNewFeeds(rssObj, currentTime.setSeconds(0, 0));
})
);
};

const result = newFeeds.flat().sort((currentFeed, nextFeed) => {
const dateCurrent = new Date(currentFeed.pubDate);
const dateNext = new Date(nextFeed.pubDate);
return dateCurrent.getTime() - dateNext.getTime();
});

export const performTask = async () => {
await deleteRecentFeed();
if (result.length === 0) {

const rssObjects = await selectAllRss();

if (rssObjects.length === 0) {
logger.info("등록된 RSS가 없습니다.");
return;
}

const newFeedsByRss = await feedGroupByRss(rssObjects);
const newFeeds = newFeedsByRss.flat();

if (newFeeds.length === 0) {
logger.info("새로운 피드가 없습니다.");
return;
}
logger.info(`총 ${newFeeds.length}개의 새로운 피드가 있습니다.`);

const recentFeedIds = await insertFeeds(newFeeds);

logger.info(`총 ${result.length}개의 새로운 피드가 있습니다.`);
const recentFeedStartId = await insertFeeds(result);
await setRecentFeedList(recentFeedStartId);
await setRecentFeedList(recentFeedIds);
asn6878 marked this conversation as resolved.
Show resolved Hide resolved
};
6 changes: 3 additions & 3 deletions rss-notifier/src/main.ts → feed-crawler/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { pool } from "./common/db-access.js";
import logger from "./common/logger.js";
import { performTask } from "./rss-notifier.js";
import { performTask } from "./feed-crawler.js";

async function runNotifier() {
async function runCrawler() {
logger.info("==========작업 시작==========");
const startTime = Date.now();
await performTask();
Expand All @@ -14,4 +14,4 @@ async function runNotifier() {
pool.end();
}

runNotifier();
runCrawler();
File renamed without changes.
23 changes: 23 additions & 0 deletions server/src/feed/feed.entity.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import {
BaseEntity,
Column,
DataSource,
Entity,
Index,
JoinColumn,
ManyToOne,
PrimaryGeneratedColumn,
ViewEntity,
} from 'typeorm';
import { RssAccept } from '../rss/rss.entity';

Expand Down Expand Up @@ -49,3 +51,24 @@ export class Feed extends BaseEntity {
})
blog: RssAccept;
}

@ViewEntity({
expression: (dataSource: DataSource) =>
dataSource
.createQueryBuilder()
.select()
.addSelect('ROW_NUMBER() OVER (ORDER BY feed.created_at) AS order_id')
.addSelect('feed.id', 'feed_id')
.addSelect('title', 'feed_title')
asn6878 marked this conversation as resolved.
Show resolved Hide resolved
.addSelect('feed.path', 'feed_path')
.addSelect('feed.created_at', 'feed_created_at')
.addSelect('feed.thumbnail', 'feed_thumbnail')
.addSelect('feed.view_count', 'feed_view_count')
.addSelect('rss_accept.name', 'blog_name')
.addSelect('rss_accept.blog_platform', 'blog_platform')
.from(Feed, 'feed')
.innerJoin(RssAccept, 'rss_accept', 'rss_accept.id = feed.blog_id')
.orderBy('feed_created_at'),
name: 'feed_view',
})
export class FeedView {}
Loading