-
Notifications
You must be signed in to change notification settings - Fork 11
closes #161 - add Ignite key-value in-memory database #247
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
base: master
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
const IgniteClient = require("apache-ignite-client"); | ||
const logger = require("./../../lib/log")(__filename); | ||
const { SqlFieldsQuery } = require("apache-ignite-client/lib/Query"); | ||
const IgniteClientConfiguration = IgniteClient.IgniteClientConfiguration; | ||
const CacheConfiguration = IgniteClient.CacheConfiguration; | ||
require("dotenv").config(); | ||
let igniteModule = {}; | ||
let igniteClient; | ||
igniteModule.startIgnite = async () => { | ||
try { | ||
igniteClient = new IgniteClient(); | ||
const configuration = new IgniteClientConfiguration(process.env.IGNITE_HOST) | ||
.setUserName(process.env.IGNITE_USER) | ||
.setPassword(process.env.IGNITE_PASSWORD); | ||
await igniteClient.connect(configuration); | ||
logger.info("Connected to Ignite database"); | ||
} catch (err) { | ||
logger.error(err); | ||
throw new Error(err); | ||
} | ||
}; | ||
igniteModule.checkAccount = async (username) => { | ||
if (!username) return; | ||
try { | ||
const caches = await igniteClient.cacheNames(); | ||
return caches.includes(username); | ||
} catch (err) { | ||
logger.error(`Error checking ignite database for ${username}`); | ||
throw new Error(err); | ||
} | ||
}; | ||
igniteModule.closeIgnite = async () => { | ||
try { | ||
await igniteClient.disconnect(); | ||
logger.info("Closed connection to Ignite database"); | ||
} catch (err) { | ||
logger.error(`Error closing connection to Ignite database`); | ||
throw new Error(err); | ||
} | ||
}; | ||
|
||
igniteModule.createAccount = async (user) => { | ||
const { username, dbPassword } = user; | ||
if (!username || !dbPassword) return; | ||
try { | ||
const cache = await igniteClient.getOrCreateCache( | ||
username, | ||
new CacheConfiguration().setSqlSchema("PUBLIC") | ||
); | ||
await cache.query( | ||
new SqlFieldsQuery( | ||
`CREATE USER "${username}" WITH PASSWORD '${dbPassword}'` | ||
) | ||
); | ||
logger.info(`Successfully created ${username} cache`); | ||
} catch (err) { | ||
logger.error(`Error creating ignite ${username}`); | ||
throw new Error(err); | ||
} | ||
}; | ||
igniteModule.deleteAccount = async (username) => { | ||
if (!username) return; | ||
try { | ||
const cache = await igniteClient.getOrCreateCache( | ||
username, | ||
new CacheConfiguration().setSqlSchema("PUBLIC") | ||
); | ||
await cache.query(new SqlFieldsQuery(`DROP USER "${username}"`)); | ||
await igniteClient.destroyCache(username); | ||
logger.info(`Successfully deleted ${username} cache`); | ||
} catch (err) { | ||
logger.error(`Error deleting ignite ${username}`); | ||
throw new Error(err); | ||
} | ||
}; | ||
module.exports = igniteModule; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
jest.mock("apache-ignite-client"); | ||
jest.mock("../../lib/log"); | ||
const logGen = require("../../lib/log"); | ||
const logger = { | ||
info: jest.fn(), | ||
error: jest.fn(), | ||
}; | ||
logGen.mockReturnValue(logger); | ||
require("dotenv").config(); | ||
const { | ||
startIgnite, | ||
createAccount, | ||
deleteAccount, | ||
checkAccount, | ||
closeIgnite, | ||
} = require("./ignite"); | ||
|
||
const IgniteClient = require("apache-ignite-client"); | ||
const mockClient = { | ||
cacheNames: jest.fn(), | ||
connect: jest.fn(), | ||
disconnect: jest.fn(), | ||
getOrCreateCache: jest.fn().mockReturnThis(), | ||
query: jest.fn(), | ||
destroyCache: jest.fn(), | ||
}; | ||
IgniteClient.mockImplementation(() => mockClient); | ||
const mockConfig = { | ||
setUserName: jest.fn().mockReturnThis(), | ||
setPassword: jest.fn().mockReturnThis(), | ||
}; | ||
IgniteClient.IgniteClientConfiguration.mockImplementation(() => mockConfig); | ||
describe("IgniteDb functions", () => { | ||
beforeEach(() => { | ||
jest.clearAllMocks(); | ||
}); | ||
const user = { username: "username", dbPassword: "password" }; | ||
it("should start Ignite client", () => { | ||
startIgnite(); | ||
expect(IgniteClient).toHaveBeenCalledTimes(1); | ||
expect(mockClient.connect).toHaveBeenCalledTimes(1); | ||
expect(IgniteClient.IgniteClientConfiguration).toHaveBeenCalledTimes(1); | ||
expect(mockConfig.setUserName.mock.calls[0][0]).toEqual("ignite"); | ||
expect(mockConfig.setPassword.mock.calls[0][0]).toEqual("ignite"); | ||
}); | ||
it("should throw an error if starting client goes wrong", async () => { | ||
try { | ||
mockClient.connect.mockReturnValue(Promise.reject()); | ||
expect(await startIgnite()).rejects.toThrow(); | ||
} catch (err) { | ||
expect(logger.error).toHaveBeenCalledTimes(1); | ||
} | ||
}); | ||
it("should close ignite client", () => { | ||
closeIgnite(); | ||
expect(mockClient.disconnect).toHaveBeenCalledTimes(1); | ||
}); | ||
it("should call logger error if closing client goes wrong", async () => { | ||
try { | ||
mockClient.disconnect.mockReturnValue(Promise.reject()); | ||
await closeIgnite(); | ||
} catch (err) { | ||
expect(logger.error).toHaveBeenCalledTimes(1); | ||
} | ||
}); | ||
it("should check for existsting accounts", async () => { | ||
mockClient.cacheNames.mockReturnValue(["testName", "", "username"]); | ||
const result = await checkAccount(user.username); | ||
expect(mockClient.cacheNames).toHaveBeenCalledTimes(1); | ||
expect(result).toEqual(true); | ||
}); | ||
it("should call logger error if checking for account goes wrong", async () => { | ||
try { | ||
mockClient.cacheNames.mockReturnValue(Promise.reject()); | ||
await checkAccount(user.username); | ||
} catch (err) { | ||
expect(logger.error).toHaveBeenCalledTimes(1); | ||
} | ||
}); | ||
it("should create new account", async () => { | ||
await createAccount(user); | ||
expect(mockClient.getOrCreateCache.mock.calls[0][0]).toEqual(user.username); | ||
}); | ||
it("should delete user", async () => { | ||
await deleteAccount(user.username); | ||
expect(mockClient.getOrCreateCache.mock.calls[0][0]).toEqual(user.username); | ||
expect(mockClient.destroyCache.mock.calls[0][0]).toEqual(user.username); | ||
}); | ||
it("should call logger error if creating a user goes wrong", async () => { | ||
try { | ||
mockClient.getOrCreateCache.mockReturnValue(Promise.reject()); | ||
expect(await createAccount(user)).rejects.toThrow(); | ||
} catch (err) { | ||
expect(logger.error).toHaveBeenCalledTimes(1); | ||
} | ||
}); | ||
it("should return undefined if no arguments were provided", async () => { | ||
expect(await createAccount({})).toEqual(undefined); | ||
expect(await checkAccount("")).toEqual(undefined); | ||
expect(await deleteAccount("")).toEqual(undefined); | ||
}); | ||
it("should calll logger error if deleting user goes wrong", async () => { | ||
try { | ||
mockClient.getOrCreateCache.mockReturnValue(Promise.reject()); | ||
await deleteAccount(user.username); | ||
} catch (err) { | ||
expect(logger.error).toHaveBeenCalledTimes(1); | ||
} | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
<?xml version="1.0" encoding="UTF-8"?> | ||
<beans xmlns="http://www.springframework.org/schema/beans" | ||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | ||
xsi:schemaLocation=" | ||
http://www.springframework.org/schema/beans | ||
http://www.springframework.org/schema/beans/spring-beans.xsd"> | ||
<bean class="org.apache.ignite.configuration.IgniteConfiguration"> | ||
<property name="dataStorageConfiguration"> | ||
<bean class="org.apache.ignite.configuration.DataStorageConfiguration"> | ||
<property name="defaultDataRegionConfiguration"> | ||
<bean class="org.apache.ignite.configuration.DataRegionConfiguration"> | ||
<property name="maxSize" value="#{200 * 1024 * 1024}"/> | ||
<property name="persistenceEnabled" value="true"/> | ||
</bean> | ||
</property> | ||
</bean> | ||
</property> | ||
|
||
<property name="authenticationEnabled" value="true"/> | ||
|
||
</bean> | ||
</beans> |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,6 +3,7 @@ const db = require("../sequelize/db"); | |
const pg = require("../database/postgres/pg"); | ||
const arango = require("../database/arango/arango"); | ||
const es = require("../database/elasticsearch/elastic"); | ||
const ignite = require("../database/ignite/ignite"); | ||
const logger = require("./log")(__filename); | ||
|
||
const util = {}; | ||
|
@@ -37,6 +38,9 @@ util.cleanAnonymous = async () => { | |
const arangoDbExists = await arango.checkIfDatabaseExists(username); | ||
if (arangoDbExists) await arango.deleteAccount(username); | ||
|
||
const igniteDbExists = await ignite.checkAccount(username); | ||
if(igniteDbExists) await ignite.deleteAccount(username); | ||
|
||
|
||
return await user.destroy(); | ||
}) | ||
).then(() => { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,13 +3,15 @@ jest.mock("../sequelize/db"); | |
jest.mock("../database/postgres/pg"); | ||
jest.mock("../database/arango/arango"); | ||
jest.mock("../database/elasticsearch/elastic"); | ||
jest.mock("../database/ignite/ignite"); | ||
jest.mock("./log"); | ||
|
||
const sequelize = require("sequelize"); | ||
const db = require("../sequelize/db"); | ||
const pg = require("../database/postgres/pg"); | ||
const es = require("../database/elasticsearch/elastic"); | ||
const arango = require("../database/arango/arango"); | ||
const ignite = require("../database/ignite/ignite"); | ||
|
||
sequelize.Op = { and: "and", lt: "lt" }; | ||
const Accounts = { | ||
|
@@ -21,6 +23,7 @@ db.getModels = () => { | |
pg.deletePgAccount = jest.fn(); | ||
es.deleteAccount = jest.fn(); | ||
arango.deleteAccount = jest.fn(); | ||
ignite.deleteAccount = jest.fn(); | ||
const logGen = require("./log"); | ||
const logger = { | ||
info: jest.fn(), | ||
|
@@ -58,16 +61,19 @@ describe("Testing cleanAnonymous function", () => { | |
expect(pg.deletePgAccount).not.toHaveBeenCalled(); | ||
expect(es.deleteAccount).not.toHaveBeenCalled(); | ||
expect(arango.deleteAccount).not.toHaveBeenCalled(); | ||
expect(ignite.deleteAccount).not.toHaveBeenCalled(); | ||
}); | ||
it("should call database functions if expired accounts are found", async () => { | ||
Accounts.findAll.mockReturnValue([{ destroy: () => {} }]); | ||
pg.userHasPgAccount = () => true; | ||
es.checkAccount = () => true; | ||
arango.checkIfDatabaseExists = () => true; | ||
ignite.checkAccount = ()=>true; | ||
|
||
await util.cleanAnonymous(); | ||
expect(pg.deletePgAccount).toHaveBeenCalled(); | ||
expect(es.deleteAccount).toHaveBeenCalled(); | ||
expect(arango.deleteAccount).toHaveBeenCalled(); | ||
expect(ignite.deleteAccount).toHaveBeenCalled(); | ||
}); | ||
it("should call logger.error if cleaning fails", async () => { | ||
Accounts.findAll.mockImplementation(() => { | ||
|
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
|
||
# Intro | ||
|
||
In-memory database is a database which relies primarily on memory for data storage. | ||
|
||
Pros: | ||
* Performance. RAM is way faster than any disks IOs | ||
|
||
Cons: | ||
* Data persistence can be tricky | ||
|
||
Some datasets could be stored entirely in memory but most often in-memory databases are used as caches. | ||
|
||
# Apache Ignite | ||
|
||
Ignite is an open source distributed in-memory database. It can be used as cache, data grid or key-value store. In our case we will use the latter. | ||
|
||
Key-value store is the simplest data model, one unique key which is mapped to some value. With no schema limitations writes are very fast, but since database doesn't know anything about the value you can't use indexes to speed up searches. | ||
|
||
# Client | ||
|
||
Ignite provides their own node.js client [apache-ignite-client](https://www.npmjs.com/package/apache-ignite-client). | ||
|
||
To connect to database: | ||
``` | ||
const IgniteClient = require("apache-ignite-client"); | ||
const IgniteClientConfiguration = IgniteClient.IgniteClientConfiguration; | ||
|
||
const igniteClientConfiguration = new IgniteClientConfiguration("host") | ||
.setUserName("username") | ||
.setPassword("password"); | ||
const connect = async () => { | ||
const igniteClient = new IgniteClient(); | ||
await igniteClient.connect(igniteClientConfiguration); | ||
}; | ||
connect(); | ||
``` | ||
|
||
To add values into cache you need to connect to cache, specify data type and its key: | ||
|
||
``` | ||
const ObjectType = IgniteClient.ObjectType; | ||
const basicOperations = async () => { | ||
const igniteClient = new IgniteClient(); | ||
await igniteClient.connect(igniteClientConfiguration); | ||
console.log("successfully connected"); | ||
const cache = (await igniteClient.getOrCreateCache("username")).setKeyType( | ||
ObjectType.PRIMITIVE_TYPE.INTEGER | ||
); | ||
await cache.put(1, "hello world"); | ||
console.log(await cache.get(1)); | ||
igniteClient.disconnect(); | ||
}; | ||
basicOperations(); | ||
``` | ||
|
||
You can find more examples [here](https://github.com/apache/ignite/tree/master/modules/platforms/nodejs/examples). | ||
|
||
*Under construction* |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
igniteModule
should be declaredconst
instead oflet