#iocaineDevLog

algernon, evil bachelor of artsalgernon@come-from.mad-scientist.club
2025-06-25
{
{
  "_msg": "poisoning request: reject",
  "service": "iocaine",
  "verdict": {
    "type": "reject",
    "outcome": "not_for_us"
  },
  "foobar": true,
  "number?": 42,
  "detect": "nyihaha",
  "sure": true,
  "nested": {
    "hell": "yes"
  },
  "lua": "yes"
}

Nice.

#iocaineDevLog

algernon, evil bachelor of artsalgernon@come-from.mad-scientist.club
2025-06-25
local metadata = {}
local outcome = Outcome.garbage

-- do stuff that shoves info into metadata

log(outcome, metadata)

This'll do. It will turn it into a similar format as Roto's logger.

#iocaineDevLog

algernon, evil bachelor of artsalgernon@come-from.mad-scientist.club
2025-06-25

I think the Lua support pretty much reached feature parity with Roto by now, only logging left to do.

#iocaineDevLog

algernon, evil bachelor of artsalgernon@come-from.mad-scientist.club
2025-06-25

A very nice #Lua convenience: I don't need an init() function! I can just do the init in the body of the script.

function decide(request)
   if _G["foobar"]:contains(request:header("user-agent")) then
      return Outcome.not_for_us
   end

   return Outcome.garbage
end

_G["foobar"] = Patterns("test-agent", "curl")

It is also easier to make this kind of stuff available to Lua, from the Rust side:

#[derive(Clone)]
struct Patterns(AhoCorasick);

impl UserData for Patterns {
    fn add_methods<M: mlua::UserDataMethods<Self>>(methods: &mut M) {
        methods.add_method("contains", |_, this, needle: String| {
            Ok(this.0.is_match(&needle))
        })
    }
}

pub fn register(runtime: &Lua) -> Result<()> {
    let constructor = runtime.create_function(|_, patterns: Variadic<String>| {
        let ac = AhoCorasick::builder()
            .ascii_case_insensitive(true)
            .build(patterns.iter()).unwrap();
        Ok(Patterns(ac))
    })?;
    runtime.globals().set("Patterns", constructor)?;

    Ok(())
}

(Yes, there's an unwrap() there, I will get rid of it.)

It's so much simpler than for Roto, because Roto does not support lists yet, so I had to add a list builder too.

#iocaineDevLog

algernon, evil bachelor of artsalgernon@come-from.mad-scientist.club
2025-06-25

So... how do I minimize runtime dispatch?

I guess some context is necessary: iocaine stores the request handler instance in a struct called IocaineState, which eventually ends up being made available to every Axum request handler (.with_state(state)).

The most straightforward thing to do would be to make that state struct generic: struct IocaineState<T>, where the type would impl SexDungeon. However... that would mess with reload.

At reload time, I load the new configuration, and replace parts of IocaineState. But I can't replace the entire thing without rebuilding the routes, and that is not something I'm doing now, nor something I want to do anytime soon.

If I were to make state generic, reload could still work - as long as the language of the request handler doesn't change. But I want to enable switching languages without restarting!

So I guess I will have to accept a little bit of dynamic dispatch somewhere. Shouldn't hit performance too hard, though.

#iocaineDevLog

algernon, evil bachelor of artsalgernon@come-from.mad-scientist.club
2025-06-25

Ok, I have to make a common Trait. What do I call it? And what do I call the struct I use to implement the lua scripting?

The Roto stuff lives in MeansOfProduction. Hardly any of my struct names make sense, btw. I'm naming structs either in a funny way, or in a way that LLMs won't pick them up, even if they manage to scrape #iocaine's source.

I have names like TenXProgrammer (metrics), AssembledStatisticalSequences (templating), queer::HRT (persistent metrics), bullshit (garbage generation), bullshit::GargleBargle (wordlist gen), bullshit::WurstSalatGeneratorPro (markov gen), bullshit::GobbledyGook (rng), MeansOfProduction (roto scripting).

For the trait, I'm thinking SexDungeon, and Lua stuff will be Howl.

I don't envy anyone who tries to make sense of the iocaine sourcecode. But I am having lots of giggles, and lots of giggles is a good thing in this silly timeline of ours.

#iocaineDevLog

algernon, evil bachelor of artsalgernon@come-from.mad-scientist.club
2025-06-25
    pub fn decide(
        &self,
        headers: HeaderMap,
        path: Option<String>,
        method: &str,
        params: &BTreeMap<String, String>,
    ) -> Result<Outcome, Outcome> {
        let request = Request {
            method: method.to_owned(),
            path: path.unwrap_or_default(),
            headers,
            params: params.clone(),
        };

        let outcome = self
            .decide
            .call::<Outcome>(request)
            .unwrap_or(Outcome::Garbage);

        if outcome == Outcome::NotForUs {
            Err(outcome)
        } else {
            Ok(outcome)
        }
    }

This is pretty much the final implementation of the decide function for Lua, on the rust side. Everything else goes into the constructor.

#iocaineDevLog

algernon, evil bachelor of artsalgernon@come-from.mad-scientist.club
2025-06-25

In a number of ways, #Lua is going to be a better fit than #Roto: it's a far better known language, and a whole bunch of things are easier to do in Lua.

Do I regret going with #Roto first? Absolutely not. I like Roto's syntax better, and prefer its minimalism over Lua. From what I remember about my prior benchmarks, Roto is also significantly faster. But I'll do some side-by-side comparisons once the Lua support is in a better place, and once I can actually choose which one to use.

Right now I just made a struct that implements the same functions as MeansOfProduction, and replaced Roto with Lua. That is obviously not how it will work down the road.

#iocaineDevLog

algernon, evil bachelor of artsalgernon@come-from.mad-scientist.club
2025-06-25
function decide(request)
   if request:header("user-agent") == "test-agent/1.0" then
      return Outcome.not_for_us
   end

   return Outcome.garbage
end

Works like a charm, and just like with Roto, request is not serialized, or transformed into another struct. I merely expose a few accessors.

Cool, cool. I can soon move on to traiting, and then I can start exploring if there's anything else I need to expose to the Lua runtime. I imagine that the various bulk matchers I expose to Roto, will have to be exposed to Lua too.

However, unlike Roto, Lua doesn't need a context, the runtime is the context: init() can shove stuff into _G and call it a day, I don't have to prepare anything in advance.

#iocaineDevLog

algernon, evil bachelor of artsalgernon@come-from.mad-scientist.club
2025-06-25

Ok, I really need to teach this thing to load a script from a file. Recompiling every time I want to change the lua script to try something got old really fast.

#iocaineDevLog

algernon, evil bachelor of artsalgernon@come-from.mad-scientist.club
2025-06-25
function decide()
  return Outcome.challenge
end

This took 6 lines of Rust to implement, and was very simple. I have a good feeling about this.

#iocaineDevLog

algernon, evil bachelor of artsalgernon@come-from.mad-scientist.club
2025-06-25

Meanwhile, I made it pick the decide function once, so the fast path is now this:

pub fn decide(
    &self,
    headers: HeaderMap,
    path: Option<String>,
    method: &str,
    params: &BTreeMap<String, String>,
) -> Result<Outcome, Outcome> {
    let outcome = self.decide.call::<bool>(()).unwrap();

    if outcome {
        Ok(Outcome::Garbage)
    } else {
        Err(Outcome::NotForUs)
    }
}

...and we're at ~160k req/sec. This will slow down a notch, once I start adding stuff to the runtime, but... it won't slow down that much. I expect it will be similar to Roto's speed, maybe marginally slower.

(I'm using LuaJIT here, btw. I'll try other Luas too, at some point.)

#iocaineDevLog

algernon, evil bachelor of artsalgernon@come-from.mad-scientist.club
2025-06-25

If this works out the way I hope it will work out, then the next challenge will be building a Trait, and figuring out how to allow either Roto or Lua, without dyn or Box, and preferably not even a match on an enum.

#iocaineDevLog

algernon, evil bachelor of artsalgernon@come-from.mad-scientist.club
2025-06-25

Last night I started to wonder: what if I embedded #Lua in #iocaine?

So today I'm going to do that. A first, exploratory hack is compiling now, so I can benchmark it. It doesn't load any external files yet, it's a hardcoded return false.

#iocaineDevLog

algernon, evil bachelor of artsalgernon@come-from.mad-scientist.club
2025-06-22

Well, as I can't remember wtf I wanted to do, I guess I can muse about Tera a bit? No coding, just... thinking out loud.

Since the template engine is changing again, which will be a breaking change, I can take this opportunity to make some of the helper functions less confusing.

So before writing any code, before trying to translate the status quo, I'll spend a little time experimenting to see what kind of functions would work, what makes more sense, and so on.

However, that won't be today. I just caught myself nodding off, so time to hit the bed soon.

#iocaineDevLog

algernon, evil bachelor of artsalgernon@come-from.mad-scientist.club
2025-06-22
==1374722== All heap blocks were freed -- no leaks are possible

I guess this is Just The Way Things Areā„¢ then. I'm okay with that, I don't like hunting leaks in Rust code, it's not as much fun as it is in C.

Right. So... I still don't remember what I wanted to do before getting side tracked.

#iocaineDevLog

algernon, evil bachelor of artsalgernon@come-from.mad-scientist.club
2025-06-22

I wonder if I'm leaking training data. Or Roto stuff. I should probably try checking memory use with scripting disabled, to see how that fares.

Would make it simple to rule that part out!

#iocaineDevLog

algernon, evil bachelor of artsalgernon@come-from.mad-scientist.club
2025-06-22

Testing locally, started off with ~100MiB reported by iocaine, after 100 reloads, 210MiB, another 100 reloads, down to 187MiB.

Bit weird. Valgrind time.

#iocaineDevLog

algernon, evil bachelor of artsalgernon@come-from.mad-scientist.club
2025-06-22

Interesting. systemctl says memory use is ~85.3MiB, but iocaine's own metrics report 98MiB. That's a not an insignificant difference.

Anyway, valgrind time soon, just waiting for a release build with debug symbols to compile.

#iocaineDevLog

algernon, evil bachelor of artsalgernon@come-from.mad-scientist.club
2025-06-22

Eeeeeh, no. It fell back to ~85MiB.

I suppose this is where I reach for valgrind locally, and stop messing with prod.

C'mon, I'm just enjoying that reload finally is a thing! I'm like a kid who has a new toy! :flan_ooh: :flan_aww:

#iocaineDevLog

Client Info

Server: https://mastodon.social
Version: 2025.04
Repository: https://github.com/cyevgeniy/lmst