Merge pull request #133 from jetzig-framework/jetkv-valkey

Valkey backend for JetKV
This commit is contained in:
bobf 2024-12-06 21:36:26 +00:00 committed by GitHub
commit 475ed26952
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 310 additions and 187 deletions

View File

@ -7,12 +7,12 @@
.hash = "1220d0e8734628fd910a73146e804d10a3269e3e7d065de6bb0e3e88d5ba234eb163", .hash = "1220d0e8734628fd910a73146e804d10a3269e3e7d065de6bb0e3e88d5ba234eb163",
}, },
.zmpl = .{ .zmpl = .{
.url = "https://github.com/jetzig-framework/zmpl/archive/ef1930b08e1f174ddb02a3a0a01b35aa8a4af235.tar.gz", .url = "https://github.com/jetzig-framework/zmpl/archive/7b5e0309ee49c06b99c242fecd218d3f3d15cd40.tar.gz",
.hash = "1220a7bacb828f12cd013b0906da61a17fac6819ab8cee81e00d9ae1aa0faa992720", .hash = "12204d61eb58ee860f748e5817ef9300ad56c9d5efef84864ae590c87baf2e0380a1",
}, },
.jetkv = .{ .jetkv = .{
.url = "https://github.com/jetzig-framework/jetkv/archive/2b1130a48979ea2871c8cf6ca89c38b1e7062839.tar.gz", .url = "https://github.com/jetzig-framework/jetkv/archive/acaa30db281f1c331d20c48cfe6539186549ad45.tar.gz",
.hash = "12201d75d73aad5e1c996de4d5ae87a00e58479c8d469bc2eeb5fdeeac8857bc09af", .hash = "1220b260b20cb65d801a00a39dc6506387f5faa1a225f85160e011bd2aabd2ce6e0b",
}, },
.jetquery = .{ .jetquery = .{
.url = "https://github.com/jetzig-framework/jetquery/archive/52e1cf900c94f3c103727ade6ba2dab3057c8663.tar.gz", .url = "https://github.com/jetzig-framework/jetquery/archive/52e1cf900c94f3c103727ade6ba2dab3057c8663.tar.gz",

View File

@ -94,43 +94,54 @@ pub const jetzig_options = struct {
}, },
}; };
/// Key-value store options. Set backend to `.file` to use a file-based store. /// Key-value store options.
/// When using `.file` backend, you must also set `.file_options`. /// Available backends:
/// The key-value store is exposed as `request.store` in views and is also available in as /// * memory: Simple, in-memory hashmap-backed store.
/// `env.store` in all jobs/mailers. /// * file: Rudimentary file-backed store.
pub const store: jetzig.kv.Store.KVOptions = .{ /// * valkey: Valkey-backed store with connection pool.
///
/// When using `.file` or `.valkey` backend, you must also set `.file_options` or
/// `.valkey_options` respectively.
///
/// ## File backend:
// .backend = .file,
// .file_options = .{
// .path = "/path/to/jetkv-store.db",
// .truncate = false, // Set to `true` to clear the store on each server launch.
// .address_space_size = jetzig.jetkv.JetKV.FileBackend.addressSpace(4096),
// },
//
// ## Valkey backend
// .backend = .valkey,
// .valkey_options = .{
// .host = "localhost",
// .port = 6379,
// .timeout = 1000, // in milliseconds, i.e. 1 second.
// .connect = .lazy, // Connect on first use, or `auto` to connect on server startup.
// .buffer_size = 8192,
// .pool_size = 8,
// },
/// Available configuration options for `store`, `job_queue`, and `cache` are identical.
///
/// For production deployment, the `valkey` backend is recommended for all use cases.
///
/// The general-purpose key-value store is exposed as `request.store` in views and is also
/// available in as `env.store` in all jobs/mailers.
pub const store: jetzig.kv.Store.Options = .{
.backend = .memory, .backend = .memory,
// .backend = .file,
// .file_options = .{
// .path = "/path/to/jetkv-store.db",
// .truncate = false, // Set to `true` to clear the store on each server launch.
// .address_space_size = jetzig.jetkv.JetKV.FileBackend.addressSpace(4096),
// },
}; };
/// Job queue options. Identical to `store` options, but allows using different /// Job queue options. Identical to `store` options, but allows using different
/// backends (e.g. `.memory` for key-value store, `.file` for jobs queue. /// backends (e.g. `.memory` for key-value store, `.file` for jobs queue.
/// The job queue is managed internally by Jetzig. /// The job queue is managed internally by Jetzig.
pub const job_queue: jetzig.kv.Store.KVOptions = .{ pub const job_queue: jetzig.kv.Store.Options = .{
.backend = .memory, .backend = .memory,
// .backend = .file,
// .file_options = .{
// .path = "/path/to/jetkv-queue.db",
// .truncate = false, // Set to `true` to clear the store on each server launch.
// .address_space_size = jetzig.jetkv.JetKV.FileBackend.addressSpace(4096),
// },
}; };
/// Cache options. Identical to `store` options, but allows using different /// Cache options. Identical to `store` options, but allows using different
/// backends (e.g. `.memory` for key-value store, `.file` for cache. /// backends (e.g. `.memory` for key-value store, `.file` for cache.
pub const cache: jetzig.kv.Store.KVOptions = .{ pub const cache: jetzig.kv.Store.Options = .{
.backend = .memory, .backend = .memory,
// .backend = .file,
// .file_options = .{
// .path = "/path/to/jetkv-cache.db",
// .truncate = false, // Set to `true` to clear the store on each server launch.
// .address_space_size = jetzig.jetkv.JetKV.FileBackend.addressSpace(4096),
// },
}; };
/// SMTP configuration for Jetzig Mail. It is recommended to use a local SMTP relay, /// SMTP configuration for Jetzig Mail. It is recommended to use a local SMTP relay,

