Skip to content

Commit f242094

Browse files
committed
FLS-1452 - Actually retrieve forms from Pre-Award API
All of the work we've done has been leading up to this point! In this commit we introduce two new methods into the AdapterCacheService - validateCachedForm and fetchAndCacheForm. These are then used in a revamped getAdapterFormModel. Now, instead of simply looking in the cache, which is no longer in itself sufficient as we don't preload the cache on app startup, we check the cache for the form, if it's not there we go and fetch it from the Pre-Award API and cache it, and if it IS there, then we check to make sure that it hasn't been updated in Pre-Award by comparing the hashed form configurations. Session management is unaffected.
1 parent 2588a78 commit f242094

File tree

1 file changed

+114
-20
lines changed

1 file changed

+114
-20
lines changed

runner/src/server/services/AdapterCacheService.ts

Lines changed: 114 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import Crypto from 'crypto';
1515
import {HapiRequest, HapiServer} from "../types";
1616
import {AdapterFormModel} from "../plugins/engine/models";
1717
import Boom from "boom";
18+
import { PreAwardApiService, PublishedFormResponse } from "./PreAwardApiService";
1819

1920
const partition = "cache";
2021
const LOGGER_DATA = {
@@ -34,7 +35,7 @@ if (process.env.FORM_RUNNER_ADAPTER_REDIS_INSTANCE_URI) {
3435
redisUri = process.env.FORM_RUNNER_ADAPTER_REDIS_INSTANCE_URI;
3536
}
3637

37-
export const FORMS_KEY_PREFIX = "forms:cache:"
38+
export const FORMS_KEY_PREFIX = "forms"
3839

3940
enum ADDITIONAL_IDENTIFIER {
4041
Confirmation = ":confirmation",
@@ -57,11 +58,13 @@ const createRedisClient = (): Redis | null => {
5758
};
5859

5960
export class AdapterCacheService extends CacheService {
61+
private apiService: PreAwardApiService;
6062
private formStorage: Redis | any;
6163

6264
constructor(server: HapiServer) {
6365
//@ts-ignore
6466
super(server);
67+
this.apiService = server.services([]).preAwardApiService;
6568
const redisClient = this.getRedisClient();
6669
if (redisClient) {
6770
this.formStorage = redisClient;
@@ -134,19 +137,55 @@ export class AdapterCacheService extends CacheService {
134137
}
135138

136139
/**
137-
* handling form configuration's in a redis cache to easily distribute them among instance
138-
* * If given fom id is not available, it will generate a hash based on the configuration, And it will be saved in redis cache
139-
* * If hash is change then updating the redis
140-
* @param formId form id
141-
* @param configuration form definition configurations
142-
* @param server server object
140+
* Validates cached form against Pre-Award API.
141+
*/
142+
private async validateCachedForm(formId: string, cachedHash: string, request: HapiRequest): Promise<boolean> {
143+
try {
144+
const currentHash = await this.apiService.getFormHash(formId);
145+
return currentHash === cachedHash;
146+
} catch (error) {
147+
// If we can't validate, assume cache is valid
148+
request.logger.warn({
149+
...LOGGER_DATA,
150+
message: `Could not validate cache for form ${formId}, using cached version`
151+
});
152+
return true;
153+
}
154+
}
155+
156+
/**
157+
* Fetches form from Pre-Award API and caches it.
158+
*/
159+
private async fetchAndCacheForm(formId: string, request: HapiRequest): Promise<PublishedFormResponse | null> {
160+
try {
161+
const apiResponse = await this.apiService.getPublishedForm(formId);
162+
if (!apiResponse) return null;
163+
const formsCacheKey = `${FORMS_KEY_PREFIX}:${formId}`;
164+
await this.formStorage.set(formsCacheKey, JSON.stringify(apiResponse));
165+
request.logger.info({
166+
...LOGGER_DATA,
167+
message: `Cached form ${formId} from Pre-Award API`
168+
});
169+
return apiResponse as PublishedFormResponse;
170+
} catch (error) {
171+
request.logger.error({
172+
...LOGGER_DATA,
173+
message: `Failed to fetch form ${formId}`,
174+
error: error
175+
});
176+
return null;
177+
}
178+
}
179+
180+
/**
181+
* This is used to ensure unit tests can populate the cache
143182
*/
144183
async setFormConfiguration(formId: string, configuration: any): Promise<void> {
145184
if (!formId || !configuration) return;
146185
const hashValue = Crypto.createHash('sha256')
147186
.update(JSON.stringify(configuration))
148187
.digest('hex');
149-
const key = `${FORMS_KEY_PREFIX}${formId}`;
188+
const key = `${FORMS_KEY_PREFIX}:${formId}`;
150189
try {
151190
const existingConfigString = await this.formStorage.get(key);
152191
if (existingConfigString === null) {
@@ -175,25 +214,80 @@ export class AdapterCacheService extends CacheService {
175214
}
176215
}
177216

217+
/**
218+
* Retrieves form configuration, either from cache or Pre-Award API.
219+
*/
178220
async getFormAdapterModel(formId: string, request: HapiRequest): Promise<AdapterFormModel> {
179221
const {translationLoaderService} = request.services([]);
180222
const translations = translationLoaderService.getTranslations();
181-
const jsonDataString = await this.formStorage.get(`${FORMS_KEY_PREFIX}${formId}`);
223+
const formCacheKey = `${FORMS_KEY_PREFIX}:${formId}`;
224+
const jsonDataString = await this.formStorage.get(formCacheKey);
225+
// We use a separate key to track if we've validated that this form is up-to-date in this session
226+
// We use yar.id instead of form_session_identifier as form_session_identifier is not present in the first request
227+
const formSessionCacheKey = `${formCacheKey}:${request.yar.id}`;
228+
const sessionValidated = await this.formStorage.get(formSessionCacheKey);
229+
let configObj = null;
182230
if (jsonDataString !== null) {
183-
const configObj = JSON.parse(jsonDataString);
184-
return new AdapterFormModel(configObj.configuration, {
185-
basePath: configObj.id ? configObj.id : formId,
186-
hash: configObj.hash,
187-
previewMode: true,
188-
translationEn: translations.en,
189-
translationCy: translations.cy
231+
// Cache hit
232+
request.logger.debug({
233+
...LOGGER_DATA,
234+
message: `Cache hit for form ${formId}`
235+
});
236+
configObj = JSON.parse(jsonDataString);
237+
if (!sessionValidated) {
238+
// Validate cached form once per session
239+
request.logger.debug({
240+
...LOGGER_DATA,
241+
message: `First access of form ${formId} in yar session ${request.yar.id}, validating cache`
242+
});
243+
const isValid = await this.validateCachedForm(formId, configObj.hash, request);
244+
if (!isValid) {
245+
request.logger.info({
246+
...LOGGER_DATA,
247+
message: `Cache stale for form ${formId}, fetching fresh version`
248+
});
249+
const freshConfig = await this.fetchAndCacheForm(formId, request);
250+
if (freshConfig) {
251+
configObj = freshConfig;
252+
}
253+
} else {
254+
request.logger.debug({
255+
...LOGGER_DATA,
256+
message: `Cache valid for form ${formId}`
257+
});
258+
}
259+
} else {
260+
request.logger.debug({
261+
...LOGGER_DATA,
262+
message: `Form ${formId} already validated in yar session ${request.yar.id}`
263+
});
264+
}
265+
} else {
266+
// Cache miss - fetch from Pre-Award API
267+
request.logger.info({
268+
...LOGGER_DATA,
269+
message: `Cache miss for form ${formId}, fetching from Pre-Award API`
270+
});
271+
configObj = await this.fetchAndCacheForm(formId, request);
272+
if (!configObj) {
273+
throw Boom.notFound(`Form '${formId}' not found`);
274+
}
275+
}
276+
if (!sessionValidated) {
277+
// Mark form as validated in this session
278+
request.logger.debug({
279+
...LOGGER_DATA,
280+
message: `Marking form ${formId} as validated in yar session ${request.yar.id}`
190281
});
282+
await this.formStorage.setex(formSessionCacheKey, sessionTimeout / 1000, true);
191283
}
192-
request.logger.error({
193-
...LOGGER_DATA,
194-
message: `[FORM-CACHE] Cannot find the form ${formId}`
284+
return new AdapterFormModel(configObj.configuration, {
285+
basePath: formId,
286+
hash: configObj.hash,
287+
previewMode: true,
288+
translationEn: translations.en,
289+
translationCy: translations.cy
195290
});
196-
throw Boom.notFound("Cannot find the given form");
197291
}
198292

199293
private getRedisClient(): Redis | null {

0 commit comments

Comments
 (0)