Skip to content

Commit 6493c98

Browse files
committed
Log before provider-orphaning eviction happens
Signed-off-by: Fredrik Adelöw <freben@gmail.com>
1 parent 80f7b64 commit 6493c98

File tree

3 files changed

+177
-54
lines changed

3 files changed

+177
-54
lines changed

.changeset/brown-turkeys-send.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@backstage/plugin-catalog-backend': patch
3+
---
4+
5+
Log before provider-orphaning eviction happens

plugins/catalog-backend/src/processing/evictEntitiesFromOrphanedProviders.test.ts

Lines changed: 156 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -13,62 +13,166 @@
1313
* See the License for the specific language governing permissions and
1414
* limitations under the License.
1515
*/
16-
import { EntityProvider } from '@backstage/plugin-catalog-node';
17-
import { mockServices } from '@backstage/backend-test-utils';
16+
17+
import { mockServices, TestDatabases } from '@backstage/backend-test-utils';
1818
import { DefaultProviderDatabase } from '../database/DefaultProviderDatabase';
19-
import { evictEntitiesFromOrphanedProviders } from './evictEntitiesFromOrphanedProviders';
20-
21-
describe('evictEntitiesFromOrphanedProviders', () => {
22-
const db = {
23-
transaction: jest.fn().mockImplementation(cb => cb((() => {}) as any)),
24-
replaceUnprocessedEntities: jest.fn(),
25-
listReferenceSourceKeys: jest.fn(),
26-
} as unknown as jest.Mocked<DefaultProviderDatabase>;
27-
28-
const providers = [
29-
{ getProviderName: () => 'provider1' },
30-
{ getProviderName: () => 'provider2' },
31-
] as unknown as EntityProvider[];
32-
const logger = mockServices.logger.mock();
33-
34-
it('replaces unprocessed entities for orphaned providers with empty items', async () => {
35-
db.listReferenceSourceKeys.mockResolvedValue(['foo', 'bar']);
36-
37-
await evictEntitiesFromOrphanedProviders({ db, providers, logger });
38-
39-
expect(db.replaceUnprocessedEntities).toHaveBeenCalledTimes(2);
40-
expect(db.replaceUnprocessedEntities).toHaveBeenNthCalledWith(
41-
1,
42-
expect.anything(),
43-
{
44-
sourceKey: 'foo',
45-
type: 'full',
46-
items: [],
47-
},
48-
);
49-
expect(db.replaceUnprocessedEntities).toHaveBeenNthCalledWith(
50-
2,
51-
expect.anything(),
52-
{
53-
sourceKey: 'bar',
54-
type: 'full',
55-
items: [],
56-
},
57-
);
19+
import {
20+
evictEntitiesFromOrphanedProviders,
21+
getOrphanedEntityProviderNames,
22+
} from './evictEntitiesFromOrphanedProviders';
23+
import { applyDatabaseMigrations } from '../database/migrations';
24+
import { Knex } from 'knex';
25+
26+
jest.setTimeout(60_000);
27+
28+
const databases = TestDatabases.create();
29+
const logger = mockServices.logger.mock();
30+
31+
afterEach(() => {
32+
jest.resetAllMocks();
33+
});
34+
35+
describe.each(databases.eachSupportedId())('%p', databaseId => {
36+
let knex: Knex;
37+
let db: DefaultProviderDatabase;
38+
39+
beforeEach(async () => {
40+
knex = await databases.init(databaseId);
41+
await applyDatabaseMigrations(knex);
42+
db = new DefaultProviderDatabase({ database: knex, logger });
43+
});
44+
45+
afterEach(async () => {
46+
await knex.destroy();
5847
});
5948

60-
it('does not replace unprocessed entities for providers that are not orphaned', async () => {
61-
db.listReferenceSourceKeys.mockResolvedValue(['foo', 'provider1']);
49+
describe('getOrphanedEntityProviderNames', () => {
50+
it('correctly locates and logs orphaned providers', async () => {
51+
const providers = [
52+
{ getProviderName: () => 'provider1', connect: jest.fn() },
53+
{ getProviderName: () => 'provider2', connect: jest.fn() },
54+
];
55+
56+
await knex('refresh_state').insert([
57+
{
58+
entity_id: 'x',
59+
entity_ref: 'x',
60+
unprocessed_entity: '{}',
61+
processed_entity: '{}',
62+
errors: '[]',
63+
next_update_at: knex.fn.now(),
64+
last_discovery_at: knex.fn.now(),
65+
},
66+
]);
67+
await knex('refresh_state_references').insert([
68+
{ source_key: 'provider2', target_entity_ref: 'x' },
69+
{ source_key: 'provider3', target_entity_ref: 'x' },
70+
]);
71+
72+
await expect(
73+
getOrphanedEntityProviderNames({
74+
db,
75+
providers,
76+
logger,
77+
}),
78+
).resolves.toEqual(['provider3']);
79+
80+
expect(logger.warn).toHaveBeenCalledTimes(4);
81+
expect(logger.warn).toHaveBeenCalledWith(
82+
`Found 1 orphaned entity provider(s)`,
83+
);
84+
expect(logger.warn).toHaveBeenCalledWith(
85+
`Database contained providers: 'provider2', 'provider3'`,
86+
);
87+
expect(logger.warn).toHaveBeenCalledWith(
88+
`Installed providers were: 'provider1', 'provider2'`,
89+
);
90+
expect(logger.warn).toHaveBeenCalledWith(
91+
`Orphaned providers were thus: 'provider3'`,
92+
);
93+
});
94+
});
95+
96+
describe('evictEntitiesFromOrphanedProviders', () => {
97+
it('replaces unprocessed entities for orphaned providers with empty items', async () => {
98+
jest.spyOn(db, 'replaceUnprocessedEntities');
99+
100+
const providers = [
101+
{ getProviderName: () => 'provider1', connect: jest.fn() },
102+
{ getProviderName: () => 'provider2', connect: jest.fn() },
103+
];
104+
105+
await knex('refresh_state').insert([
106+
{
107+
entity_id: 'x',
108+
entity_ref: 'x',
109+
unprocessed_entity: '{}',
110+
processed_entity: '{}',
111+
errors: '[]',
112+
next_update_at: knex.fn.now(),
113+
last_discovery_at: knex.fn.now(),
114+
},
115+
]);
116+
await knex('refresh_state_references').insert([
117+
{ source_key: 'foo', target_entity_ref: 'x' },
118+
{ source_key: 'bar', target_entity_ref: 'x' },
119+
]);
120+
121+
await evictEntitiesFromOrphanedProviders({ db, providers, logger });
122+
123+
expect(db.replaceUnprocessedEntities).toHaveBeenCalledTimes(2);
124+
expect(db.replaceUnprocessedEntities).toHaveBeenCalledWith(
125+
expect.anything(),
126+
{
127+
sourceKey: 'foo',
128+
type: 'full',
129+
items: [],
130+
},
131+
);
132+
expect(db.replaceUnprocessedEntities).toHaveBeenCalledWith(
133+
expect.anything(),
134+
{
135+
sourceKey: 'bar',
136+
type: 'full',
137+
items: [],
138+
},
139+
);
140+
});
141+
142+
it('does not replace unprocessed entities for providers that are not orphaned', async () => {
143+
jest.spyOn(db, 'replaceUnprocessedEntities');
144+
145+
const providers = [
146+
{ getProviderName: () => 'provider1', connect: jest.fn() },
147+
{ getProviderName: () => 'provider2', connect: jest.fn() },
148+
];
149+
150+
await knex('refresh_state').insert([
151+
{
152+
entity_id: 'x',
153+
entity_ref: 'x',
154+
unprocessed_entity: '{}',
155+
processed_entity: '{}',
156+
errors: '[]',
157+
next_update_at: knex.fn.now(),
158+
last_discovery_at: knex.fn.now(),
159+
},
160+
]);
161+
await knex('refresh_state_references').insert([
162+
{ source_key: 'foo', target_entity_ref: 'x' },
163+
{ source_key: 'provider1', target_entity_ref: 'x' },
164+
]);
62165

63-
await evictEntitiesFromOrphanedProviders({ db, providers, logger });
166+
await evictEntitiesFromOrphanedProviders({ db, providers, logger });
64167

65-
expect(db.replaceUnprocessedEntities).not.toHaveBeenCalledWith(
66-
expect.anything(),
67-
{
68-
sourceKey: 'provider1',
69-
type: 'full',
70-
items: [],
71-
},
72-
);
168+
expect(db.replaceUnprocessedEntities).not.toHaveBeenCalledWith(
169+
expect.anything(),
170+
{
171+
sourceKey: 'provider1',
172+
type: 'full',
173+
items: [],
174+
},
175+
);
176+
});
73177
});
74178
});

plugins/catalog-backend/src/processing/evictEntitiesFromOrphanedProviders.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,22 +18,36 @@ import { EntityProvider } from '@backstage/plugin-catalog-node';
1818
import { LoggerService } from '@backstage/backend-plugin-api';
1919
import { ProviderDatabase } from '../database/types';
2020

21-
async function getOrphanedEntityProviderNames({
21+
export async function getOrphanedEntityProviderNames({
2222
db,
2323
providers,
24+
logger,
2425
}: {
2526
db: ProviderDatabase;
2627
providers: EntityProvider[];
28+
logger: LoggerService;
2729
}): Promise<string[]> {
2830
const dbProviderNames = await db.transaction(async tx =>
2931
db.listReferenceSourceKeys(tx),
3032
);
3133

3234
const providerNames = providers.map(p => p.getProviderName());
3335

34-
return dbProviderNames.filter(
36+
const orphaned = dbProviderNames.filter(
3537
dbProviderName => !providerNames.includes(dbProviderName),
3638
);
39+
40+
if (orphaned.length) {
41+
const dbProviderNamesString = dbProviderNames.map(p => `'${p}'`).join(', ');
42+
const providerNamesString = providerNames.map(p => `'${p}'`).join(', ');
43+
const orphanedString = orphaned.map(p => `'${p}'`).join(', ');
44+
logger.warn(`Found ${orphaned.length} orphaned entity provider(s)`);
45+
logger.warn(`Database contained providers: ${dbProviderNamesString}`);
46+
logger.warn(`Installed providers were: ${providerNamesString}`);
47+
logger.warn(`Orphaned providers were thus: ${orphanedString}`);
48+
}
49+
50+
return orphaned;
3751
}
3852

3953
async function removeEntitiesForProvider({

0 commit comments

Comments
 (0)