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

Shared memory hash table #1379

Open
wants to merge 15 commits into
base: develop
Choose a base branch
from

Conversation

levkk
Copy link
Contributor

@levkk levkk commented Nov 7, 2023

Shared memory hash table. Implementation based (pretty much copied from) on pg_stat_statements. Subject to the same shared memory limitations as the rest of shared memory data structures, but still cool to have a native Postgres HashMap implementation in shared memory.

@levkk
Copy link
Contributor Author

levkk commented Nov 8, 2023

* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=1, address=0x9)
    frame #0: 0x00000001a00f7b38 libsystem_platform.dylib`_platform_strcmp + 8
libsystem_platform.dylib`_platform_strcmp:
->  0x1a00f7b38 <+8>:  ldrb   w4, [x0], #0x1
    0x1a00f7b3c <+12>: ldrb   w5, [x1], #0x1
    0x1a00f7b40 <+16>: subs   x3, x4, x5
    0x1a00f7b44 <+20>: ccmp   w4, #0x0, #0x4, eq
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=1, address=0x9)
  * frame #0: 0x00000001a00f7b38 libsystem_platform.dylib`_platform_strcmp + 8
    frame #1: 0x000000019ffcd310 libsystem_c.dylib`hash_search + 116
    frame #2: 0x000000010191dfbc shmem.so`{closure#0} at pg13.rs:27674:1
    frame #3: 0x00000001019079f0 shmem.so`hash_search [inlined] pg_guard_ffi_boundary_impl<*mut core::ffi::c_void, pgrx_pg_sys::include::pg13::hash_search::{closure_env#0}> at ffi.rs:120:26
    frame #4: 0x0000000101907930 shmem.so`hash_search [inlined] pg_guard_ffi_boundary<*mut core::ffi::c_void, pgrx_pg_sys::include::pg13::hash_search::{closure_env#0}> at ffi.rs:94:14
    frame #5: 0x0000000101907930 shmem.so`hash_search at pg13.rs:27674:1
    frame #6: 0x00000001018ef51c shmem.so`insert at shmem_hash.rs:59:13
    frame #7: 0x00000001018a3d4c shmem.so`hash_table_insert at lib.rs:68:5
    frame #8: 0x00000001018a3dd4 shmem.so`hash_table_insert_wrapper_inner at lib.rs:67:1
    frame #9: 0x00000001018cc810 shmem.so`{closure#0} at lib.rs:67:1
    frame #10: 0x00000001018d6e0c shmem.so`do_call<shmem::hash_table_insert_wrapper::{closure_env#0}, ()> at panicking.rs:500:40
    frame #11: 0x00000001018d7b14 shmem.so`__rust_try + 32
    frame #12: 0x00000001018d6ab8 shmem.so`try<(), shmem::hash_table_insert_wrapper::{closure_env#0}> at panicking.rs:464:19
    frame #13: 0x00000001018aa644 shmem.so`catch_unwind<shmem::hash_table_insert_wrapper::{closure_env#0}, ()> at panic.rs:142:14
    frame #14: 0x00000001018bcdd0 shmem.so`run_guarded<shmem::hash_table_insert_wrapper::{closure_env#0}, ()> at panic.rs:408:11
    frame #15: 0x00000001018bff10 shmem.so`pgrx_extern_c_guard<shmem::hash_table_insert_wrapper::{closure_env#0}, ()> at panic.rs:385:11
    frame #16: 0x00000001018a3d84 shmem.so`hash_table_insert_wrapper at lib.rs:67:1
    frame #17: 0x0000000100d174b4 postgres`ExecInterpExpr(state=0x0000000145018970, econtext=<unavailable>, isnull=<unavailable>) at execExprInterp.c:704:8 [opt]
    frame #18: 0x0000000100d4a7d8 postgres`ExecResult [inlined] ExecEvalExprSwitchContext(state=0x0000000145018970, econtext=0x0000000145018670, isNull=0x000000016f275e4f) at executor.h:322:13 [opt]
    frame #19: 0x0000000100d4a7b0 postgres`ExecResult [inlined] ExecProject(projInfo=0x0000000145018968) at executor.h:356:9 [opt]
    frame #20: 0x0000000100d4a794 postgres`ExecResult(pstate=<unavailable>) at nodeResult.c:136:10 [opt]
    frame #21: 0x0000000100d1e3d0 postgres`standard_ExecutorRun [inlined] ExecProcNode(node=0x0000000145018558) at executor.h:248:9 [opt]
    frame #22: 0x0000000100d1e3b4 postgres`standard_ExecutorRun [inlined] ExecutePlan(estate=0x0000000145018320, planstate=0x0000000145018558, use_parallel_mode=<unavailable>, operation=CMD_SELECT, sendTuples=<unavailable>, numberTuples=0, direction=<unavailable>, dest=0x0000000135009158, execute_once=<unavailable>) at execMain.c:1632:10 [opt]
    frame #23: 0x0000000100d1e388 postgres`standard_ExecutorRun(queryDesc=0x000000014500eb20, direction=<unavailable>, count=0, execute_once=<unavailable>) at execMain.c:350:3 [opt]
    frame #24: 0x0000000100d1e2c0 postgres`ExecutorRun(queryDesc=<unavailable>, direction=<unavailable>, count=<unavailable>, execute_once=<unavailable>) at execMain.c:294:3 [opt] [artificial]
    frame #25: 0x0000000100e9847c postgres`PortalRunSelect(portal=0x000000014682d920, forward=<unavailable>, count=0, dest=0x0000000135009158) at pquery.c:921:4 [opt]
    frame #26: 0x0000000100e980b4 postgres`PortalRun(portal=0x000000014682d920, count=9223372036854775807, isTopLevel=<unavailable>, run_once=<unavailable>, dest=0x0000000135009158, altdest=0x0000000135009158, qc=0x000000016f2760e8) at pquery.c:765:18 [opt]
    frame #27: 0x0000000100e972e4 postgres`exec_simple_query(query_string="select hash_table_insert(10, 9);") at postgres.c:1238:10 [opt]
    frame #28: 0x0000000100e94808 postgres`PostgresMain(argc=<unavailable>, argv=<unavailable>, dbname=<unavailable>, username=<unavailable>) at postgres.c:0 [opt]
    frame #29: 0x0000000100e11500 postgres`BackendRun(port=0x0000000144f047e0) at postmaster.c:4557:2 [opt]
    frame #30: 0x0000000100e10c20 postgres`ServerLoop [inlined] BackendStartup(port=0x0000000144f047e0) at postmaster.c:4241:3 [opt]
    frame #31: 0x0000000100e10bfc postgres`ServerLoop at postmaster.c:1739:7 [opt]
    frame #32: 0x0000000100e0dff8 postgres`PostmasterMain(argc=<unavailable>, argv=<unavailable>) at postmaster.c:1412:11 [opt]
    frame #33: 0x0000000100d69b34 postgres`main(argc=8, argv=0x0000600001b30dc0) at main.c:210:3 [opt]
    frame #34: 0x000000019fd73f28 dyld`start + 2236
