Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: make etag calculation sensitive to inscription location gap fills and upserts #156

Merged
merged 2 commits into from
Jul 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions migrations/1676395230930_inscriptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,17 @@ export function up(pgm: MigrationBuilder): void {
curse_type: {
type: 'text',
},
updated_at: {
type: 'timestamptz',
default: pgm.func('(NOW())'),
notNull: true,
},
});
pgm.createConstraint('inscriptions', 'inscriptions_number_unique', 'UNIQUE(number)');
pgm.createIndex('inscriptions', ['genesis_id']);
pgm.createIndex('inscriptions', ['mime_type']);
pgm.createIndex('inscriptions', ['sat_ordinal']);
pgm.createIndex('inscriptions', ['sat_rarity']);
pgm.createIndex('inscriptions', ['sat_coinbase_height']);
pgm.createIndex('inscriptions', [{ name: 'updated_at', sort: 'DESC' }]);
}
14 changes: 7 additions & 7 deletions src/api/util/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { InscriptionIdParamCType, InscriptionNumberParamCType } from '../schemas
import { logger } from '@hirosystems/api-toolkit';

export enum ETagType {
inscriptionTransfers,
inscriptionsIndex,
inscription,
inscriptionsPerBlock,
}
Expand All @@ -24,7 +24,7 @@ export async function handleInscriptionTransfersCache(
request: FastifyRequest,
reply: FastifyReply
) {
return handleCache(ETagType.inscriptionTransfers, request, reply);
return handleCache(ETagType.inscriptionsIndex, request, reply);
}

