Problem
Different application consuming the API’s but the backend has no idea who is consuming it and from where the request coming is from.
...
To avoid querying the datastore every time, we can cache custom entities in-memory on the node, so that frequent entity lookups don't trigger a datastore query every time, but happen in-memory, which is much faster and reliable than querying it from the datastore (especially under heavy load).
A. Lazy caching: Cache custom entities in-memory on the node on the first request, so that frequent entity lookups don’t trigger a datastore query every time (only the first time), but happen in-memory. On every first request by a consumer, associated App IDs to requester consumer will be retrieved from datastore and that will be cached, but If there are no APP IDs associated then empty App ID's arrays will be cached.
By doing so it doesn’t matter how many requests the consumer makes, after the first request every lookup will be done in-memory without querying the datastore.
- Pros
- Size of cache will be less as it will cache the records that are in use.
- The object is only loaded from the datastore only one time per consumer.
- Cons
- Every first request by each consumer will hit the datastore.
...
- In a multi node cluster, the data store may be hit once per node per consumer since the cache is local
We will use the following kong utility method to get APP-ID mapped to consumer. The method takes care of querying the datastore only the first time. Subsequent calls will return cached data.
Method | Description |
---|---|
value = cache.get_or_set(key, function) | This is an utility method that retrieves an object with the specified key, but if the object is nil then the passed function will be executed instead, whose return value will be used to store the object at the specified key. This effectively makes sure that the object is only loaded from the datastore one time, since every other invocation will load the object from the in-memory cache. |
...
. |
...
E.g
{
appid.e1a622e8-c351-432a-83f5-a4e0e300449c : ["Portal", "Mobile"],
appid.89a41fef-3b40-4bb0-b5af-33da57a7ffcf : ["NTP"],
appid.2fc51405-5991-44c3-b851-bf5c88be8310 : [ ],
}
B. Full table cache on kong service start: Cache the all the records exist in the table on every node immediately after kong service start, so entity lookups don’t trigger a datastore query every time.
- Pros
- No lookup on the data-store except initial so it will help for reducing latency in request response time.
- Cons
- If the database is size is large then the cache and invalidation process will take time.
- Every Node will query datastore after the start of service that will put a load on data-store for some time.
- It will cache the all records even if record not in use and that will unnecessarily increase cache size on every node.
Kong does not seem to have a specific "event" that is broadcasted on startup, so this approach may not be feasible. init_worker is available, but the implementation for that seems complex.
Cache Storage
The cache will be stored in the key-value pair where the key will be 'appid
'.consumer_id and value will be an array of APP ids.
E.g
{ appid.e1a622e8-c351-432a-83f5-a4e0e300449c : ["Portal", "Mobile"] }
{ appid.89a41fef-3b40-4bb0-b5af-33da57a7ffcf : ["NTP"] }
{ appid.2fc51405-5991-44c3-b851-bf5c88be8310 : [ ] }
Cache Invalidation
Every time an entity is being created/updated/deleted in the datastore, Kong notifies the datastore operation across all the nodes telling what command has been executed and what entity has been affected by it.
...
Code Block | ||
---|---|---|
| ||
-- hooks.lua local events = require "kong.core.events" local cache = require "kong.tools.database_cache" local function invalidate_on_update(message_t) if message_t.collection == "appids" then cache.delete("appid."..message_t.old_entity.consumer_id) end end local function invalidate_on_delete(message_t) if message_t.collection == "appids" then cache.delete("apikeysappid."..message_t.entity.consumer_id) end end return { [events.TYPES.ENTITY_UPDATED] = function(message_t) invalidate_on_update(message_t) end, [events.TYPES.ENTITY_DELETED] = function(message_t) invalidate_on_delete(message_t) end } |
...
In the example above the plugin is listening to the ENTITY_UPDATED
and ENTITY_DELETED
events and responding by invoking the appropriate function. The message_t
table contains the event properties
...
Error message
- If App ID is not sent with the request: X-APP-ID can't be blank
- Consumer and APP-ID mapping doesn't exist: Consumer and X-APP-ID mapping doesn't exist
- Invalid App ID: Invalid X-APP-ID
Questions / Note
- Can we map same APP ID to different consumers ?
- Process for issuing app id needs to be updated - for developer installation (in this case Sunbird credentials are needed and provided by support team) and for server installation (in this case Ekstep credentials will be needed which the developer currently creates himself)
Migration
- Existing consumers that are using app id need to be updated
Proposed format for appid
To ensure the app ids are named consistently, the following format is proposed
<organisation name>.<app name>
Eg: arghyam.mobile_app, shikshalokam.portal
The app id allows lowercase alphabets, numbers and dot.
Database table structure
Column | Type |
---|---|
id | uuid, Primary Key |
consumer_id | uuid, Not Null |
appid | varchar(255100), Not Null |
created_at | timestamp |
...
Retrieve consumers associated with the App Id
Note: Kong upgrade required
$ curl -X GET http://localhost:8001/appids/{id}/consumer
{
"created_at":1507936639000,
"username":"foo",
"id":"c0d92ba9-8306-482a-b60d-0cfdd2f0e880"
}