(lldb)

@levkk
Copy link
Contributor Author

levkk commented Nov 8, 2023

@thomcc Hi Thom, I'm getting a segfault here and the backtrace (comment above) is showing that the linker picked up the wrong hash_search function. Instead of using the one in postgres, it's using the one in Mac OS's libc. Any ideas?

To repro:

  1. cargo pgrx run the shmem example
  2. SELECT pg_backend_pid()
  3. sudo lldb -p <backend pid>
  4. SELECT hash_table_insert(10, 9) twice. The first time, it'll be ok, the second you should get a segfault. Basically, calling the wrong hash_search function twice results in bad memory access.

Thank you!

@levkk levkk changed the title [WIP] Shared memory hash table Shared memory hash table Nov 8, 2023
@levkk levkk marked this pull request as ready for review November 9, 2023 00:03
pgrx/src/shmem_hash.rs Outdated Show resolved Hide resolved
@levkk levkk marked this pull request as draft November 9, 2023 17:13
@workingjubilee
Copy link
Member

  Error: bindgen failed for pg16

  Caused by:
     0: Unable to generate bindings for pg16
     1: clang diagnosed error: /usr/lib/llvm-14/lib/clang/14.0.0/include/emmintrin.h:2228:10: error: invalid conversion between vector type '__m128i' (vector of 2 'long long' values) and integer type 'int' of different size
        /usr/lib/llvm-14/lib/clang/14.0.0/include/emmintrin.h:2250:10: error: invalid conversion between vector type '__m128i' (vector of 2 'long long' values) and integer type 'int' of different size
        /usr/lib/llvm-14/lib/clang/14.0.0/include/emmintrin.h:2271:10: error: invalid conversion between vector type '__m128i' (vector of 2 'long long' values) and integer type 'int' of different size
        /usr/lib/llvm-14/lib/clang/14.0.0/include/emmintrin.h:2292:10: error: invalid conversion between vector type '__m128i' (vector of 2 'long long' values) and integer type 'int' of different size
        /usr/lib/llvm-14/lib/clang/14.0.0/include/emmintrin.h:2670:10: error: invalid conversion between vector type '__m128i' (vector of 2 'long long' values) and integer type 'int' of different size
        /usr/lib/llvm-14/lib/clang/14.0.0/include/emmintrin.h:2691:10: error: invalid conversion between vector type '__m128i' (vector of 2 'long long' values) and integer type 'int' of different size
        /usr/lib/llvm-14/lib/clang/14.0.0/include/emmintrin.h:2711:10: error: invalid conversion between vector type '__m128i' (vector of 2 'long long' values) and integer type 'int' of different size
        /usr/lib/llvm-14/lib/clang/14.0.0/include/emmintrin.h:2731:10: error: invalid conversion between vector type '__m128i' (vector of 2 'long long' values) and integer type 'int' of different size


  Location:
      pgrx-pg-sys/build.rs:733:10

not this shit again.

@levkk
Copy link
Contributor Author

levkk commented Nov 9, 2023

Yeah I also lied about this being infinitely large...still hitting out of memory when allocating more than 512kb or so. Gonna keep debugging to find out why. Probably some shared memory limit constant somewhere?

@workingjubilee
Copy link
Member

we got #1351 over that, it's weird.

@workingjubilee
Copy link
Member

Replace PgLwLock with PgSpinLock. However, PgSpinLock constructor isn't const, not sure why. I don't see it used anywhere, so might not be ready yet?

Please feel free to make PgSpinLock::new a const fn.

@levkk
Copy link
Contributor Author

levkk commented Nov 9, 2023

Replace PgLwLock with PgSpinLock. However, PgSpinLock constructor isn't const, not sure why. I don't see it used anywhere, so might not be ready yet?

Please feel free to make PgSpinLock::new a const fn.

Will do!

@levkk
Copy link
Contributor Author

levkk commented Nov 17, 2023

Yes, I should finish this...

@workingjubilee
Copy link
Member

I just kicked all the open PRs for CI reasons, don't worry~

@levkk levkk marked this pull request as ready for review November 18, 2023 18:10
@levkk
Copy link
Contributor Author

levkk commented Nov 27, 2023

Looks like cargo fmt in develop needs to be run.

@workingjubilee
Copy link
Member

Whoops.
But also

111 | pub use shmem_hash::*;

@levkk
Copy link
Contributor Author

levkk commented Nov 27, 2023

Ah forgot a cfg. One moment.

@levkk
Copy link
Contributor Author

levkk commented Nov 27, 2023

Look at that build queue! $$$

@levkk
Copy link
Contributor Author

levkk commented Nov 27, 2023

Now I feel bad! Maybe we could have just one job sniff testing basics before launching the whole test suite?


#[derive(Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum Error {
Copy link
Member

Choose a reason for hiding this comment

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

A public error type shall be given a name that does not collide with the name of the Error trait.