View File

@ -48,22 +48,13 @@ pub fn start(self: *const App, routes_module: type, options: AppOptions) !void {
self.allocator.free(custom_route.template); self.allocator.free(custom_route.template);
}; };
var store = try jetzig.kv.Store.init( var store = try jetzig.kv.Store.GeneralStore.init(self.allocator, self.env.logger, .general);
self.allocator,
jetzig.config.get(jetzig.kv.Store.KVOptions, "store"),
);
defer store.deinit(); defer store.deinit();
var job_queue = try jetzig.kv.Store.init( var job_queue = try jetzig.kv.Store.JobQueueStore.init(self.allocator, self.env.logger, .jobs);
self.allocator,
jetzig.config.get(jetzig.kv.Store.KVOptions, "job_queue"),
);
defer job_queue.deinit(); defer job_queue.deinit();
var cache = try jetzig.kv.Store.init( var cache = try jetzig.kv.Store.CacheStore.init(self.allocator, self.env.logger, .cache);
self.allocator,
jetzig.config.get(jetzig.kv.Store.KVOptions, "cache"),
);
defer cache.deinit(); defer cache.deinit();
var repo = try jetzig.database.repo(self.allocator, self); var repo = try jetzig.database.repo(self.allocator, self);

View File

@ -107,38 +107,38 @@ pub const Schema: type = struct {};
/// When using `.file` backend, you must also set `.file_options`. /// When using `.file` backend, you must also set `.file_options`.
/// The key-value store is exposed as `request.store` in views and is also available in as /// The key-value store is exposed as `request.store` in views and is also available in as
/// `env.store` in all jobs/mailers. /// `env.store` in all jobs/mailers.
pub const store: kv.Store.KVOptions = .{ pub const store: kv.Store.Options = .{
.backend = .memory, .backend = .memory,
// .backend = .file, // .backend = .file,
// .file_options = .{ // .file_options = .{
// .path = "/path/to/jetkv-store.db", // .path = "/path/to/jetkv-store.db",
// .truncate = false, // Set to `true` to clear the store on each server launch. // .truncate = false, // Set to `true` to clear the store on each server launch.
// .address_space_size = jetzig.jetkv.JetKV.FileBackend.addressSpace(4096), // .address_space_size = jetzig.jetkv.FileBackend.addressSpace(4096),
// }, // },
}; };
/// Job queue options. Identical to `store` options, but allows using different /// Job queue options. Identical to `store` options, but allows using different
/// backends (e.g. `.memory` for key-value store, `.file` for jobs queue. /// backends (e.g. `.memory` for key-value store, `.file` for jobs queue.
/// The job queue is managed internally by Jetzig. /// The job queue is managed internally by Jetzig.
pub const job_queue: kv.Store.KVOptions = .{ pub const job_queue: kv.Store.Options = .{
.backend = .memory, .backend = .memory,
// .backend = .file, // .backend = .file,
// .file_options = .{ // .file_options = .{
// .path = "/path/to/jetkv-queue.db", // .path = "/path/to/jetkv-queue.db",
// .truncate = false, // Set to `true` to clear the store on each server launch. // .truncate = false, // Set to `true` to clear the store on each server launch.
// .address_space_size = jetzig.jetkv.JetKV.FileBackend.addressSpace(4096), // .address_space_size = jetzig.jetkv.JetKV.addressSpace(4096),
// }, // },
}; };
/// Cache. Identical to `store` options, but allows using different /// Cache. Identical to `store` options, but allows using different
/// backends (e.g. `.memory` for key-value store, `.file` for cache. /// backends (e.g. `.memory` for key-value store, `.file` for cache.
pub const cache: kv.Store.KVOptions = .{ pub const cache: kv.Store.Options = .{
.backend = .memory, .backend = .memory,
// .backend = .file, // .backend = .file,
// .file_options = .{ // .file_options = .{
// .path = "/path/to/jetkv-cache.db", // .path = "/path/to/jetkv-cache.db",
// .truncate = false, // Set to `true` to clear the store on each server launch. // .truncate = false, // Set to `true` to clear the store on each server launch.
// .address_space_size = jetzig.jetkv.JetKV.FileBackend.addressSpace(4096), // .address_space_size = jetzig.jetkv.JetKV.addressSpace(4096),
// }, // },
}; };

