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

Add initial PostgreSQL implementation for get & put operations #5

Merged
merged 2 commits into from
Apr 21, 2023

Conversation

G8XSU
Copy link
Collaborator

@G8XSU G8XSU commented Jan 13, 2023

TestStrategy

How?

  • PostgresIntegrationTest uses testcontainers to spin up docker containers for each test.
  • AbstractKVStoreIntegrationTest defines behavior that every impl of KVStore needs to follow. Different impl's can re-use the same abstract class to test their impl against it.
  • We will keep on adding tests to this as we add more impl details.

@G8XSU G8XSU requested a review from jkczyz January 13, 2023 01:58
@G8XSU G8XSU changed the title Add basic PostgreSQL implementation for get & put operations Add initial PostgreSQL implementation for get & put operations Jan 13, 2023
@G8XSU G8XSU requested a review from devrandom January 17, 2023 22:36
app/build.gradle Outdated
generateSchemaSourceOnCompilation = true

generationTool {
Copy link
Member

Choose a reason for hiding this comment

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

we need to make a decision if we check in the generated Java code or not. personally, I'm a bit uncomfortable with generating code on the fly on prod machines, but let's see what other people think.

Copy link

Choose a reason for hiding this comment

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

Discussed this offline. Prefer to check in the code but also provide a way for users to generate it at as an option to the build process.


VssDbRecord globalVersionRecord = buildVssRecord(storeId,
KeyValue.newBuilder()
.setKey(GLOBAL_VERSION_KEY)
Copy link
Member

Choose a reason for hiding this comment

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

I think the global version should be stored in a separate table. otherwise, you have to make sure the caller doesn't try to use this key, and it seems messy.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The advantage is that it can be treated as any other keyvalue and easily handled with exactly same cases.

Other consideration is, if we want client to be able to override global_version in certain scenarios. My take is we should allow, end of the day its their storage and versioning, they can do whatever they want with it.

There is also case where client just wants to do a "get" on global_version.
For all purposes, it seems simpler to keep it as normal key-value, otherwise i will need to introduce additional code to support everything.

One case that we will have to handle separately from normal key-value is to not return global_version as part of ListKeyVersions api. (we have dedicated field in response for it)

app/build.gradle Outdated Show resolved Hide resolved
@devrandom
Copy link
Member

also, would be good to have an integration test. not sure if it's better to have it against postgres (more precise model of a production environment) or against a lightweight in-memory SQL DB (faster to run the tests).

.setKey(GLOBAL_VERSION_KEY)
.setVersion(request.getGlobalVersion())
.build());
if (request.hasGlobalVersion()) {
Copy link
Member

Choose a reason for hiding this comment

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

this doesn't increment the global version when it's not specified in the request. we always want to increment it, right?

Copy link
Member

Choose a reason for hiding this comment

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

I guess we could have the specifications say that it doesn't get incremented if not present in the request, but we should be explicit about it, since the developer might not expect that

Copy link
Collaborator Author

@G8XSU G8XSU Jan 25, 2023

Choose a reason for hiding this comment

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

Based on how optimistic locking works, version needs to be provided by client in order to enact it.
So expectation is that we don't increment it and dont perform the check if developer doesn't supply it.

If global_version is not supplied, it means it is a non-global-version-check-required write.
We shouldn't be incrementing global_version in this case as client-side will not increment and has no way of knowing this without performing a sync.

Based on different application needs, some might not need a global_version check on every write, and this feature is meant to support those applications.

@G8XSU
Copy link
Collaborator Author

G8XSU commented Jan 25, 2023

Added an integration test for AbstractKVStore and PostgresIntegration test uses it.
Can review it as part of this PR or we can separate it out as well in #5

postgreSQLContainer.getUsername(), postgreSQLContainer.getPassword());
DSLContext dslContext = DSL.using(conn, SQLDialect.POSTGRES);

this.kvStore = new PostgresBackendImpl(dslContext);
Copy link
Member

Choose a reason for hiding this comment

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

don't we need to close the previous DB connection, otherwise we may run out of file descriptors or such if we have many tests?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Each and every test spins up new db cluster/instance so db connection shouldn't really be a problem.
But I added a AfterEach block to destroy connection in any case.

For production, we are using connection pool and DSL.using(dataSource, dialect) where jooq/pool does the connection management for us.

@G8XSU G8XSU marked this pull request as ready for review January 26, 2023 01:38
Copy link

@jkczyz jkczyz left a comment

Choose a reason for hiding this comment

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

Need to get through the tests still

app/build.gradle Outdated
generateSchemaSourceOnCompilation = true

generationTool {
Copy link

Choose a reason for hiding this comment

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

Discussed this offline. Prefer to check in the code but also provide a way for users to generate it at as an option to the build process.


ListKeyVersionsResponse listKeyVersions(ListKeyVersionsRequest request);
}

Copy link

Choose a reason for hiding this comment

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

nit: no blanks at end of file here and throughout

@@ -0,0 +1,8 @@
CREATE TABLE vss_db (
store_id character varying(120) NOT NULL,
Copy link

Choose a reason for hiding this comment

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

Is there a convention to have two spaces before NULL / NOT NULL?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

idk, just a copy-paste side-effect from describe table in postgres i guess, will replace with single spaces.

store_id character varying(120) NOT NULL,
key character varying(120) NOT NULL,
value bytea NULL,
version bigint NOT NULL,
Copy link

Choose a reason for hiding this comment

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

Was the choice of using a signed integer in the vss.proto based on the fact the database doesn't support unsigned types?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Mostly because many languages(kotlin/java/python) don't have good support for unsigned values.
So we avoid using them directly in interface if possible. (had this feedback from cashapp as well)

Comment on lines 49 to 54
dslContext.execute("CREATE TABLE vss_db ("
+ "store_id character varying(120) NOT NULL CHECK (store_id <> ''),"
+ "key character varying(120) NOT NULL,"
+ "value bytea NULL,"
+ "version bigint NOT NULL,"
+ "PRIMARY KEY (store_id, key));");
Copy link

Choose a reason for hiding this comment

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

Would it be possible to take this from the sql file?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Note that the sql file is present only for documentation purpose and git history.
Its not going to be directly used in db creation, will need to be done manually.
I wanted to avoid a filepath dependency in tests, let me know your thoughts.

Copy link

Choose a reason for hiding this comment

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

Could creation be accomplished through a script / program that reads the schema?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

jooq doesn't create table for us and we assume in production that db schema is already created.
In tests however we are creating a fresh instance of db for every test and need to create table/schema everytime.


private void createTable(DSLContext dslContext) {
dslContext.execute("CREATE TABLE vss_db ("
+ "store_id character varying(120) NOT NULL CHECK (store_id <> ''),"
Copy link

Choose a reason for hiding this comment

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

Why does this line differ from the sql file?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

will fix this.

Comment on lines +53 to +56
} else {
keyValue = KeyValue.newBuilder()
.setKey(request.getKey()).build();
}
Copy link

Choose a reason for hiding this comment

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

Should we return an error if there is no record for the key?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I think an empty response is preferable in this case.
ResourceNotFound or 404 might represent a myriad of issues and things going wrong. (url wrong etc.)
In this case, we consider it a perfectly valid request to get a key which does not exist or to check existence of a key.
I see no harm in mixing keys that exist with no data and keys which don't exist, to make clients life easier instead of throwing an exception in one case.

Note: This is only applicable if client has permission to storeId, if not then we would want to throw ResourceNotFound.

However, this is a controversial topic and there are reasons to go either way.

Comment on lines 49 to 54
dslContext.execute("CREATE TABLE vss_db ("
+ "store_id character varying(120) NOT NULL CHECK (store_id <> ''),"
+ "key character varying(120) NOT NULL,"
+ "value bytea NULL,"
+ "version bigint NOT NULL,"
+ "PRIMARY KEY (store_id, key));");
Copy link

Choose a reason for hiding this comment

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

Could creation be accomplished through a script / program that reads the schema?

KeyValue response = getObject("non_existent_key");

assertThat(response.getKey(), is("non_existent_key"));
assertTrue(response.getValue().isEmpty());
Copy link

Choose a reason for hiding this comment

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

Also check that version is empty / zero?

Comment on lines +86 to +94
int[] batchResult = dsl.batch(batchQueries).execute();

for (int numOfRowsUpdated : batchResult) {
if (numOfRowsUpdated == 0) {
throw new ConflictException(
"Transaction could not be completed due to a possible conflict");
}
}
});
Copy link

Choose a reason for hiding this comment

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

Could you test the case where one key failing causes the entire transaction to fail? (i.e., the successful key is not updated)

Copy link

@jkczyz jkczyz left a comment

Choose a reason for hiding this comment

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

Looks like some of the comments for the first commit were resolved in the second commit. You'll want to make sure they are resolved in the right commit so that they are self-contained. Otherwise, looks good and sorry about the delay.

PRIMARY KEY (store_id, key)
);

);
Copy link

Choose a reason for hiding this comment

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

Add newline.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I earlier got comment about removing newlines/blanks at end of files,
bit confused, let me know if i misunderstood.

Copy link

Choose a reason for hiding this comment

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

Sorry, please disregard. I thought that Github was showing the "No newline at end of file" symbol, but I guess I was mistaken.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Ok, i might have fixed it,
So just to clarify there "should" be a newline at EOF?

Addressed other comments as well.

Copy link

Choose a reason for hiding this comment

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

Correct, it should contain a newline, just not an empty line.

@G8XSU G8XSU merged commit 5de859d into lightningdevkit:main Apr 21, 2023
@G8XSU G8XSU mentioned this pull request May 10, 2023
31 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants