Skip to content

chore(core): add preferences to /settings #5621

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

Open
wants to merge 50 commits into
base: master
Choose a base branch
from

Conversation

glasstiger
Copy link
Contributor

@glasstiger glasstiger commented Apr 24, 2025

Functionality to save key-value pairs (currently referred to as preferences) on the server into a file via a POST or PUT request sent to the /settings endpoint.
The saved preferences returned back to the client in the response of a GET request sent to the /settings endpoint.

  • PUT: overwrite mode, current preferences gets replaced with the one sent in the request
  • POST: merge mode, current preferences gets merged with the one sent in the request
  • GET: returns settings which consists of 2 parts, config which comes from server.conf and preferences

Preferences are sent/received in json format, and versioned.
When preferences are updated, the version on which the changes are based has to be sent to the server.
If the base version is not matching with the current version, meaning another update was already processed, the update request is rejected.
The client has to query the latest version of preferences, and re-send any changes it wants to make.

The web console will use this to save instance name, description and colour as the first step, and the server will take these from preferences instead of env variables.
Later we can move reloadable configuration or other environment variables into preferences, and build a screen in the web console to manage them.


This PR also contains some refactorings:

  • HTTP request validation: now processors declare what kind of requests they can accept
  • HTTP processor lookup: there is a bit more abstraction now, instead of registering processors directly for each URI, we register request handlers now, and the request handlers provide the right processor for each request based on their HTTP header.

Example API calls:

  • query preferences:
/settings
GET
Response: {
  "config":{...},
  "preferences.version":0,
  "preferences":{}
}
  • overwrite current preferences:
/settings?version=0
PUT
{"key1":"value1","key2":"value2"}
Response: OK

/settings
GET
Response: {
  "config":{...},
  "preferences.version":1,
  "preferences":{"key1":"value1","key2":"value2"}
}
  • merge changes with current preferences:
/settings?version=1
POST
{"key3":"value3","key2":"value222"}
Response: OK

/settings
GET
Response: {
  "config":{...},
  "preferences.version":2,
  "preferences":{"key1":"value1","key2":"value222","key3":"value3"}
}

Required by:
https://github.com/questdb/questdb-enterprise/pull/614
questdb/ui#427


TODO:

  • UX, Web Console changes - @emrberk
  • test improvements: fragmentation test, error scenarios - @glasstiger
  • move hardcoded initial pool size into config - @glasstiger
  • return the saved config via the /settings endpoint - @glasstiger
  • add versioning, reject update if not based on the current version, web console can re-try - @glasstiger
  • accept a URL param called mode which can have two values: overwrite and merge. overwrite replaces the config with the one sent, merge is treated as an incremental update with dedup (new value wins) - @glasstiger
  • add support for non-multipart POST - @glasstiger
  • create json parser for the config, and save key-value pairs into the file instead of full json string - @glasstiger
  • concurrent tests - @glasstiger