View File

@ -61,7 +61,7 @@ pub fn parse(self: *Query) !void {
else => return error.JetzigQueryParseError, else => return error.JetzigQueryParseError,
} }
} else { } else {
var array = try jetzig.zmpl.Data.createArray(self.data.allocator()); var array = try jetzig.zmpl.Data.createArray(self.data.allocator);
try array.append(self.dataValue(item.value)); try array.append(self.dataValue(item.value));
try params.put(key, array); try params.put(key, array);
} }
@ -72,7 +72,7 @@ pub fn parse(self: *Query) !void {
else => return error.JetzigQueryParseError, else => return error.JetzigQueryParseError,
} }
} else { } else {
var object = try jetzig.zmpl.Data.createObject(self.data.allocator()); var object = try jetzig.zmpl.Data.createObject(self.data.allocator);
try object.put(mapping.field, self.dataValue(item.value)); try object.put(mapping.field, self.dataValue(item.value));
try params.put(mapping.key, object); try params.put(mapping.key, object);
} }
@ -109,10 +109,10 @@ fn mappingParam(input: []const u8) ?struct { key: []const u8, field: []const u8
fn dataValue(self: Query, value: ?[]const u8) *jetzig.data.Data.Value { fn dataValue(self: Query, value: ?[]const u8) *jetzig.data.Data.Value {
if (value) |item_value| { if (value) |item_value| {
const duped = self.data.allocator().dupe(u8, item_value) catch @panic("OOM"); const duped = self.data.allocator.dupe(u8, item_value) catch @panic("OOM");
return self.data.string(uriDecode(duped)); return self.data.string(uriDecode(duped));
} else { } else {
return jetzig.zmpl.Data._null(self.data.allocator()); return jetzig.zmpl.Data._null(self.data.allocator);
} }
} }

View File

@ -50,65 +50,75 @@ middleware_data: jetzig.http.middleware.MiddlewareData = undefined,
rendered_multiple: bool = false, rendered_multiple: bool = false,
rendered_view: ?jetzig.views.View = null, rendered_view: ?jetzig.views.View = null,
start_time: i128, start_time: i128,
store: RequestStore, store: RequestStore(jetzig.kv.Store.GeneralStore),
cache: RequestStore, cache: RequestStore(jetzig.kv.Store.CacheStore),
repo: *jetzig.database.Repo, repo: *jetzig.database.Repo,
global: *jetzig.Global, global: *jetzig.Global,
/// Wrapper for KV store that uses the request's arena allocator for fetching values. /// Wrapper for KV store that uses the request's arena allocator for fetching values.
pub const RequestStore = struct { pub fn RequestStore(T: type) type {
allocator: std.mem.Allocator, return struct {
store: *jetzig.kv.Store, allocator: std.mem.Allocator,
store: *T,
/// Put a String or into the key-value store. const Self = @This();
pub fn get(self: RequestStore, key: []const u8) !?*jetzig.data.Value {
return try self.store.get(try self.data(), key);
}
/// Get a String from the store. /// Get a Value from the store.
pub fn put(self: RequestStore, key: []const u8, value: anytype) !void { pub fn get(self: Self, key: []const u8) !?*jetzig.data.Value {
const alloc = (try self.data()).allocator(); return try self.store.get(try self.data(), key);
try self.store.put(key, try jetzig.Data.zmplValue(value, alloc)); }
}
/// Remove a String to from the key-value store and return it if found. /// Store a Value in the key-value store.
pub fn fetchRemove(self: RequestStore, key: []const u8) !?*jetzig.data.Value { pub fn put(self: Self, key: []const u8, value: anytype) !void {
return try self.store.fetchRemove(try self.data(), key); const alloc = (try self.data()).allocator;
} try self.store.put(key, try jetzig.Data.zmplValue(value, alloc));
}
/// Remove a String to from the key-value store. /// Store a Value in the key-value store with an expiration time in seconds.
pub fn remove(self: RequestStore, key: []const u8) !void { pub fn putExpire(self: Self, key: []const u8, value: anytype, expiration: i32) !void {
try self.store.remove(key); const alloc = (try self.data()).allocator;
} try self.store.putExpire(key, try jetzig.Data.zmplValue(value, alloc), expiration);
}
/// Append a Value to the end of an Array in the key-value store. /// Remove a String to from the key-value store and return it if found.
pub fn append(self: RequestStore, key: []const u8, value: anytype) !void { pub fn fetchRemove(self: Self, key: []const u8) !?*jetzig.data.Value {
const alloc = (try self.data()).allocator(); return try self.store.fetchRemove(try self.data(), key);
try self.store.append(key, try jetzig.Data.zmplValue(value, alloc)); }
}
/// Prepend a Value to the start of an Array in the key-value store. /// Remove a String to from the key-value store.
pub fn prepend(self: RequestStore, key: []const u8, value: anytype) !void { pub fn remove(self: Self, key: []const u8) !void {
const alloc = (try self.data()).allocator(); try self.store.remove(key);
try self.store.prepend(key, try jetzig.Data.zmplValue(value, alloc)); }
}
/// Pop a String from an Array in the key-value store. /// Append a Value to the end of an Array in the key-value store.
pub fn pop(self: RequestStore, key: []const u8) !?*jetzig.data.Value { pub fn append(self: Self, key: []const u8, value: anytype) !void {
return try self.store.pop(try self.data(), key); const alloc = (try self.data()).allocator;
} try self.store.append(key, try jetzig.Data.zmplValue(value, alloc));
}
/// Left-pop a String from an Array in the key-value store. /// Prepend a Value to the start of an Array in the key-value store.
pub fn popFirst(self: RequestStore, key: []const u8) !?*jetzig.data.Value { pub fn prepend(self: Self, key: []const u8, value: anytype) !void {
return try self.store.popFirst(try self.data(), key); const alloc = (try self.data()).allocator;
} try self.store.prepend(key, try jetzig.Data.zmplValue(value, alloc));
}
fn data(self: RequestStore) !*jetzig.data.Data { /// Pop a String from an Array in the key-value store.
const arena_data = try self.allocator.create(jetzig.data.Data); pub fn pop(self: Self, key: []const u8) !?*jetzig.data.Value {
arena_data.* = jetzig.data.Data.init(self.allocator); return try self.store.pop(try self.data(), key);
return arena_data; }
}
}; /// Left-pop a String from an Array in the key-value store.
pub fn popFirst(self: Self, key: []const u8) !?*jetzig.data.Value {
return try self.store.popFirst(try self.data(), key);
}
fn data(self: Self) !*jetzig.data.Data {
const arena_data = try self.allocator.create(jetzig.data.Data);
arena_data.* = jetzig.data.Data.init(self.allocator);
return arena_data;
}
};
}
pub fn init( pub fn init(
allocator: std.mem.Allocator, allocator: std.mem.Allocator,

View File

@ -15,9 +15,9 @@ job_definitions: []const jetzig.JobDefinition,
mailer_definitions: []const jetzig.MailerDefinition, mailer_definitions: []const jetzig.MailerDefinition,
mime_map: *jetzig.http.mime.MimeMap, mime_map: *jetzig.http.mime.MimeMap,
initialized: bool = false, initialized: bool = false,
store: *jetzig.kv.Store, store: *jetzig.kv.Store.GeneralStore,
job_queue: *jetzig.kv.Store, job_queue: *jetzig.kv.Store.JobQueueStore,
cache: *jetzig.kv.Store, cache: *jetzig.kv.Store.CacheStore,
repo: *jetzig.database.Repo, repo: *jetzig.database.Repo,
global: *anyopaque, global: *anyopaque,
decoded_static_route_params: []*jetzig.data.Value = &.{}, decoded_static_route_params: []*jetzig.data.Value = &.{},
@ -33,9 +33,9 @@ pub fn init(
job_definitions: []const jetzig.JobDefinition, job_definitions: []const jetzig.JobDefinition,
mailer_definitions: []const jetzig.MailerDefinition, mailer_definitions: []const jetzig.MailerDefinition,
mime_map: *jetzig.http.mime.MimeMap, mime_map: *jetzig.http.mime.MimeMap,
store: *jetzig.kv.Store, store: *jetzig.kv.Store.GeneralStore,
job_queue: *jetzig.kv.Store, job_queue: *jetzig.kv.Store.JobQueueStore,
cache: *jetzig.kv.Store, cache: *jetzig.kv.Store.CacheStore,
repo: *jetzig.database.Repo, repo: *jetzig.database.Repo,
global: *anyopaque, global: *anyopaque,
) Server { ) Server {

View File

@ -22,9 +22,9 @@ pub const JobEnv = struct {
/// All jobs detected by Jetzig on startup /// All jobs detected by Jetzig on startup
jobs: []const jetzig.JobDefinition, jobs: []const jetzig.JobDefinition,
/// Global key-value store /// Global key-value store
store: *jetzig.kv.Store, store: *jetzig.kv.Store.GeneralStore,
/// Global cache /// Global cache
cache: *jetzig.kv.Store, cache: *jetzig.kv.Store.CacheStore,
/// Database repo /// Database repo
repo: *jetzig.database.Repo, repo: *jetzig.database.Repo,
/// Global mutex - use with caution if it is necessary to guarantee thread safety/consistency /// Global mutex - use with caution if it is necessary to guarantee thread safety/consistency
@ -33,9 +33,9 @@ pub const JobEnv = struct {
}; };
allocator: std.mem.Allocator, allocator: std.mem.Allocator,
store: *jetzig.kv.Store, store: *jetzig.kv.Store.GeneralStore,
job_queue: *jetzig.kv.Store, job_queue: *jetzig.kv.Store.JobQueueStore,
cache: *jetzig.kv.Store, cache: *jetzig.kv.Store.CacheStore,
logger: jetzig.loggers.Logger, logger: jetzig.loggers.Logger,
name: []const u8, name: []const u8,
definition: ?JobDefinition, definition: ?JobDefinition,
@ -47,9 +47,9 @@ const Job = @This();
/// Initialize a new Job /// Initialize a new Job
pub fn init( pub fn init(
allocator: std.mem.Allocator, allocator: std.mem.Allocator,
store: *jetzig.kv.Store, store: *jetzig.kv.Store.GeneralStore,
job_queue: *jetzig.kv.Store, job_queue: *jetzig.kv.Store.JobQueueStore,
cache: *jetzig.kv.Store, cache: *jetzig.kv.Store.CacheStore,
logger: jetzig.loggers.Logger, logger: jetzig.loggers.Logger,
jobs: []const JobDefinition, jobs: []const JobDefinition,
name: []const u8, name: []const u8,

View File

@ -5,7 +5,7 @@ const jetzig = @import("../../jetzig.zig");
const Pool = @This(); const Pool = @This();
allocator: std.mem.Allocator, allocator: std.mem.Allocator,
job_queue: *jetzig.kv.Store, job_queue: *jetzig.kv.Store.JobQueueStore,
job_env: jetzig.jobs.JobEnv, job_env: jetzig.jobs.JobEnv,
pool: std.Thread.Pool = undefined, pool: std.Thread.Pool = undefined,
workers: std.ArrayList(*jetzig.jobs.Worker), workers: std.ArrayList(*jetzig.jobs.Worker),
@ -13,7 +13,7 @@ workers: std.ArrayList(*jetzig.jobs.Worker),
/// Initialize a new worker thread pool. /// Initialize a new worker thread pool.
pub fn init( pub fn init(
allocator: std.mem.Allocator, allocator: std.mem.Allocator,
job_queue: *jetzig.kv.Store, job_queue: *jetzig.kv.Store.JobQueueStore,
job_env: jetzig.jobs.JobEnv, job_env: jetzig.jobs.JobEnv,
) Pool { ) Pool {
return .{ return .{

View File

@ -6,14 +6,14 @@ const Worker = @This();
allocator: std.mem.Allocator, allocator: std.mem.Allocator,
job_env: jetzig.jobs.JobEnv, job_env: jetzig.jobs.JobEnv,
id: usize, id: usize,
job_queue: *jetzig.kv.Store, job_queue: *jetzig.kv.Store.JobQueueStore,
interval: usize, interval: usize,
pub fn init( pub fn init(
allocator: std.mem.Allocator, allocator: std.mem.Allocator,
job_env: jetzig.jobs.JobEnv, job_env: jetzig.jobs.JobEnv,
id: usize, id: usize,
job_queue: *jetzig.kv.Store, job_queue: *jetzig.kv.Store.JobQueueStore,
interval: usize, interval: usize,
) Worker { ) Worker {
return .{ return .{

View File

@ -1,3 +1,36 @@
const std = @import("std"); const std = @import("std");
pub const Store = @import("kv/Store.zig"); const config = @import("config.zig");
pub const Store = struct {
/// Configuration for JetKV. Encompasses all backends:
/// * valkey
/// * memory
/// * file
///
/// The Valkey backend is recommended for production deployment. `memory` and `file` can be
/// used in local development for convenience. All backends have a unified interface, i.e.
/// they can be swapped out without any code changes.
pub const Options = @import("kv/Store.zig").KVOptions;
// For backward compatibility - `jetzig.kv.Options` is preferred.
pub const KVOptions = Options;
/// General-purpose store. Use for storing data with no expiry.
pub const GeneralStore = @import("kv/Store.zig").Store(config.get(Store.Options, "store"));
/// Store ephemeral data.
pub const CacheStore = @import("kv/Store.zig").Store(config.get(Store.Options, "cache"));
/// Background job storage.
pub const JobQueueStore = @import("kv/Store.zig").Store(config.get(Store.Options, "job_queue"));
/// Generic store type. Create a custom store by passing `Options`, e.g.:
/// ```zig
/// var store = Generic(.{ .backend = .memory }).init(allocator, logger, .custom);
/// ```
pub const Generic = @import("kv/Store.zig").Store;
/// Role a given store fills. Used in log outputs.
pub const Role = @import("kv/Store.zig").Role;
};

View File

@ -1,87 +1,156 @@
const std = @import("std"); const std = @import("std");
const jetzig = @import("../../jetzig.zig"); const jetzig = @import("../../jetzig.zig");
const Store = @This();
store: jetzig.jetkv.JetKV,
options: KVOptions,
pub const KVOptions = struct { pub const KVOptions = struct {
backend: enum { memory, file } = .memory, backend: enum { memory, file, valkey } = .memory,
file_options: struct { file_options: struct {
path: ?[]const u8 = null, path: ?[]const u8 = null,
address_space_size: u32 = jetzig.jetkv.JetKV.FileBackend.addressSpace(4096), address_space_size: u32 = jetzig.jetkv.FileBackend.addressSpace(4096),
truncate: bool = false, truncate: bool = false,
} = .{}, } = .{},
valkey_options: struct {
host: []const u8 = "localhost",
port: u16 = 6379,
connect_timeout: u64 = 1000, // (ms)
read_timeout: u64 = 1000, // (ms)
connect: enum { auto, manual, lazy } = .lazy,
buffer_size: u32 = 8192,
pool_size: u16 = 8,
} = .{},
}; };
const ValueType = enum { string, array }; const ValueType = enum { string, array };
/// Initialize a new memory or file store. fn jetKVOptions(options: KVOptions) jetzig.jetkv.Options {
pub fn init(allocator: std.mem.Allocator, options: KVOptions) !Store { return switch (options.backend) {
const store = try jetzig.jetkv.JetKV.init( .file => .{
allocator, .backend = .file,
switch (options.backend) { .file_backend_options = .{
.file => .{ .path = options.file_options.path,
.backend = .file, .address_space_size = options.file_options.address_space_size,
.file_backend_options = .{ .truncate = options.file_options.truncate,
.path = options.file_options.path,
.address_space_size = options.file_options.address_space_size,
.truncate = options.file_options.truncate,
},
},
.memory => .{
.backend = .memory,
}, },
}, },
); .memory => .{
.backend = .memory,
return .{ .store = store, .options = options }; },
.valkey => .{
.backend = .valkey,
.valkey_backend_options = .{
.host = options.valkey_options.host,
.port = options.valkey_options.port,
.connect_timeout = options.valkey_options.connect_timeout * std.time.ms_per_s,
.read_timeout = options.valkey_options.read_timeout * std.time.ms_per_s,
.connect = std.enums.nameCast(
jetzig.jetkv.ValkeyBackendOptions.ConnectMode,
options.valkey_options.connect,
),
.buffer_size = options.valkey_options.buffer_size,
.pool_size = options.valkey_options.pool_size,
},
},
};
} }
/// Free allocated resources/close database file. /// Role a given store fills. Used in log outputs.
pub fn deinit(self: *Store) void { pub const Role = enum { jobs, cache, general, custom };
self.store.deinit();
}
/// Put a or into the key-value store. pub fn Store(comptime options: KVOptions) type {
pub fn put(self: *Store, key: []const u8, value: *jetzig.data.Value) !void { return struct {
try self.store.put(key, try value.toJson()); const Self = @This();
}
/// Get a Value from the store. store: jetzig.jetkv.JetKV(jetKVOptions(options)),
pub fn get(self: *Store, data: *jetzig.data.Data, key: []const u8) !?*jetzig.data.Value { logger: jetzig.loggers.Logger,
return try parseValue(data, try self.store.get(data.allocator(), key)); options: KVOptions,
} role: Role,
/// Remove a Value to from the key-value store and return it if found. /// Initialize a new memory or file store.
pub fn fetchRemove(self: *Store, data: *jetzig.data.Data, key: []const u8) !?*jetzig.data.Value { pub fn init(allocator: std.mem.Allocator, logger: jetzig.loggers.Logger, role: Role) !Self {
return try parseValue(data, try self.store.fetchRemove(data.allocator(), key)); const store = try jetzig.jetkv.JetKV(jetKVOptions(options)).init(allocator);
}
/// Remove a Value to from the key-value store. return .{ .store = store, .role = role, .logger = logger, .options = options };
pub fn remove(self: *Store, key: []const u8) !void { }
try self.store.remove(key);
}
/// Append a Value to the end of an Array in the key-value store. /// Free allocated resources/close database file.
pub fn append(self: *Store, key: []const u8, value: *const jetzig.data.Value) !void { pub fn deinit(self: *Self) void {
try self.store.append(key, try value.toJson()); self.store.deinit();
} }
/// Prepend a Value to the start of an Array in the key-value store. /// Put a or into the key-value store.
pub fn prepend(self: *Store, key: []const u8, value: *const jetzig.data.Value) !void { pub fn put(self: *Self, key: []const u8, value: *jetzig.data.Value) !void {
try self.store.prepend(key, try value.toJson()); try self.store.put(key, try value.toJson());
} if (self.role == .cache) {
try self.logger.DEBUG(
"[cache:{s}:store] {s}",
.{ @tagName(self.store.backend), key },
);
}
}
/// Pop a Value from an Array in the key-value store. /// Put a or into the key-value store with an expiration in seconds.
pub fn pop(self: *Store, data: *jetzig.data.Data, key: []const u8) !?*jetzig.data.Value { pub fn putExpire(self: *Self, key: []const u8, value: *jetzig.data.Value, expiration: i32) !void {
return try parseValue(data, try self.store.pop(data.allocator(), key)); try self.store.putExpire(key, try value.toJson(), expiration);
} if (self.role == .cache) {
try self.logger.DEBUG(
"[cache:{s}:store:expire:{d}s] {s}",
.{ @tagName(self.store.backend), expiration, key },
);
}
}
/// Left-pop a Value from an Array in the key-value store. /// Get a Value from the store.
pub fn popFirst(self: *Store, data: *jetzig.data.Data, key: []const u8) !?*jetzig.data.Value { pub fn get(self: *Self, data: *jetzig.data.Data, key: []const u8) !?*jetzig.data.Value {
return try parseValue(data, try self.store.popFirst(data.allocator(), key)); const start = std.time.nanoTimestamp();
const json = try self.store.get(data.allocator, key);
const value = try parseValue(data, json);
const end = std.time.nanoTimestamp();
if (self.role == .cache) {
if (value == null) {
try self.logger.DEBUG("[cache:miss] {s}", .{key});
} else {
try self.logger.DEBUG(
"[cache:{s}:hit:{}] {s}",
.{
@tagName(self.store.backend),
std.fmt.fmtDuration(@intCast(end - start)),
key,
},
);
}
}
return value;
}
/// Remove a Value to from the key-value store and return it if found.
pub fn fetchRemove(self: *Self, data: *jetzig.data.Data, key: []const u8) !?*jetzig.data.Value {
return try parseValue(data, try self.store.fetchRemove(data.allocator, key));
}
/// Remove a Value to from the key-value store.
pub fn remove(self: *Self, key: []const u8) !void {
try self.store.remove(key);
}
/// Append a Value to the end of an Array in the key-value store.
pub fn append(self: *Self, key: []const u8, value: *const jetzig.data.Value) !void {
try self.store.append(key, try value.toJson());
}
/// Prepend a Value to the start of an Array in the key-value store.
pub fn prepend(self: *Self, key: []const u8, value: *const jetzig.data.Value) !void {
try self.store.prepend(key, try value.toJson());
}
/// Pop a Value from an Array in the key-value store.
pub fn pop(self: *Self, data: *jetzig.data.Data, key: []const u8) !?*jetzig.data.Value {
return try parseValue(data, try self.store.pop(data.allocator, key));
}
/// Left-pop a Value from an Array in the key-value store.
pub fn popFirst(self: *Self, data: *jetzig.data.Data, key: []const u8) !?*jetzig.data.Value {
return try parseValue(data, try self.store.popFirst(data.allocator, key));
}
};
} }
fn parseValue(data: *jetzig.data.Data, maybe_json: ?[]const u8) !?*jetzig.data.Value { fn parseValue(data: *jetzig.data.Data, maybe_json: ?[]const u8) !?*jetzig.data.Value {

View File

@ -130,7 +130,7 @@ fn defaultHtml(
data.value = if (params.get("params")) |capture| data.value = if (params.get("params")) |capture|
capture capture
else else
try jetzig.zmpl.Data.createObject(data.allocator()); try jetzig.zmpl.Data.createObject(data.allocator);
try data.addConst("jetzig_view", data.string("")); try data.addConst("jetzig_view", data.string(""));
try data.addConst("jetzig_action", data.string("")); try data.addConst("jetzig_action", data.string(""));
return if (jetzig.zmpl.findPrefixed("mailers", mailer.html_template)) |template| return if (jetzig.zmpl.findPrefixed("mailers", mailer.html_template)) |template|
@ -148,7 +148,7 @@ fn defaultText(
data.value = if (params.get("params")) |capture| data.value = if (params.get("params")) |capture|
capture capture
else else
try jetzig.zmpl.Data.createObject(data.allocator()); try jetzig.zmpl.Data.createObject(data.allocator);
try data.addConst("jetzig_view", data.string("")); try data.addConst("jetzig_view", data.string(""));
try data.addConst("jetzig_action", data.string("")); try data.addConst("jetzig_action", data.string(""));
return if (jetzig.zmpl.findPrefixed("mailers", mailer.text_template)) |template| return if (jetzig.zmpl.findPrefixed("mailers", mailer.text_template)) |template|

View File

@ -4,13 +4,14 @@ const jetzig = @import("../../jetzig.zig");
const httpz = @import("httpz"); const httpz = @import("httpz");
const App = @This(); const App = @This();
const MemoryStore = jetzig.kv.Store.Generic(.{ .backend = .memory });
allocator: std.mem.Allocator, allocator: std.mem.Allocator,
routes: []const jetzig.views.Route, routes: []const jetzig.views.Route,
arena: *std.heap.ArenaAllocator, arena: *std.heap.ArenaAllocator,
store: *jetzig.kv.Store, store: *MemoryStore,
cache: *jetzig.kv.Store, cache: *MemoryStore,
job_queue: *jetzig.kv.Store, job_queue: *MemoryStore,
multipart_boundary: ?[]const u8 = null, multipart_boundary: ?[]const u8 = null,
logger: jetzig.loggers.Logger, logger: jetzig.loggers.Logger,
server: Server, server: Server,
@ -60,9 +61,9 @@ pub fn init(allocator: std.mem.Allocator, routes_module: type) !App {
.arena = arena, .arena = arena,
.allocator = allocator, .allocator = allocator,
.routes = &routes_module.routes, .routes = &routes_module.routes,
.store = try createStore(arena.allocator()), .store = try createStore(arena.allocator(), logger, .general),
.cache = try createStore(arena.allocator()), .cache = try createStore(arena.allocator(), logger, .cache),
.job_queue = try createStore(arena.allocator()), .job_queue = try createStore(arena.allocator(), logger, .jobs),
.logger = logger, .logger = logger,
.server = .{ .logger = logger }, .server = .{ .logger = logger },
.repo = repo, .repo = repo,
@ -391,9 +392,17 @@ fn multiFormKeyValue(allocator: std.mem.Allocator, max: usize) !*httpz.key_value
return key_value; return key_value;
} }
fn createStore(allocator: std.mem.Allocator) !*jetzig.kv.Store { fn createStore(
const store = try allocator.create(jetzig.kv.Store); allocator: std.mem.Allocator,
store.* = try jetzig.kv.Store.init(allocator, .{}); logger: jetzig.loggers.Logger,
role: jetzig.kv.Store.Role,
) !*MemoryStore {
const store = try allocator.create(MemoryStore);
store.* = try MemoryStore.init(
allocator,
logger,
role,
);
return store; return store;
} }