Skip to content

Commit ff3fa87

Browse files
committed
fix: clean up stale and orphaned entries from subscriptions.json
Clear jsonStore before seeding from MongoDB on startup to prevent stale disk cache entries from persisting. Add cleanup for orphaned resource documents that have no corresponding subscription. Run cleanup immediately on startup instead of waiting for the first 24-hour cycle.
1 parent a31cf02 commit ff3fa87

File tree

3 files changed

+68
-2
lines changed

3 files changed

+68
-2
lines changed

app.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ console.log(`${config.appName} ${config.appVersion}`);
1818

1919
// Schedule cleanup tasks
2020
function scheduleCleanupTasks() {
21+
// Run cleanup immediately on startup
22+
removeExpiredSubscriptions()
23+
.then(() => console.log('Startup subscription cleanup completed'))
24+
.catch(err => console.error('Error in startup subscription cleanup:', err));
25+
2126
// Run subscription cleanup every 24 hours
2227
setInterval(async() => {
2328
try {
@@ -70,6 +75,7 @@ app.use(require('./controllers'));
7075

7176
// Start server
7277
async function seedJsonStore() {
78+
jsonStore.clear();
7379
const db = mongodb.get('rsscloud');
7480
const resources = await db.collection('resources').find({}).toArray();
7581
const subscriptions = await db.collection('subscriptions').find({}).toArray();

services/remove-expired-subscriptions.js

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,14 +134,48 @@ async function removeExpiredSubscriptions() {
134134
console.log(`Created ${resourcesCreated} missing resource documents`);
135135
}
136136

137-
console.log(`Subscription cleanup completed: ${totalRemoved} expired/errored subscriptions removed, ${documentsProcessed} documents processed, ${documentsDeleted} empty documents deleted, ${urlsFixed} URLs fixed`);
137+
// Find resources with no corresponding subscription and remove them
138+
let orphanedResourcesRemoved = 0;
139+
const orphanedResourcesCursor = db.collection('resources').aggregate([
140+
{
141+
$lookup: {
142+
from: 'subscriptions',
143+
localField: '_id',
144+
foreignField: '_id',
145+
as: 'subscription'
146+
}
147+
},
148+
{
149+
$match: {
150+
subscription: { $size: 0 }
151+
}
152+
}
153+
]);
154+
155+
while (await orphanedResourcesCursor.hasNext()) {
156+
const doc = await orphanedResourcesCursor.next();
157+
// Skip recently-checked resources (preserved by the subscription cleanup above)
158+
if (doc.whenLastCheck && dayjs(doc.whenLastCheck).isAfter(dayjs().subtract(24, 'hours'))) {
159+
continue;
160+
}
161+
await db.collection('resources').deleteOne({ _id: doc._id });
162+
jsonStore.removeEntry(doc._id);
163+
orphanedResourcesRemoved++;
164+
}
165+
166+
if (orphanedResourcesRemoved > 0) {
167+
console.log(`Removed ${orphanedResourcesRemoved} orphaned resource documents`);
168+
}
169+
170+
console.log(`Subscription cleanup completed: ${totalRemoved} expired/errored subscriptions removed, ${documentsProcessed} documents processed, ${documentsDeleted} empty documents deleted, ${urlsFixed} URLs fixed, ${orphanedResourcesRemoved} orphaned resources removed`);
138171

139172
return {
140173
subscriptionsRemoved: totalRemoved,
141174
documentsProcessed,
142175
documentsDeleted,
143176
resourcesCreated,
144-
urlsFixed
177+
urlsFixed,
178+
orphanedResourcesRemoved
145179
};
146180

147181
} catch (error) {

test/remove-expired-subscriptions.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,32 @@ describe('RemoveExpiredSubscriptions', function() {
155155
expect(resDoc).to.not.be.null;
156156
});
157157

158+
it('should remove orphaned resource with no subscription document', async function() {
159+
const feedPath = '/rss.xml',
160+
resourceUrl = mock.serverUrl + feedPath,
161+
dayjs = await getDayjs();
162+
163+
// Add resource but no subscription document (last checked 48 hours ago)
164+
await mongodb.addResource(resourceUrl, {
165+
lastHash: 'abc',
166+
lastSize: 100,
167+
ctChecks: 1,
168+
ctUpdates: 0,
169+
whenLastCheck: new Date(dayjs().utc().subtract(48, 'hours').format())
170+
});
171+
jsonStore.setResource(resourceUrl, { lastHash: 'abc', lastSize: 100 });
172+
173+
await removeExpiredSubscriptions();
174+
175+
// Resource document should be removed
176+
const resDoc = await mongodb.findResource(resourceUrl);
177+
expect(resDoc).to.be.null;
178+
179+
// JSON store entry should be fully removed
180+
const storeData = jsonStore.getData();
181+
expect(storeData).to.not.have.property(resourceUrl);
182+
});
183+
158184
it('should create resource for subscription without one', async function() {
159185
const feedPath = '/rss.xml',
160186
resourceUrl = mock.serverUrl + feedPath,

0 commit comments

Comments
 (0)