@glasstiger glasstiger marked this pull request as ready for review May 6, 2025 19:36
@@ -729,7 +733,7 @@ default boolean isValidateSampleByFillType() {
*/
boolean mangleTableDirNames();

default void populateSettings(CharSequenceObjHashMap<CharSequence> settings) {
default void populateSettings(Utf8StringSink sink) {
Copy link
Member

Choose a reason for hiding this comment

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

could we please have an interface here, such as Utf8Sink rather than a specific class. Also "populateSettings" is ambiguous name:

  • lets have JavaDoc here
  • lets rename the method, using terminology that makes "direction" unambiguous (are we populating config from sink or sink from config?)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

renamed method in 1eb812a

private int state = STATE_START;

public PreferencesParser(CairoConfiguration configuration, CharSequenceObjHashMap<CharSequence> parserMap) {
super(16384, configuration.getPreferencesParserCacheSizeLimit(), configuration.getPreferencesStringPoolCapacity());
Copy link
Member

Choose a reason for hiding this comment

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

if 2 of 3 settings are taken from config, why the initial cache size is hardcoded?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

the thinking here was that instead of adding yet another config property (which will be barely used, if ever) we can just start with something sensible, and it will expand if it has to.


@Override
public void close() throws IOException {
sink.close();
Copy link
Member

Choose a reason for hiding this comment

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

we have QuietCloseable and IOException is usually not thrown

Copy link
Contributor Author

Choose a reason for hiding this comment

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

addressed in 7a56017

preferencesStore.save(transientState.sink, mode, version);
sendOk();
} catch (JsonException | CairoError e) {
LOG.error().$("error while saving config").$((Throwable) e).$();
Copy link
Member

Choose a reason for hiding this comment

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

nit: terminology + formatting "could not" or "cannot" - for consistency

Copy link
Contributor Author

Choose a reason for hiding this comment

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

addressed in 013292a

preferencesStore.save(transientState.sink, mode, version);
sendOk();
} catch (JsonException | CairoError e) {
LOG.error().$("error while saving config").$((Throwable) e).$();
Copy link
Member

Choose a reason for hiding this comment

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

erroneous behaviour is not tested, also missing input fragmentation test

Copy link
Contributor Author

Choose a reason for hiding this comment

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

added fragmentation test in 9e6798b

negative tests are in d6b14dd

@@ -59,6 +59,10 @@ default ObjList<String> getContextPathImport() {
return new ObjList<>("/imp");
}

default ObjList<String> getContextPathPreferences() {
return new ObjList<>("/preferences");
Copy link
Member

Choose a reason for hiding this comment

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

this is also quite funky to have "preferences" and "settings", those are synonyms. More standard approach would be to have single URL /settings. To save settings we would send POST /settings and to read GET /settings

Copy link
Contributor Author

Choose a reason for hiding this comment

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

/preferences has been removed.
/settings handles GET, POST and PUT.
PUT: overwrite mode, current preferences gets replaced with the one sent in the request
POST: merge mode, current preferences gets merged with the one sent in the request
GET: returns settings which consists of 2 parts, config which comes from server.conf and preferences

import io.questdb.std.str.AbstractCharSequence;
import io.questdb.std.str.DirectUtf8Sink;

public abstract class AbstractJsonParser implements JsonParser, Mutable, QuietCloseable {
Copy link
Member

Choose a reason for hiding this comment

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

why do we have abstract class? Preferences parser is the only descendant

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ent uses this class too

Copy link
Contributor Author

Choose a reason for hiding this comment

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

a comment has been added to this class that it should not be removed

@@ -964,8 +964,7 @@ private ExecutionModel parseCreateMatView(
}
}

final String matViewSql = Chars.toString(lexer.getContent(), startOfQuery, endOfQuery);
tableOpBuilder.setSelectText(matViewSql, startOfQuery);
tableOpBuilder.setSelectText(lexer.getContent().subSequence(startOfQuery, endOfQuery), startOfQuery);
Copy link
Member

Choose a reason for hiding this comment

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

this is a lot less explicit that toString and thus will be error prone. tableOp is a detachable object, it cannot depend on mutable objects. subSequence() returns potentially mutable interface

Copy link
Contributor Author

Choose a reason for hiding this comment

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

changed this back in a57460a

assertEquals("\"preferences\":{\"key1\":\"value1\",\"key2\":\"value2\",\"key3\":\"value3\"}", sink);
}
} catch (Throwable th) {
errors.put(threadIndex, th);
Copy link
Member

Choose a reason for hiding this comment

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

reader and writer threads will be overwriting each other errors

Copy link
Contributor Author

Choose a reason for hiding this comment

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

fixed in afd5fc4

}
}

public synchronized void populateSettings(Utf8StringSink sink) {
Copy link
Member

Choose a reason for hiding this comment

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

svae/populateSettings are not obviously symmetrical method names

Copy link
Contributor Author

Choose a reason for hiding this comment

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

they are not meant to be symmetrical.
for save() the symmetrical method is load().

@glasstiger glasstiger changed the title chore(core): config endpoint chore(core): add preferences to /settings May 14, 2025
@glasstiger glasstiger requested a review from bluestreak01 May 14, 2025 11:55
@glasstiger
Copy link
Contributor Author

[PR Coverage check]

😍 pass : 442 / 456 (96.93%)

file detail

path covered line new line coverage
🔵 io/questdb/cutlass/http/HttpRequestHandler.java 0 1 00.00%
🔵 io/questdb/cutlass/http/processors/TextImportProcessor.java 1 2 50.00%
🔵 io/questdb/cutlass/http/processors/StaticContentProcessor.java 1 2 50.00%
🔵 io/questdb/cutlass/http/processors/LineHttpProcessorImpl.java 4 6 66.67%
🔵 io/questdb/cutlass/http/processors/RejectProcessor.java 2 3 66.67%
🔵 io/questdb/cutlass/json/AbstractJsonParser.java 42 45 93.33%
🔵 io/questdb/cutlass/http/processors/SettingsProcessor.java 60 62 96.77%
🔵 io/questdb/preferences/PreferencesStore.java 72 74 97.30%
🔵 io/questdb/preferences/PreferencesMap.java 60 61 98.36%
🔵 io/questdb/PropServerConfiguration.java 18 18 100.00%
🔵 io/questdb/cutlass/http/HttpHeaderParser.java 10 10 100.00%
🔵 io/questdb/cutlass/http/HttpRequestProcessor.java 1 1 100.00%
🔵 io/questdb/ServerConfiguration.java 6 6 100.00%
🔵 io/questdb/PublicPassthroughConfiguration.java 1 1 100.00%
🔵 io/questdb/cutlass/http/processors/HealthCheckProcessor.java 2 2 100.00%
🔵 io/questdb/cutlass/http/HttpConnectionContext.java 29 29 100.00%
🔵 io/questdb/cutlass/http/HttpMultipartContentProcessor.java 1 1 100.00%
🔵 io/questdb/cutlass/http/processors/LineHttpPingProcessor.java 3 3 100.00%
🔵 io/questdb/preferences/PreferencesParser.java 18 18 100.00%
🔵 io/questdb/cutlass/http/processors/JsonQueryProcessor.java 1 1 100.00%
🔵 io/questdb/cutlass/http/HttpServer.java 21 21 100.00%
🔵 io/questdb/cairo/DefaultCairoConfiguration.java 2 2 100.00%
🔵 io/questdb/cairo/CairoEngine.java 4 4 100.00%
🔵 io/questdb/PropertyKey.java 1 1 100.00%
🔵 io/questdb/cutlass/http/HttpMultipartContentParser.java 9 9 100.00%
🔵 io/questdb/cutlass/http/processors/TableStatusCheckProcessor.java 1 1 100.00%
🔵 io/questdb/cairo/CairoConfiguration.java 1 1 100.00%
🔵 io/questdb/cutlass/Services.java 4 4 100.00%
🔵 io/questdb/cutlass/http/ChunkedContentParser.java 1 1 100.00%
🔵 io/questdb/cutlass/http/processors/PrometheusMetricsProcessor.java 1 1 100.00%
🔵 io/questdb/cairo/CairoConfigurationWrapper.java 3 3 100.00%
🔵 io/questdb/cutlass/http/processors/WarningsProcessor.java 1 1 100.00%
🔵 io/questdb/cutlass/http/HttpPostPutProcessor.java 1 1 100.00%
🔵 io/questdb/cutlass/http/HttpRequestValidator.java 38 38 100.00%
🔵 io/questdb/cutlass/http/processors/SettingsProcessorState.java 8 8 100.00%
🔵 io/questdb/cutlass/http/processors/TextQueryProcessor.java 2 2 100.00%
🔵 io/questdb/cairo/CairoException.java 12 12 100.00%

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants