While building pg_clickhouse, I developed a PostgreSQL setting (or "GUC") that takes a list of key/value pairs and sets them as session settings in every query sent to ClickHouse. The v0.1.0 release kept the list of settings as a string, and parsed it for every query. We wanted to reduce that overhead by parsing them into a key/value data structure when the GUC is set.
This post goes into the implementation weeds for this work, released in v0.1.1, covering the various attempts and ultimate solution. It will hopefully be helpful to other PostgreSQL extension developers.
Just want to try querying ClickHouse from PostgreSQL with pg_clickhouse and tune out all this C code and internals stuff? Check out the tutorial.
The Challenge #
I wanted to optimize away parsing the key/value pairs from the pg_clickhouse
pg_clickhouse.session_settings GUC for every query by pre-parsing it on
assignment and assigning it to a separate variable. It took a few tries, as
the GUC API requires quite specific memory allocation for extra data to work
properly. It took me a few tries to land on a workable and correct solution.
Here's the pg_clickhouse.session_settings setup:
DefineCustomStringVariable(
"pg_clickhouse.session_settings",
"Sets the default ClickHouse session settings.",
NULL,
&ch_session_settings,
"join_use_nulls 1, group_by_use_nulls 1, final 1",
PGC_USERSET,
0,
chfdw_check_settings_guc,
chfdw_settings_assign_hook,
NULL
);
So many arguments! Here's the public declaration:
extern void DefineCustomStringVariable(
const char *name,
const char *short_desc,
const char *long_desc,
char **valueAddr,
const char *bootValue,
GucContext context,
int flags,
GucStringCheckHook check_hook,
GucStringAssignHook assign_hook,
GucShowHook show_hook
) pg_attribute_nonnull(1, 4);
Quick explanations:
-
name: The name of the GUC, a used bySET. Extension GUCs should start with a prefix and a dot; hencepg_clickhouse.session_settings -
short_desc: A short description of the GUC. -
long_desc: A long description of the GUC. -
valueAddr: A pointer to the variable in which the GUC API stores the value. The pg_clickhouse GUC uses a globalchar *variable. -
bootValue: The default value for the GUC when the extension loads. -
context: Determines who can set the GUC and when. We want to allow any users to setpg_clickhouse.session_settings, but there are a number of other options. -
flags: A bitmask of GUC flags that modify behavior, determine formatting and parsing. -
check_hook: A callback function that validates a value and optionally setsextradata to be passed to the assign hook. Our code points tochfdw_check_settings_guc, which raises an error if a new value fails to parse as a valid key/value list. -
assign_hook: A callback function that assigns a GUC value to a variable other thanvalueAddr. May useextradata set by the check hook. -
show_hook: A callback that reformats a GUC for display. Used by some GUCs to normalize or canonicalize values, such as time zones.
The check_hook and assign_hook parameters allow the extra data parsing we
need. The idea is that the check hook can point extra at a data structure
and that value is passed to the assign hook, which assigns it to a variable.
First Attempt #
For my first attempt, I created a pointer to a list of key/value pairs,
assuming that I could point extra at it in the check hook and then assign it
in the assign hook. Something like:
static bool
chfdw_check_settings_guc(char **newval, void **extra, GucSource source)
{
if (*newval == NULL || *newval[0] == '\0')
return true;
kv_list * settings = parse_and_malloc_kv_list(*newval);
if (!settings)
return false;
*extra = settings;
return true;
}
Note that parse_and_malloc_kv_list() uses malloc() to allocate memory for
the list, each item in the list, and each key and value. It uses malloc()
because we're going to store the value in a global, so it must not be cleaned
up by the GUC memory context.
Then, in the assign hook:
static void
chfdw_settings_assign_hook(const char *newval, void *extra)
{
if (ch_session_settings_list)
kv_list_free(ch_session_settings_list);
ch_session_settings_list = (kv_list *) extra;
}
Free the existing settings list, if it's set, then assign the new list from
extra.
Seems pretty simple, right? Alas, I could never get the assignment to work; I need to better understand memory allocation, pointers, pointers to pointers, etc.
Second Attempt #
So I tried just doing the assignment in the check hook and doing away with the assign hook. Something like:
static bool
chfdw_check_settings_guc(char **newval, void **extra, GucSource source)
{
if (*newval == NULL || *newval[0] == '\0')
return true;
kv_list * pairs = parse_and_malloc_kv_list(*newval);
if (!pairs)
return false;
if (ch_session_settings_list)
kv_list_free(ch_session_settings_list);
ch_session_settings_list = pairs;
return true;
}
I asked about this approach on the Postgres Discord, where Tom Lane kindly responded with:
You absolutely must not change any session state in a check hook. That's called when we're merely speculating about whether (eg) a proposed ALTER DATABASE SET command is okay; there is no guarantee that we are about to apply the setting.
I also noticed that RESET wouldn't work, as the check hook isn't called for
a reset. So much for that idea!
Third Attempt #
Next I switched to a double parse in order to avoid these issues. The check hook simplified to:
static bool
chfdw_check_settings_guc(char **newval, void **extra, GucSource source)
{
if (*newval == NULL || *newval[0] == '\0')
return true;
kv_list * settings = parse_and_malloc_kv_list(*newval);
if (!settings)
return false
kv_list_free(ch_session_settings_list);
/* No errors, return true. */
return true;
}
And the assign hook became:
static void
chfdw_settings_assign_hook(const char *newval, void *extra)
{
if (ch_session_settings_list)
kv_list_free(ch_session_settings_list);
PG_TRY();
{
ch_session_settings_list = parse_and_malloc_kv_list(newval);
}
PG_CATCH();
{
ereport(LOG,
(errcode(ERRCODE_FDW_ERROR),
errmsg("unexpected error parsing \"%s\"", newval)));
}
PG_END_TRY();
}
Now both hooks execute parse_and_malloc_kv_list(). This double parsing
seemed acceptable, given that it replaces parsing the settings for every
query. This solution became pg_clickhouse#95. I was, however, dissuaded from
applying it because its memory management is more complicated than the
eventual solution, and also by this offhand Discord comment, again from Tom
Lane:
I do not recommend doing new allocations inside the assign hook. It is expected to be incapable of failing.
Which makes me think that the risk of actually raising an error that escapes
PG_TRY(), while low, renders this approach suboptimal, to say the least.
Interlude: Proper use of Extra #
Back in my first attempt, parse_and_malloc_kv_list() would
malloc() memory for each bit of the structure it built: the kv_list, a
pointer to a list of key/value pairs, each of those pairs, plus the key and
value strings themselves. The pattern still holds in pg_clickhouse#95's
kv_list.c. But it made use of extra impossible --- or at least it
surpassed my C expertise. Tom educated me about how to properly use extra:
You're supposed to use guc_malloc, and the extra data has to be a single malloc block, not a list.
The general principle with GUC extra data is that once your check_hook hands it back, it's guc.c's responsibility to free it when no longer needed. The single-chunk restriction is because guc.c wouldn't know about other components of the data structure.
This approach allows correct use of the extra data, does no other assignment
in the check hook, and does nothing but assign in the assign hook, eliminating
the error risk. All I had to do was figure out how to do a single
guc_malloc() with a list of key/value pairs.
Fourth Attempt #
The solution submitted in pg_clickhouse#94 follows the example of
ConvertTimeZoneAbbrevs() in PostgreSQL's datetime.c to calculate the total
memory required for all the key/value pairs and allocate that memory as a
single block. The struct's definition:
typedef struct kv_list
{
int length;
char data[];
} kv_list;
Note the use of a flexible array, data[]. It serves as a pointer to the
keys and values, which the constructor simply writes from its address as a
list of null-terminated strings. It is not actually a C structure at all. How
is the memory allocated? Once the constructor sums up the memory required for
all the keys and values like so:
kv_list * list = guc_malloc(ERROR, offsetof(kv_list, data) + summed_size);
It requires the size of the kv_list struct up to the data field, and adds
the total it summed for the key and value strings. Once the kv_list has been
allocated, the code iterates over the key/value pairs again and simply appends
them all, one after another, to the point starting at the data field.`
Now, iterating over these pairs is tricky, so the resulting kv_list API also provides an iterator structure to simplify it.
for (kv_iter iter = new_kv_iter(settings); !kv_iter_done(&iter); kv_iter_next(&iter))
{
printf("%s => %s\n", iter.name, iter.value);
}
This keeps things simple for the places in the code that need to process the setting, namely setting them up for binary and http queries.
With all that in place and a single guc_malloc()ed block of memory,
pg_clickhouse#94 finally uses the check and assign hooks properly:
static bool
chfdw_check_settings_guc(char **newval, void **extra, GucSource source)
{
if (*newval == NULL || *newval[0] == '\0')
return true;
kv_list * settings = parse_and_guc_malloc_kv_list(*newval);
if (!settings)
return false;
*extra = settings;
return true;
}
This is almost identical to the First Attempt, except that
parse_and_guc_malloc_kv_list() uses guc_malloc() to allocates settings
as a single block of memory, later assigned to extra. The assign hook then
becomes error-prone-free:
static void
chfdw_settings_assign_hook(const char *newval, void *extra)
{
ch_session_settings_list = (kv_list *) extra;
}
It no longer frees the previous values of ch_session_settings_list; instead
the GUC API cleans it up at the appropriate time.
More to Learn #
This solution satisfies me, in that it minimizes the memory required for the
settings values, leaves memory management to the GUC API, provides a simple
interface for iterating over settings, and correctly uses the check and assign
hooks. Plus I learned a ton about that API and how to coerce C memory to bend
away from strict struct-based allocation.
Although I've also kept a local copy of pg_clickhouse#95 to return to as I
learn more about pointers, pointers to pointers (and pointers to pointers to
pointers?) in C. I would think there could be a way to point extra and a
point in memory such that the GUC API just removes the pointer, not the memory
it points to. That would allow the data to be malloc()ed in the check hook
and assigned in the assigned hook, something like this in the check hook:
kv_list * settings = parse_and_malloc_kv_list(*newval);
if (!settings)
return false;
/* Allocate just the memory needed to point to a kv_list. */
extra = guc_malloc(ERROR, sizeof(kv_list *));
extra = &settings;
return true;
Then the assign hooks would be something like:
ch_session_settings_list = (kv_list *) *extra;
But I haven't figured out the proper incantation yet; as I said. Not that I
would ever merge a solution along these lines; it would again prevent RESET
from working, since GUC assumes it actually has the complete extra value. But
it challenges me, creates the opportunity to learn more and better C
techniques. And maybe, someday, how to abuse them more effectively. 😈



