Skip to content
Merged
20 changes: 19 additions & 1 deletion .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ jobs:
working-directory: ${{ github.event.repository.name }}
run: |
jq -n -r '
[${{ toJSON(secrets) }}, ${{ toJSON(vars) }}]
[${{ toJSON(secrets) }}, ${{ toJSON(vars) }}]
| add
| to_entries
| map(select(.value | test("\n") | not))
Expand Down Expand Up @@ -157,3 +157,21 @@ jobs:
environment: ${{ inputs.environment }}
core-image-tag: ${{ inputs.core-image-tag }}
secrets: inherit

reindex-after-seed:
needs: seed-data
if: ${{ inputs.reset == 'true' && needs.seed-data.result == 'success' }}
uses: ./.github/workflows/reindex.yml
with:
environment: ${{ inputs.environment }}
core-image-tag: ${{ inputs.core-image-tag }}
secrets: inherit

reindex-after-deploy:
needs: deploy
if: ${{ inputs.reset != 'true' && needs.deploy.result == 'success' }}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Inconsistent Job Success Checks

The new reindex jobs check for success using needs.<job>.result, which is inconsistent with the established pattern of using needs.<job>.outputs.outcome in this workflow. For jobs like deploy that explicitly define an outcome output, result and outputs.outcome can differ, potentially causing reindex jobs to run unexpectedly.

Fix in Cursor Fix in Web

uses: ./.github/workflows/reindex.yml
with:
environment: ${{ inputs.environment }}
core-image-tag: ${{ inputs.core-image-tag }}
secrets: inherit
66 changes: 66 additions & 0 deletions .github/workflows/reindex.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
name: Reindex search
run-name: Reindex search in ${{ inputs.environment }} core=${{ inputs.core-image-tag }}
on:
workflow_call:
inputs:
environment:
required: true
type: string
core-image-tag:
required: true
type: string
workflow_dispatch:
inputs:
environment:
type: choice
description: Environment where to reindex search
required: true
default: 'development'
options:
- development
- qa
- staging
- production
core-image-tag:
description: Core DockerHub image tag
required: true
jobs:
reindex:
environment: ${{ inputs.environment }}
runs-on: ubuntu-24.04
timeout-minutes: 60

steps:
- name: Clone country config resource package
uses: actions/checkout@v3
with:
fetch-depth: 0
ref: ${{ github.ref_name }}
path: './${{ github.event.repository.name }}'

- name: Read known hosts
run: |
cd ${{ github.event.repository.name }}
echo "KNOWN_HOSTS<<EOF" >> $GITHUB_ENV
sed -i -e '$a\' ./infrastructure/known-hosts
cat ./infrastructure/known-hosts >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
- name: Install SSH Key
uses: shimataro/ssh-key-action@v2
with:
key: ${{ secrets.SSH_KEY }}
known_hosts: ${{ env.KNOWN_HOSTS }}

- name: Run reindex script in docker container
run: |
ssh -p ${{ vars.SSH_PORT }} ${{ secrets.SSH_USER }}@${{ vars.SSH_HOST }} ${{ vars.SSH_ARGS }} "
docker run --rm \
-v /opt/opencrvs/infrastructure/deployment:/workspace \
-w /workspace \
--network opencrvs_overlay_net \
-e 'AUTH_URL=http://auth:4040/' \
-e 'EVENTS_URL=http://gateway:7070/events/reindex' \
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Reindex Workflow SSH and URL Configuration Issues

The reindex workflow's SSH connection uses vars.SSH_HOST and vars.SSH_PORT without the fallback to secrets, which differs from other workflows and could lead to failures. Additionally, the EVENTS_URL is set to the full endpoint http://gateway:7070/events/reindex, but the reindex.sh script appends /events/reindex again, resulting in a malformed URL and failed reindex requests.

Fix in Cursor Fix in Web

alpine \
sh -c 'apk add --no-cache curl jq && sh reindex.sh'"
53 changes: 53 additions & 0 deletions infrastructure/deployment/reindex.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
#
# OpenCRVS is also distributed under the terms of the Civil Registration
# & Healthcare Disclaimer located at http://opencrvs.org/license.
#
# Copyright (C) The OpenCRVS Authors located at https://github.com/opencrvs/opencrvs-core/blob/master/AUTHORS.

#!/usr/bin/env bash
set -euo pipefail

EVENTS_URL="${EVENTS_URL:-http://localhost:5555/}"
AUTH_URL="${AUTH_URL:-http://localhost:4040/}"


get_reindexing_token() {
curl -s "${AUTH_URL%/}/internal/reindexing-token" | jq -r '.token'
}

trigger_reindex() {
token="$(get_reindexing_token)"
out="$(curl -s -w '\n%{http_code}' -X POST \
-H "Authorization: Bearer ${token}" \
-H "Content-Type: application/json" \
"${EVENTS_URL%/}/events/reindex")"
body="$(printf '%s' "$out" | sed '$d')"
code="$(printf '%s' "$out" | tail -n1)"
echo "$body"
[ "$code" -ge 200 ] && [ "$code" -lt 300 ]
}

reindexing_attempts=0
while true; do
if [ "$reindexing_attempts" -eq 0 ]; then
echo "Reindexing search..."
else
echo "Reindexing search... (attempt $reindexing_attempts)"
fi

if trigger_reindex; then
echo "...done reindexing"
exit 0
else
echo "Reindex attempt failed"
reindexing_attempts=$((reindexing_attempts + 1))
if [ "$reindexing_attempts" -gt 30 ]; then
echo "Failed to reindex search after $reindexing_attempts attempts."
exit 1
fi
sleep 5
fi
done
24 changes: 14 additions & 10 deletions infrastructure/postgres/setup-analytics.sh
Original file line number Diff line number Diff line change
Expand Up @@ -50,31 +50,35 @@ PGPASSWORD="$POSTGRES_PASSWORD" psql -v ON_ERROR_STOP=1 -h "$POSTGRES_HOST" -p "

CREATE SCHEMA IF NOT EXISTS analytics;

CREATE OR REPLACE VIEW analytics.locations
WITH (security_barrier)
AS
SELECT * FROM app.locations;
CREATE TABLE IF NOT EXISTS analytics.locations (
id uuid PRIMARY KEY,
name text NOT NULL,
parent_id uuid REFERENCES analytics.locations(id),
location_type TEXT NOT NULL
);

CREATE UNIQUE INDEX IF NOT EXISTS analytics_locations_pkey ON analytics.locations(id uuid_ops);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Redundant Index Creation and Syntax Error

The CREATE UNIQUE INDEX statement for analytics.locations(id) is redundant. The id column is already a PRIMARY KEY, which automatically creates a unique index. Additionally, the uuid_ops operator class is incorrect syntax for a single column index.

Fix in Cursor Fix in Web