/// A shared memory HashMap using Postgres' `HTAB`.
/// This HashMap is used for `pg_stat_statements` and Postgres
/// internals to store key/value pairs in shared memory.
pub struct PgHashMap<K: Copy + Clone, V: Copy + Clone> {
Copy link
Member

Choose a reason for hiding this comment

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

The type for new APIs shall not be prefixed with "Pg". It's Postgres, everything's Pg everywhere, all these types are namespaced via pgrx::, and this style is inconsistently used at best (Array vs. PgHeapTuple, for instance).

Copy link
Member

Choose a reason for hiding this comment

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

ShmemHashMap would be fine.

impl<K: Copy + Clone, V: Copy + Clone> PgHashMap<K, V> {
/// Create new `PgHashMap`. This still needs to be allocated with
/// `pg_shmem_init!` just like any other shared memory structure.
pub const fn new(size: u64) -> PgHashMap<K, V> {
Copy link
Member

Choose a reason for hiding this comment

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

It's common for ::new() to be semantically equivalent to ::default(), so the size parameter's purpose should be documented rather than left implicit, if we require it.

Copy link
Member

Choose a reason for hiding this comment

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

It should also be made clear why it's a u64 instead of the more common usize. Yes, the difference "doesn't matter", but the difference between those two types informs what purpose it serves.

@@ -28,6 +28,7 @@ use std::{cell::UnsafeCell, marker::PhantomData};
/// [`storage/spin.h`]:
/// https://github.com/postgres/postgres/blob/1f0c4fa255253d223447c2383ad2b384a6f05854/src/include/storage/spin.h
#[doc(alias = "slock_t")]
#[derive(Debug)]
Copy link
Member

Choose a reason for hiding this comment

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

This will just say

PgSpinLock { item: UnsafeCell { .. }, lock: UnsafeCell { .. } }

Please impl Debug explicitly with something more concise.

@workingjubilee
Copy link
Member

Now I feel bad! Maybe we could have just one job sniff testing basics before launching the whole test suite?

I certainly would not reject PRs improving our test suite's economy of CPU cycles!

Copy link
Member

@workingjubilee workingjubilee left a comment

Choose a reason for hiding this comment

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

Thank you. I realize reviewing twice on entirely different parts of the PR like this may be slightly unusual(?), and I appreciate you bearing with me here. My first pass often is about smaller nits because once they are addressed, it's easier to focus on the finer details.

Comment on lines +91 to +94
/// * `size` - Maximum number of elements in the HashMap. This is allocated
/// at server start and cannot be changed. `i64` is the expected type
/// for `pg_sys::ShmemInitHash`, so we don't attempt runtime conversions
/// unnecessarily.
Copy link
Member

Choose a reason for hiding this comment

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

...okay, "you only can allocate this once ever" is pretty coherent as to why we need it, yeah. Though I believe a runtime conversion that happens once ever is... fine?

Comment on lines 214 to 215
/// Get the number of elements in the HashMap.
pub fn len(&self) -> u64 {
pub fn len(&self) -> i64 {
Copy link
Member

Choose a reason for hiding this comment

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

This reminds me, this needs capacity(&self) as well.

/// Remove the value from the `ShmemHashMap` and return it.
/// If the key doesn't exist, return None.
pub fn remove(&self, key: K) -> Option<V> {
if let Some(value) = self.get(key) {
Copy link
Member

Choose a reason for hiding this comment

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

This should probably use let-else, like so:

Suggested change
if let Some(value) = self.get(key) {
let Some(value) = self.get(key) else { return None };

} else {
let value_ptr = value_ptr!(entry);
let value = unsafe { std::ptr::read(value_ptr) };
return Some(value.value);
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
return Some(value.value);
Some(value.value)

};

if entry.is_null() {
return None;
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
return None;
None

Comment on lines +208 to +211
return Some(value);
} else {
return None;
}
Copy link
Member

Choose a reason for hiding this comment

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

But if you do use let-else this should probably be updated.

Copy link
Member

Choose a reason for hiding this comment

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

Something like this, but then it will want you to hit it with rustfmt.

Suggested change
return Some(value);
} else {
return None;
}
Some(value)

Comment on lines +183 to +185
let value_ptr = value_ptr!(entry);
let value = unsafe { std::ptr::read(value_ptr) };
return Some(value.value);
Copy link
Member

Choose a reason for hiding this comment

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

Is there a reason this returns V and not &V? Because we cannot rely on concurrent changes not altering the data in the K/V pair? It would be good to understand the soundness conditions better here and why things look the way they do.

/// A shared memory HashMap using Postgres' `HTAB`.
/// This HashMap is used for `pg_stat_statements` and Postgres
/// internals to store key/value pairs in shared memory.
pub struct ShmemHashMap<K: Copy + Clone, V: Copy + Clone> {
Copy link
Member

Choose a reason for hiding this comment

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

Please weaken the bounds for the struct and instead keep them on the implementation. It should be made clear why certain functions require the bounds they do in order to be sound, instead of plastering the bounds across the entire API. Aside from leaking implementation details, it makes it unclear where the soundness requirements actually start, which means later refactoring that attempts to weaken the bounds can be made unsound.

Copy link
Member

Choose a reason for hiding this comment

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

Note that it's fine if the bounds are overeagerly applied in cases where it's not clear, but e.g. I have done a lot of this refactoring on Array<'_, T> and List<T> recently, and in general I tried to pull the scope tighter.

Comment on lines +238 to +239
let htab_ptr = unsafe {
pg_sys::ShmemInitHash(
Copy link
Member

Choose a reason for hiding this comment

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

Preconditions, postconditions, "why is this sound?", etc.

Comment on lines +52 to +53
/// HTAB protected by a SpinLock.
htab: OnceCell<PgSpinLock<ShmemHashMapInner>>,
Copy link
Member

Choose a reason for hiding this comment

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

Will the use of OnceCell remain correct in the event of this being fixed?

Copy link
Member

Choose a reason for hiding this comment

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

It's fine if the answer is "no", I just want to check to make sure if this should get a note relating it back to that issue.

@@ -11,22 +11,35 @@ use pgrx::prelude::*;
use pgrx::{pg_shmem_init, PgAtomic, PgLwLock, PgSharedMemoryInitialization};
use std::sync::atomic::AtomicBool;

#[cfg(feature = "cshim")]
use pgrx::PgHashMap;
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
use pgrx::PgHashMap;
use pgrx::ShmemHashMap;

static ATOMIC: PgAtomic<AtomicBool> = PgAtomic::new();
static LWLOCK: PgLwLock<bool> = PgLwLock::new();

#[cfg(feature = "cshim")]
static HASH_MAP: PgHashMap<i64, i64> = PgHashMap::new(500);
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
static HASH_MAP: PgHashMap<i64, i64> = PgHashMap::new(500);
static HASH_MAP: ShmemHashMap<i64, i64> = ShmemHashMap::new(500);

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.

3 participants