diff --git a/src/lib/adopted-probes.ts b/src/lib/adopted-probes.ts index 819fc682..6826e301 100644 --- a/src/lib/adopted-probes.ts +++ b/src/lib/adopted-probes.ts @@ -1,6 +1,7 @@ import type { Knex } from 'knex'; import { scopedLogger } from './logger.js'; import { client } from './sql/client.js'; +import { fetchSockets } from './ws/server.js'; const logger = scopedLogger('adopted-probes'); @@ -9,14 +10,18 @@ const TABLE_NAME = 'adopted_probes'; type AdoptedProbe = { ip: string; uuid: string; - city: string; + lastSyncDate: string; } export class AdoptedProbes { private adoptedProbesByIp: Map> = new Map(); - private adoptedProbesByUuid: Map> = new Map(); + private connectedIpToUuid: Map = new Map(); + private connectedUuidToIp: Map = new Map(); - constructor (private readonly sql: Knex) {} + constructor ( + private readonly sql: Knex, + private readonly fetchWsSockets: typeof fetchSockets, + ) {} scheduleSync () { setTimeout(() => { @@ -26,20 +31,50 @@ export class AdoptedProbes { }, 5000); } - async syncProbeIds (probeIp: string, probeUuid: string) { - const adoptedProbeByIp = this.adoptedProbesByIp.get(probeIp); + private async syncDashboardData () { + const allSockets = await this.fetchWsSockets(); + this.connectedIpToUuid = new Map(allSockets.map(socket => [ socket.data.probe.ipAddress, socket.data.probe.uuid ])); + this.connectedUuidToIp = new Map(allSockets.map(socket => [ socket.data.probe.uuid, socket.data.probe.ipAddress ])); + + const adoptedProbes = await this.sql(TABLE_NAME).select('ip', 'uuid', 'lastSyncDate'); + this.adoptedProbesByIp = new Map(adoptedProbes.map(({ ip, uuid, lastSyncDate }) => [ ip, { uuid, lastSyncDate }])); + await Promise.all(adoptedProbes.map(({ ip, uuid }) => this.syncProbeIds(ip, uuid))); + await Promise.all(adoptedProbes.map(({ ip, lastSyncDate }) => this.updateSyncDate(ip, lastSyncDate))); + } + + private async syncProbeIds (ip: string, uuid: string) { + const connectedUuid = this.connectedIpToUuid.get(ip); - if (adoptedProbeByIp && adoptedProbeByIp.uuid === probeUuid) { // Probe ids are synced + if (connectedUuid && connectedUuid === uuid) { // ip and uuid are synced return; - } else if (adoptedProbeByIp) { // Uuid is wrong - await this.updateUuid(probeIp, probeUuid); + } + + if (connectedUuid && connectedUuid !== uuid) { // uuid was found, but it is outdated + await this.updateUuid(ip, connectedUuid); return; } - const adoptedProbeByUuid = this.adoptedProbesByUuid.get(probeUuid); + const connectedIp = this.connectedUuidToIp.get(uuid); - if (adoptedProbeByUuid) { // Probe not found by ip but found by uuid => ip is wrong - await this.updateIp(probeIp, probeUuid); + if (connectedIp) { // data was found by uuid, but not found by ip, therefore ip is outdated + await this.updateIp(connectedIp, uuid); + } + } + + private async updateSyncDate (ip: string, lastSyncDate: string) { + if (this.isToday(lastSyncDate)) { + return; + } + + const probeIsConnected = this.connectedIpToUuid.has(ip); + + if (probeIsConnected) { + await this.updateLastSyncDate(ip); + return; + } + + if (this.isMoreThan30DaysAgo(lastSyncDate)) { + await this.deleteAdoptedProbe(ip); } } @@ -51,12 +86,29 @@ export class AdoptedProbes { await this.sql(TABLE_NAME).where({ uuid }).update({ ip }); } - private async syncDashboardData () { - const probes = await this.sql(TABLE_NAME).select('ip', 'uuid'); - // Storing city as emtpy string until https://github.com/jsdelivr/globalping/issues/427 is implemented - this.adoptedProbesByIp = new Map(probes.map(probe => [ probe.ip, { uuid: probe.uuid, city: '' }])); - this.adoptedProbesByUuid = new Map(probes.map(probe => [ probe.uuid, { ip: probe.ip, city: '' }])); + private async updateLastSyncDate (ip: string) { + await this.sql(TABLE_NAME).where({ ip }).update({ lastSyncDate: new Date() }); + } + + private async deleteAdoptedProbe (ip: string) { + await this.sql(TABLE_NAME).where({ ip }).delete(); + } + + private isToday (dateString: string) { + const currentDate = new Date(); + const currentDateString = currentDate.toISOString().split('T')[0]; + return dateString === currentDateString; + } + + private isMoreThan30DaysAgo (dateString: string) { + const inputDate = new Date(dateString); + const currentDate = new Date(); + + const timeDifference = currentDate.getTime() - inputDate.getTime(); + const daysDifference = timeDifference / (24 * 3600 * 1000); + + return daysDifference > 30; } } -export const adoptedProbes = new AdoptedProbes(client); +export const adoptedProbes = new AdoptedProbes(client, fetchSockets); diff --git a/test/tests/unit/adopted-probes.test.ts b/test/tests/unit/adopted-probes.test.ts index bfb1d355..4b993605 100644 --- a/test/tests/unit/adopted-probes.test.ts +++ b/test/tests/unit/adopted-probes.test.ts @@ -5,13 +5,16 @@ import { AdoptedProbes } from '../../../src/lib/adopted-probes.js'; const selectStub = sinon.stub(); const updateStub = sinon.stub(); +const deleteStub = sinon.stub(); const whereStub = sinon.stub().returns({ update: updateStub, + delete: deleteStub, }); const sqlStub = sinon.stub().returns({ select: selectStub, where: whereStub, }); +const fetchSocketsStub = sinon.stub().resolves([]); let sandbox: sinon.SinonSandbox; describe('AdoptedProbes', () => { @@ -25,88 +28,128 @@ describe('AdoptedProbes', () => { }); it('startSync method should sync the data and start regular syncs', async () => { - const adoptedProbes = new AdoptedProbes(sqlStub as unknown as Knex); - selectStub.resolves([{ - ip: '1.1.1.1', - uuid: '1-1-1-1-1', - }]); + const adoptedProbes = new AdoptedProbes(sqlStub as unknown as Knex, fetchSocketsStub); + selectStub.resolves([{ ip: '1.1.1.1', uuid: '1-1-1-1-1', lastSyncDate: '1970-01-01' }]); adoptedProbes.scheduleSync(); expect(sqlStub.callCount).to.equal(0); expect(selectStub.callCount).to.equal(0); - await sandbox.clock.tickAsync(5500); + await sandbox.clock.tickAsync(5000); expect(sqlStub.callCount).to.equal(1); expect(sqlStub.args[0]).deep.equal([ 'adopted_probes' ]); expect(selectStub.callCount).to.equal(1); - expect(selectStub.args[0]).deep.equal([ 'ip', 'uuid' ]); + expect(selectStub.args[0]).deep.equal([ 'ip', 'uuid', 'lastSyncDate' ]); }); - it('syncProbeIds method should do nothing if probe was not found either by ip or uuid', async () => { - const adoptedProbes = new AdoptedProbes(sqlStub as unknown as Knex); - selectStub.resolves([{ - ip: '1.1.1.1', - uuid: '1-1-1-1-1', - }]); + it('class should update uuid if it is wrong', async () => { + const adoptedProbes = new AdoptedProbes(sqlStub as unknown as Knex, fetchSocketsStub); + selectStub.resolves([{ ip: '1.1.1.1', uuid: '1-1-1-1-1', lastSyncDate: '1970-01-01' }]); + fetchSocketsStub.resolves([{ data: { probe: { ipAddress: '1.1.1.1', uuid: '2-2-2-2-2' } } }]); adoptedProbes.scheduleSync(); - await sandbox.clock.tickAsync(5500); + await sandbox.clock.tickAsync(5000); - await adoptedProbes.syncProbeIds('2.2.2.2', '2-2-2-2-2'); + expect(whereStub.callCount).to.equal(1); + expect(whereStub.args[0]).to.deep.equal([{ ip: '1.1.1.1' }]); + expect(updateStub.callCount).to.equal(1); + expect(updateStub.args[0]).to.deep.equal([{ uuid: '2-2-2-2-2' }]); + }); + + it('class should update ip if it is wrong', async () => { + const adoptedProbes = new AdoptedProbes(sqlStub as unknown as Knex, fetchSocketsStub); + selectStub.resolves([{ ip: '1.1.1.1', uuid: '1-1-1-1-1', lastSyncDate: '1970-01-01' }]); + fetchSocketsStub.resolves([{ data: { probe: { ipAddress: '2.2.2.2', uuid: '1-1-1-1-1' } } }]); + + adoptedProbes.scheduleSync(); + await sandbox.clock.tickAsync(5000); + + expect(whereStub.callCount).to.equal(1); + expect(whereStub.args[0]).to.deep.equal([{ uuid: '1-1-1-1-1' }]); + expect(updateStub.callCount).to.equal(1); + expect(updateStub.args[0]).to.deep.equal([{ ip: '2.2.2.2' }]); + }); + + it('class should do nothing if adopted probe was not found and lastSyncDate < 30 days away', async () => { + const adoptedProbes = new AdoptedProbes(sqlStub as unknown as Knex, fetchSocketsStub); + selectStub.resolves([{ ip: '1.1.1.1', uuid: '1-1-1-1-1', lastSyncDate: '1969-12-15' }]); + fetchSocketsStub.resolves([]); + + adoptedProbes.scheduleSync(); + await sandbox.clock.tickAsync(5000); expect(whereStub.callCount).to.equal(0); expect(updateStub.callCount).to.equal(0); + expect(deleteStub.callCount).to.equal(0); }); - it('syncProbeIds method should do nothing if probe data is actual', async () => { - const adoptedProbes = new AdoptedProbes(sqlStub as unknown as Knex); - selectStub.resolves([{ - ip: '1.1.1.1', - uuid: '1-1-1-1-1', - }]); + it('class should do nothing if adopted probe was not found and lastSyncDate > 30 days away', async () => { + const adoptedProbes = new AdoptedProbes(sqlStub as unknown as Knex, fetchSocketsStub); + selectStub.resolves([{ ip: '1.1.1.1', uuid: '1-1-1-1-1', lastSyncDate: '1969-11-15' }]); + fetchSocketsStub.resolves([]); adoptedProbes.scheduleSync(); - await sandbox.clock.tickAsync(5500); + await sandbox.clock.tickAsync(5000); - await adoptedProbes.syncProbeIds('1.1.1.1', '1-1-1-1-1'); + expect(whereStub.callCount).to.equal(1); + expect(whereStub.args[0]).to.deep.equal([{ ip: '1.1.1.1' }]); + expect(updateStub.callCount).to.equal(0); + expect(deleteStub.callCount).to.equal(1); + expect(deleteStub.callCount).to.equal(1); + }); + + it('class should do nothing if probe data is actual', async () => { + const adoptedProbes = new AdoptedProbes(sqlStub as unknown as Knex, fetchSocketsStub); + selectStub.resolves([{ ip: '1.1.1.1', uuid: '1-1-1-1-1', lastSyncDate: '1970-01-01' }]); + fetchSocketsStub.resolves([{ data: { probe: { ipAddress: '1.1.1.1', uuid: '1-1-1-1-1' } } }]); + + adoptedProbes.scheduleSync(); + await sandbox.clock.tickAsync(5000); expect(whereStub.callCount).to.equal(0); expect(updateStub.callCount).to.equal(0); }); - it('syncProbeIds method should update uuid if it is wrong', async () => { - const adoptedProbes = new AdoptedProbes(sqlStub as unknown as Knex); - selectStub.resolves([{ - ip: '1.1.1.1', - uuid: '1-1-1-1-1', - }]); + it('class update lastSyncDate if probe is connected and lastSyncDate < 30 days away', async () => { + const adoptedProbes = new AdoptedProbes(sqlStub as unknown as Knex, fetchSocketsStub); + selectStub.resolves([{ ip: '1.1.1.1', uuid: '1-1-1-1-1', lastSyncDate: '1969-12-31' }]); + fetchSocketsStub.resolves([{ data: { probe: { ipAddress: '1.1.1.1', uuid: '1-1-1-1-1' } } }]); adoptedProbes.scheduleSync(); - await sandbox.clock.tickAsync(5500); - - await adoptedProbes.syncProbeIds('1.1.1.1', '2-2-2-2-2'); + await sandbox.clock.tickAsync(5000); expect(whereStub.callCount).to.equal(1); expect(whereStub.args[0]).to.deep.equal([{ ip: '1.1.1.1' }]); expect(updateStub.callCount).to.equal(1); - expect(updateStub.args[0]).to.deep.equal([{ uuid: '2-2-2-2-2' }]); + expect(updateStub.args[0]).to.deep.equal([{ lastSyncDate: new Date() }]); + expect(deleteStub.callCount).to.equal(0); }); - it('syncProbeIds method should update ip if it is wrong', async () => { - const adoptedProbes = new AdoptedProbes(sqlStub as unknown as Knex); - selectStub.resolves([{ - ip: '1.1.1.1', - uuid: '1-1-1-1-1', - }]); + it('class update lastSyncDate if probe is connected and lastSyncDate > 30 days away', async () => { + const adoptedProbes = new AdoptedProbes(sqlStub as unknown as Knex, fetchSocketsStub); + selectStub.resolves([{ ip: '1.1.1.1', uuid: '1-1-1-1-1', lastSyncDate: '1969-11-15' }]); + fetchSocketsStub.resolves([{ data: { probe: { ipAddress: '1.1.1.1', uuid: '1-1-1-1-1' } } }]); adoptedProbes.scheduleSync(); - await sandbox.clock.tickAsync(5500); - - await adoptedProbes.syncProbeIds('2.2.2.2', '1-1-1-1-1'); + await sandbox.clock.tickAsync(5000); expect(whereStub.callCount).to.equal(1); - expect(whereStub.args[0]).to.deep.equal([{ uuid: '1-1-1-1-1' }]); + expect(whereStub.args[0]).to.deep.equal([{ ip: '1.1.1.1' }]); expect(updateStub.callCount).to.equal(1); - expect(updateStub.args[0]).to.deep.equal([{ ip: '2.2.2.2' }]); + expect(updateStub.args[0]).to.deep.equal([{ lastSyncDate: new Date() }]); + expect(deleteStub.callCount).to.equal(0); + }); + + it('class update lastSyncDate should not update anything if lastSyncDate is today', async () => { + const adoptedProbes = new AdoptedProbes(sqlStub as unknown as Knex, fetchSocketsStub); + selectStub.resolves([{ ip: '1.1.1.1', uuid: '1-1-1-1-1', lastSyncDate: '1970-01-01' }]); + fetchSocketsStub.resolves([{ data: { probe: { ipAddress: '1.1.1.1', uuid: '1-1-1-1-1' } } }]); + + adoptedProbes.scheduleSync(); + await sandbox.clock.tickAsync(5000); + + expect(whereStub.callCount).to.equal(0); + expect(updateStub.callCount).to.equal(0); + expect(deleteStub.callCount).to.equal(0); }); });