CREATE TABLE IF NOT EXISTS analytics.event_actions (
event_type text NOT NULL,
action_type app.action_type NOT NULL,
action_type TEXT NOT NULL,
annotation jsonb,
assigned_to text,
created_at timestamp with time zone NOT NULL DEFAULT now(),
created_at_location uuid REFERENCES app.locations(id),
created_at_location uuid,
created_by text NOT NULL,
created_by_role text NOT NULL,
created_by_signature text,
created_by_user_type app.user_type NOT NULL,
created_by_user_type TEXT NOT NULL,
declared_at timestamp with time zone,
registered_at timestamp with time zone,
declaration jsonb NOT NULL DEFAULT '{}'::jsonb,
event_id uuid NOT NULL REFERENCES app.events(id),
event_id uuid NOT NULL,
id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
original_action_id uuid REFERENCES app.event_actions(id),
original_action_id uuid,
registration_number text UNIQUE,
request_id text,
status app.action_status NOT NULL,
status TEXT NOT NULL,
transaction_id text NOT NULL,
content jsonb,
UNIQUE (id, event_id)
Expand Down
40 changes: 39 additions & 1 deletion src/analytics/analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ import {
EventDocument,
EventState,
getActionAnnotationFields,
getCurrentEventState
getCurrentEventState,
Location
} from '@opencrvs/toolkit/events'
import { differenceInDays } from 'date-fns'
import { ExpressionBuilder, Kysely } from 'kysely'
Expand Down Expand Up @@ -232,6 +233,43 @@ export async function importEvent(event: EventDocument, trx: Kysely<any>) {
logger.info(`Event with id "${event.id}" logged into analytics`)
}

export async function importLocations(locations: Location[]) {
const client = getClient()
await client.transaction().execute(async (trx) => {
for (const [index, batch] of chunk(
locations,
INSERT_MAX_CHUNK_SIZE
).entries()) {
logger.info(
`Importing ${Math.min((index + 1) * INSERT_MAX_CHUNK_SIZE, locations.length)}/${locations.length} locations`
)

await trx
.insertInto('analytics.locations')
.values(
batch.map((l) => ({
id: l.id,
name: l.name,
parentId: l.parentId,
locationType: l.locationType
}))
)
.onConflict((oc) =>
oc
.column('id')
.doUpdateSet(
(eb: ExpressionBuilder<any, 'analytics.locations'>) => ({
name: eb.ref('excluded.name'),
parentId: eb.ref('excluded.parentId'),
locationType: eb.ref('excluded.locationType')
})
)
)
.execute()
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Case Mismatch Causes SQL Insert Failures

The importLocations function uses camelCase properties (parentId, locationType) when inserting into analytics.locations. This conflicts with the database's snake_case column names (parent_id, location_type), leading to SQL insertion failures.

Fix in Cursor Fix in Web

}
})
}

export async function importEvents(events: EventDocument[], trx: Kysely<any>) {
for (const event of events) {
await importEvent(event, trx)
Expand Down
24 changes: 20 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ import {
COUNTRY_CONFIG_PORT,
CHECK_INVALID_TOKEN,
AUTH_URL,
DEFAULT_TIMEOUT
DEFAULT_TIMEOUT,
GATEWAY_URL
} from '@countryconfig/constants'
import {
contentHandler,
Expand Down Expand Up @@ -70,11 +71,13 @@ import getUserNotificationRoutes from './config/routes/userNotificationRoutes'
import {
importEvent,
importEvents,
importLocations,
syncLocationLevels,
syncLocationStatistics
} from './analytics/analytics'
import { getClient } from './analytics/postgres'
import { env } from './environment'
import { createClient } from '@opencrvs/toolkit/api'

export interface ITokenPayload {
sub: string
Expand Down Expand Up @@ -605,6 +608,12 @@ export async function createServer() {
if (queue.length > 0) {
await importEvents(queue, trx)
}

// Import locations
const url = new URL('events', GATEWAY_URL).toString()
const client = createClient(url, req.headers.authorization)
const locations = await client.locations.list.query()
await importLocations(locations)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Transaction Inconsistency in Import Locations

The importLocations call is made within the event reindexing transaction but creates its own separate transaction. This breaks atomicity, potentially leaving the analytics database inconsistent if the main transaction fails. Additionally, the client fetching locations is initialized with an 'events' endpoint URL.

Fix in Cursor Fix in Web

})

logger.info('Reindexed all events into analytics.')
Expand Down Expand Up @@ -687,6 +696,7 @@ export async function createServer() {
const parsedPath = /^\/trigger\/events\/[^/]+\/actions\/([^/]+)$/.exec(
request.route.path
)

const actionType = parsedPath?.[1] as ActionType | null
const wasRequestForActionConfirmation =
actionType && request.method === 'post'
Expand All @@ -695,9 +705,15 @@ export async function createServer() {
if (wasRequestForActionConfirmation && wasActionAcceptedImmediately) {
const event = request.payload as EventDocument
const client = getClient()
await client.transaction().execute(async (trx) => {
await importEvent(event, trx)
})
try {
await client.transaction().execute(async (trx) => {
await importEvent(event, trx)
})
} catch (error) {
// eslint-disable-next-line no-console
console.error(error)
throw error
}
}
return h.continue
})
Expand Down
Loading