1
0
Fork 0
mirror of https://github.com/zigzap/zap.git synced 2025-10-20 15:14:08 +00:00
zap/examples/endpoint/users.zig
2025-08-28 22:03:43 +02:00

202 lines
6.2 KiB
Zig

const std = @import("std");
alloc: std.mem.Allocator = undefined,
users: std.AutoHashMap(usize, InternalUser) = undefined,
lock: std.Thread.Mutex = undefined,
count: usize = 0,
pub const Users = @This();
const InternalUser = struct {
id: usize = 0,
firstnamebuf: [64]u8,
firstnamelen: usize,
lastnamebuf: [64]u8,
lastnamelen: usize,
};
pub const User = struct {
id: usize = 0,
first_name: []const u8,
last_name: []const u8,
};
pub fn init(a: std.mem.Allocator) Users {
return .{
.alloc = a,
.users = std.AutoHashMap(usize, InternalUser).init(a),
.lock = std.Thread.Mutex{},
};
}
pub fn deinit(self: *Users) void {
self.users.deinit();
}
// the request will be freed (and its mem reused by facilio) when it's
// completed, so we take copies of the names
pub fn addByName(self: *Users, first: ?[]const u8, last: ?[]const u8) !usize {
var user: InternalUser = undefined;
user.firstnamelen = 0;
user.lastnamelen = 0;
if (first) |firstname| {
@memcpy(user.firstnamebuf[0..firstname.len], firstname);
user.firstnamelen = firstname.len;
}
if (last) |lastname| {
@memcpy(user.lastnamebuf[0..lastname.len], lastname);
user.lastnamelen = lastname.len;
}
// We lock only on insertion, deletion, and listing
self.lock.lock();
defer self.lock.unlock();
user.id = self.count + 1;
if (self.users.put(user.id, user)) {
self.count += 1;
return user.id;
} else |err| {
std.debug.print("addByName error: {}\n", .{err});
// make sure we pass on the error
return err;
}
}
pub fn delete(self: *Users, id: usize) bool {
// We lock only on insertion, deletion, and listing
self.lock.lock();
defer self.lock.unlock();
const ret = self.users.remove(id);
if (ret) {
self.count -= 1;
}
return ret;
}
pub fn get(self: *Users, id: usize) ?User {
// we don't care about locking here, as our usage-pattern is unlikely to
// get a user by id that is not known yet
if (self.users.getPtr(id)) |pUser| {
return .{
.id = pUser.id,
.first_name = pUser.firstnamebuf[0..pUser.firstnamelen],
.last_name = pUser.lastnamebuf[0..pUser.lastnamelen],
};
}
return null;
}
pub fn update(
self: *Users,
id: usize,
first: ?[]const u8,
last: ?[]const u8,
) bool {
// we don't care about locking here
// we update in-place, via getPtr
if (self.users.getPtr(id)) |pUser| {
pUser.firstnamelen = 0;
pUser.lastnamelen = 0;
if (first) |firstname| {
@memcpy(pUser.firstnamebuf[0..firstname.len], firstname);
pUser.firstnamelen = firstname.len;
}
if (last) |lastname| {
@memcpy(pUser.lastnamebuf[0..lastname.len], lastname);
pUser.lastnamelen = lastname.len;
}
}
return false;
}
pub fn toJSON(self: *Users) ![]const u8 {
self.lock.lock();
defer self.lock.unlock();
// We create a User list that's JSON-friendly
// NOTE: we could also implement the whole JSON writing ourselves here,
// working directly with InternalUser elements of the users hashmap.
// might actually save some memory
// TODO: maybe do it directly with the user.items
var l: std.ArrayList(User) = std.ArrayList(User).empty;
defer l.deinit(self.alloc);
// the potential race condition is fixed by jsonifying with the mutex locked
var it = JsonUserIteratorWithRaceCondition.init(&self.users);
while (it.next()) |user| {
try l.append(self.alloc, user);
}
std.debug.assert(self.users.count() == l.items.len);
std.debug.assert(self.count == l.items.len);
return std.json.Stringify.valueAlloc(self.alloc, l.items, .{});
}
//
// Note: the following code is kept in here because it taught us a lesson
//
pub fn listWithRaceCondition(self: *Users, out: *std.ArrayList(User)) !void {
// We lock only on insertion, deletion, and listing
//
// NOTE: race condition:
// =====================
//
// the list returned from here contains elements whose slice fields
// (.first_name and .last_name) point to char buffers of elements of the
// users list:
//
// user.first_name -> internal_user.firstnamebuf[..]
//
// -> we're only referencing the memory of first and last names.
// -> while the caller works with this list, e.g. "slowly" converting it to
// JSON, the users hashmap might be added to massively in the background,
// causing it to GROW -> realloc -> all slices get invalidated!
//
// So, to mitigate that, either:
// - [x] listing and converting to JSON must become one locked operation
// - or: the iterator must make copies of the strings
self.lock.lock();
defer self.lock.unlock();
var it = JsonUserIteratorWithRaceCondition.init(&self.users);
while (it.next()) |user| {
try out.append(user);
}
std.debug.assert(self.users.count() == out.items.len);
std.debug.assert(self.count == out.items.len);
}
const JsonUserIteratorWithRaceCondition = struct {
it: std.AutoHashMap(usize, InternalUser).ValueIterator = undefined,
pub fn init(internal_users: *std.AutoHashMap(usize, InternalUser)) JsonUserIteratorWithRaceCondition {
return .{
.it = internal_users.valueIterator(),
};
}
pub fn next(this: *JsonUserIteratorWithRaceCondition) ?User {
if (this.it.next()) |pUser| {
// we get a pointer to the internal user. so it should be safe to
// create slices from its first and last name buffers
//
// SEE ABOVE NOTE regarding race condition why this is can be problematic
var user: User = .{
// we don't need .* syntax but want to make it obvious
.id = pUser.*.id,
.first_name = pUser.*.firstnamebuf[0..pUser.*.firstnamelen],
.last_name = pUser.*.lastnamebuf[0..pUser.*.lastnamelen],
};
if (pUser.*.firstnamelen == 0) {
user.first_name = "";
}
if (pUser.*.lastnamelen == 0) {
user.last_name = "";
}
return user;
}
return null;
}
};