EFIAL-458-config/dependencies and EFIAL-459-partner-sync#76
EFIAL-458-config/dependencies and EFIAL-459-partner-sync#76rtavernaea wants to merge 57 commits into
Conversation
✅ Snyk checks have passed. No issues have been found so far.
💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse. |
edandylytics
left a comment
There was a problem hiding this comment.
A couple more comments.
| const url = this.get('UM_URL'); | ||
| const auth0Domain = this.get('UM_AUTH0_DOMAIN'); | ||
| const clientId = this.get('UM_CLIENT_ID'); | ||
| const clientSecret = this.get('UM_CLIENT_SECRET'); |
There was a problem hiding this comment.
Deployed environments will need to look up credentials in an AWS secret. You can check out other methods on this service for how to use a secret and fall back to env vars if a secret doesn't exist. postgresPoolConfig is a good example.
| } | ||
|
|
||
| for (const partner of existingPartners) { | ||
| if (!!partner.managedBy && !partner.deletedOn && !apiPartnerCodes.has(partner.id)) { |
There was a problem hiding this comment.
Yep -- TX being in the UM results is a clearer case than the one I mentioned. Here's how I think about it: It's Runway's job to determine which partners each sync manages. Runway shouldn't trust external systems to say which partners it can update. That means we need to ensure that it's Runway's internal state (partner.magagedBy) that scopes the sync, not what the external systems report.
There was a problem hiding this comment.
I think we should probably refactor away from this coordinator class. It looks like it does two things:
- Initialize PgBoss and manage its lifecycle
- Start up the sync
For PgBoss, I expect we'll be using that in other places in the app soon and it'd be helpful to have that as it's own injectable component that can be shared.
Once you remove the PgBoss initialization, do we need a coordinator? It seems each sync service could start itself in its own OnModuleInit. Then we don't need the getSyncConfig wrapper either.
Let me know if I'm missing something there, though.
There was a problem hiding this comment.
Yeah I think that makes sense, I added the pg-boss service in its own folder because I'm not sure where else it will be used
edandylytics
left a comment
There was a problem hiding this comment.
Broad strokes look good. Still spending time with it, but a few things for you to check out
| host ?? 'localhost' | ||
| }:${port ?? 5432}/${database}?sslmode=${sslMode}`; | ||
|
|
||
| this.boss = new PgBoss(connStr); |
There was a problem hiding this comment.
Do we need to construct a connection string? Looks like the constructor accepts an options object that's the same shape as the pool config.
|
|
||
| @Injectable() | ||
| export class PgBossService implements OnModuleInit, OnModuleDestroy { | ||
| boss!: PgBoss; |
There was a problem hiding this comment.
The assertion that boss is populated could cause issues down the road since it hides the fact that boss is not really available until after onModuleInit runs. Based on this typing, it looks like you should be able to reference boss in a constructor. Or you might think that you can reference boss in another provider's onModuleInit and not have to figure out whether your provider's init runs before or after PgBossService's.
|
|
||
| TX_SYNC_CRON?: string; | ||
| TX_CLIENT_SECRET?: string; | ||
| TX_CONFIG_SECRET?: string; // deployed envs: name of AWS secret containing TX credentials |
There was a problem hiding this comment.
Let's keep TX stuff out of this PR
| audience: string; | ||
| }; | ||
|
|
||
| export type TxConfig = { |
There was a problem hiding this comment.
We can remove this TX stuff too
| if (typeof secret !== 'object') { | ||
| throw new Error(`Value for AWS secret ${configSecret} must be an object`); | ||
| } | ||
| if (!syncCron) return null; |
There was a problem hiding this comment.
Why not allow the caller to default a cron schedule? I expect we'll only really need an env var for local testing and if we happen to get the default wrong for some reason
There was a problem hiding this comment.
Yeah makes sense. What makes sense as a default? Every 24 hours?
There was a problem hiding this comment.
That sounds good to me. Sometime in the early morning or a bit after UM syncs tenants from Heimdall
There was a problem hiding this comment.
SC prod syncs at 10:45 pm, went with 2 here
| const syncCron = this.get('UM_SYNC_CRON'); | ||
| const configSecret = this.get('UM_CONFIG_SECRET'); | ||
| if (configSecret) { | ||
| const secret = await this.getAWSSecret(configSecret); |
There was a problem hiding this comment.
Let's use fetchAWSSecret instead. getAWSSecret does some caching that I now think is unhelpful since it requires an app restart to pick up new values from the secret. I'd like to move away from getAWSSecret but haven't removed it fully.
| if (configSecret) { | ||
| const secret = await this.getAWSSecret(configSecret); | ||
| if (typeof secret !== 'object') { | ||
| throw new Error(`Value for AWS secret ${configSecret} must be an object`); |
There was a problem hiding this comment.
What would happen if this were to throw?
There was a problem hiding this comment.
I added a try catch here to log that the sync didn't get scheduled so the app won't fail to start up if this throws. Not sure if it's necessary to add in the sync() method too
edandylytics
left a comment
There was a problem hiding this comment.
I still need to spend some time with the tests and running locally. Functionally, this looks like it should work well. The Boss module updates are clean and I like how simple it is now.
The one correctness issue we need to update is checking managedBy for undeletes. I'd also like to add a managedBy filter to the DB queries as a backstop against bugs.
The remaining comments are mostly aimed at simplification: reducing the number of intermediate data structures, aligning the form of data structures with how they'll be used, removing layers of indirection. None of these are correctness issues, just ways to simplify and hopefully make the sync easier to update down the road. (And I realize much of this is inherited from Podium! Still, good to tighten up if we can)
| tenantsByPartner.set(tenant.partnerId, new Map()); | ||
| } | ||
| tenantsByPartner.get(tenant.partnerId)!.set(tenant.code, tenant); | ||
| } |
There was a problem hiding this comment.
I'm wondering if there's a way to simplify how we construct tenantsByPartner. It feels like a lot of data structure manipulation that all looks correct, but if we can be more concise I'd find it easier to to reason about.
What if
- We had one query on partner that does an
includeon tenants? - Then we just do something like this to create
tenantsByPartner
const tenantsByPartner = new Map(
existingPartners.map(p => [p.id, new Map(p.tenant.map(t => [t.code, t]))])
)There was a problem hiding this comment.
Now that I've read the full sync, I'm not sure we need tenantsByPartner as it's constructed here. We use it twice:
- on line 224 we convert the tenant map back into an array of tenants (so might as well use
partner.tenantsfrom aninclude) - on line 245 we do want a map of tenants, but that's easy enough to just create down there where we use it and skip the partner layer (e.g.
const tenantMap = new Map(p.tenant.map(t => [t.code, t]))
| this.logger.warn('UM sync config not set — skipping sync'); | ||
| return; | ||
| } | ||
| await this.runSync(config); |
There was a problem hiding this comment.
Why split sync and runSync?
I see that tests call runSync but they need to cast the service as any to invoke the private method, which is a bit of a smell. In general, I'd like tests to focus on testing the public interface rather than private internals as that allows you to use existing tests while refactoring. That consideration doesn't really apply here, but if we're testing the private method, then it's worth questioning whether it really need to be private. (Note that we can mock umConfig to pass mock config to the sync.)
This PR addresses EDFIAL-458 and EDFIAL-459.
EDFIAL-458:
TX_SYNC_CRONSYNC_CRONvalue but kept them separate for nowPartnerSyncCoordinatorhandles scheduling and kicking off each sync based on what config values are available.EDFIAL-459:
Both syncs share the same pg-boss connection
user_management_syncsync_managed: null) are left alone by the synctx_sync