export async function handleInscriptionsPerBlockCache(
Expand All @@ -41,8 +41,8 @@ async function handleCache(type: ETagType, request: FastifyRequest, reply: Fasti
case ETagType.inscription:
etag = await getInscriptionLocationEtag(request);
break;
case ETagType.inscriptionTransfers:
etag = await getInscriptionTransfersEtag(request);
case ETagType.inscriptionsIndex:
etag = await getInscriptionsIndexEtag(request);
break;
case ETagType.inscriptionsPerBlock:
etag = await request.server.db.getInscriptionsPerBlockETag();
Expand Down Expand Up @@ -87,13 +87,13 @@ async function getInscriptionLocationEtag(request: FastifyRequest): Promise<stri
}

/**
* Get an ETag based on the last state of inscription transfers.
* Get an ETag based on the last state of all inscriptions.
* @param request - Fastify request
* @returns ETag string
*/
async function getInscriptionTransfersEtag(request: FastifyRequest): Promise<string | undefined> {
async function getInscriptionsIndexEtag(request: FastifyRequest): Promise<string | undefined> {
try {
return await request.server.db.getInscriptionTransfersETag();
return await request.server.db.getInscriptionsIndexETag();
} catch (error) {
return;
}
Expand Down
25 changes: 15 additions & 10 deletions src/pg/pg-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -267,9 +267,11 @@ export class PgStore extends BasePgStore {
}
}

async getInscriptionTransfersETag(): Promise<string> {
const result = await this.sql<{ max: number }[]>`SELECT MAX(id) FROM locations`;
return result[0].max.toString();
async getInscriptionsIndexETag(): Promise<string> {
const result = await this.sql<{ etag: string }[]>`
SELECT date_part('epoch', MAX(updated_at))::text AS etag FROM inscriptions
`;
return result[0].etag;
}

async getInscriptionsPerBlockETag(): Promise<string> {
Expand Down Expand Up @@ -301,14 +303,12 @@ export class PgStore extends BasePgStore {

async getInscriptionETag(args: InscriptionIdentifier): Promise<string | undefined> {
const result = await this.sql<{ etag: string }[]>`
SELECT date_part('epoch', l.timestamp)::text AS etag
FROM locations AS l
INNER JOIN current_locations AS c ON l.id = c.location_id
INNER JOIN inscriptions AS i ON l.inscription_id = i.id
SELECT date_part('epoch', updated_at)::text AS etag
FROM inscriptions
WHERE ${
'genesis_id' in args
? this.sql`i.genesis_id = ${args.genesis_id}`
: this.sql`i.number = ${args.number}`
? this.sql`genesis_id = ${args.genesis_id}`
: this.sql`number = ${args.number}`
}
`;
if (result.count > 0) {
Expand Down Expand Up @@ -591,7 +591,8 @@ export class PgStore extends BasePgStore {
fee = EXCLUDED.fee,
sat_ordinal = EXCLUDED.sat_ordinal,
sat_rarity = EXCLUDED.sat_rarity,
sat_coinbase_height = EXCLUDED.sat_coinbase_height
sat_coinbase_height = EXCLUDED.sat_coinbase_height,
updated_at = NOW()
RETURNING id
`;
inscription_id = inscription[0].id;
Expand Down Expand Up @@ -826,6 +827,10 @@ export class PgStore extends BasePgStore {
SET inscription_id = ${args.inscription_id}
WHERE genesis_id = ${args.genesis_id} AND inscription_id IS NULL
`;
// Update the inscription's `updated_at` timestamp for caching purposes.
await sql`
UPDATE inscriptions SET updated_at = NOW() WHERE genesis_id = ${args.genesis_id}
`;
});
}
}
103 changes: 94 additions & 9 deletions tests/cache.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ describe('ETag cache', () => {
});
expect(response.statusCode).toBe(200);
expect(response.headers.etag).not.toBeUndefined();
const etag = response.headers.etag;
const etag1 = response.headers.etag;

// Check on numbered id too
const nResponse = await fastify.inject({
Expand All @@ -59,36 +59,88 @@ describe('ETag cache', () => {
expect(nResponse.statusCode).toBe(200);
expect(nResponse.headers.etag).not.toBeUndefined();
const nEtag = nResponse.headers.etag;
expect(nEtag).toBe(etag);
expect(nEtag).toBe(etag1);

// Cached response
const cached = await fastify.inject({
method: 'GET',
url: '/ordinals/v1/inscriptions/38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0',
headers: { 'if-none-match': etag },
headers: { 'if-none-match': etag1 },
});
expect(cached.statusCode).toBe(304);
const nCached = await fastify.inject({
method: 'GET',
url: '/ordinals/v1/inscriptions/7',
headers: { 'if-none-match': etag },
headers: { 'if-none-match': etag1 },
});
expect(nCached.statusCode).toBe(304);

// Simulate modified location and check status code
await db.sql`UPDATE locations SET timestamp = NOW() WHERE true`;
// Perform transfer and check cache
await db.updateInscriptions(
new TestChainhookPayloadBuilder()
.apply()
.block({ height: 775700, timestamp: 1678122360 })
.transaction({
hash: '0xbdda0d240132bab2af7f797d1507beb1acab6ad43e2c0ef7f96291aea5cc3444',
})
.inscriptionTransferred({
inscription_id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0',
updated_address: 'bc1p3xqwzmddceqrd6x9yxplqzkl5vucta2gqm5szpkmpuvcvgs7g8psjf8htd',
satpoint_pre_transfer:
'da2da520f055e9fadaf1a78b3e01bc53596dcbb88e9c9f53bcb61b98310b1006:0:0',
satpoint_post_transfer:
'bdda0d240132bab2af7f797d1507beb1acab6ad43e2c0ef7f96291aea5cc3444:0:0',
post_transfer_output_value: 8000,
tx_index: 0,
})
.build()
);
const cached2 = await fastify.inject({
method: 'GET',
url: '/ordinals/v1/inscriptions/38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0',
headers: { 'if-none-match': etag },
headers: { 'if-none-match': etag1 },
});
expect(cached2.statusCode).toBe(200);
const nCached2 = await fastify.inject({
method: 'GET',
url: '/ordinals/v1/inscriptions/7',
headers: { 'if-none-match': etag },
headers: { 'if-none-match': etag1 },
});
expect(nCached2.statusCode).toBe(200);
const etag2 = cached2.headers.etag;

// Perform transfer GAP FILL and check cache
await db.updateInscriptions(
new TestChainhookPayloadBuilder()
.apply()
.block({ height: 775690, timestamp: 1678122360 })
.transaction({
hash: 'bebb1357c97d2348eb8ef24e1d8639ff79c8847bf12999ca7fef463489b40f0f',
})
.inscriptionTransferred({
inscription_id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0',
updated_address: 'bc1p3xqwzmddceqrd6x9yxplqzkl5vucta2gqm5szpkmpuvcvgs7g8psjf8htd',
satpoint_pre_transfer:
'38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc:0:0',
satpoint_post_transfer:
'da2da520f055e9fadaf1a78b3e01bc53596dcbb88e9c9f53bcb61b98310b1006:0:0',
post_transfer_output_value: 8000,
tx_index: 0,
})
.build()
);
const cached3 = await fastify.inject({
method: 'GET',
url: '/ordinals/v1/inscriptions/38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0',
headers: { 'if-none-match': etag2 },
});
expect(cached3.statusCode).toBe(200);
const nCached3 = await fastify.inject({
method: 'GET',
url: '/ordinals/v1/inscriptions/7',
headers: { 'if-none-match': etag2 },
});
expect(nCached3.statusCode).toBe(200);
});

test('inscriptions index cache control', async () => {
Expand Down Expand Up @@ -158,7 +210,7 @@ describe('ETag cache', () => {
});
expect(cached.statusCode).toBe(304);

// Simulate new location
// New location
const block3 = new TestChainhookPayloadBuilder()
.apply()
.block({ height: 775618 })
Expand All @@ -181,6 +233,39 @@ describe('ETag cache', () => {
headers: { 'if-none-match': etag },
});
expect(cached2.statusCode).toBe(200);
const etag2 = cached2.headers.etag;

// Upsert genesis location
const block4 = new TestChainhookPayloadBuilder()
.apply()
.block({ height: 778575 })
.transaction({ hash: '0x9f4a9b73b0713c5da01c0a47f97c6c001af9028d6bdd9e264dfacbc4e6790201' })
.inscriptionRevealed({
content_bytes: '0x48656C6C6F',
content_type: 'text/plain',
content_length: 5,
inscription_number: 7,
inscription_fee: 705,
inscription_id: '9f4a9b73b0713c5da01c0a47f97c6c001af9028d6bdd9e264dfacbc4e6790201i0',
inscription_output_value: 10000,
inscriber_address: 'bc1pscktlmn99gyzlvymvrezh6vwd0l4kg06tg5rvssw0czg8873gz5sdkteqj',
ordinal_number: 257418248345364,
ordinal_block_height: 650000,
ordinal_offset: 0,
satpoint_post_inscription:
'9f4a9b73b0713c5da01c0a47f97c6c001af9028d6bdd9e264dfacbc4e6790201:0:0',
inscription_input_index: 0,
transfers_pre_inscription: 0,
tx_index: 0,
})
.build();
await db.updateInscriptions(block4);
const cached3 = await fastify.inject({
method: 'GET',
url: '/ordinals/v1/inscriptions',
headers: { 'if-none-match': etag2 },
});
expect(cached3.statusCode).toBe(200);
});

test('inscriptions stats per block cache control', async () => {
Expand Down
Loading