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

[RFC 0159] General purpose allocator module #159

Closed
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
151 changes: 151 additions & 0 deletions rfcs/0159-general-purpose-allocator-module.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
---
feature: general-purpose-allocator-module
start-date: 2023-08-11
author: lucasew
co-authors: (find a buddy later to help out with the RFC)
shepherd-team: (names, to be nominated and accepted by RFC steering committee)
shepherd-leader: (name to be appointed by RFC steering committee)
related-issues: (will contain links to implementation PRs)
---

# Summary
[summary]: #summary

A function that generates a suggestion based item allocator module.
Copy link

Choose a reason for hiding this comment

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

what is a suggestion in a deterministic system?
the word suggestion makes me think of uncertainty (might be just me though 🤷)

Maybe the summary here could describe the overall target, what this system would help achieve

Here you mention a function, I think the function in question is an implementation detail, the goal of this RFC is not to have a function!
👉 The goal of this RFC (as I see it) is to have a way to automatically compute values in a constrained space, so we don't have to precisely think of their precise values, and we can have high-level checks applied on top to assert invariants (e.g. uniqueness of ports) if necessary.

Copy link
Author

Choose a reason for hiding this comment

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

Suggestion = next available value in the value space

Ex: next port that isn't being used

Copy link

@bew bew Sep 20, 2023

Choose a reason for hiding this comment

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

Yeah I'm sorry, I mis-understood the subject of your RFC, I initially thought you wanted to compute all values, but instead want a kind of linter that can suggest config changes (very different!)

I think it should be described more explicitly to avoid this mis-understanding


# Motivation
[motivation]: #motivation

Sometimes there are some values that the user don't actually care about which
value a option will get in some kind of space as long is a valid one and doesn't
conflict with other definitions.

One of these spaces, for example, is the port space, like, non administrative ports
for servers (1025..49151). You will probably put a reverse proxy in front of it
so even the stability of the port number is not so important.
Copy link

Choose a reason for hiding this comment

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

I think it would be helpful to also think about another space than ports to help design a general purpose system (otherwise can we say it is general if there is currently only one use case?).

Copy link
Author

Choose a reason for hiding this comment

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

That's precisely what I am blocked now.

How can I better generalize the abstraction.


# Detailed design
[design]: #detailed-design
Copy link

Choose a reason for hiding this comment

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

In #151 there was a concern about stable value allocation even if we add/remove usage of the allocator:
#151 (comment)

Do you solve this problem? If yes, how?

I think this problem would benefit from some development in the RFC

@kevincox made a proposal for a design here: #151 (comment)
I think there are interesting things to use here, and would benefit from being included in this RFC if it wants to be generic, or at least offer a way to implement these ideas!

Copy link
Author

Choose a reason for hiding this comment

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

That's why it now suggests a value that you have to explicitly set later for it to accept.

Copy link

Choose a reason for hiding this comment

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

In ports space it might be interesting to have sub-spaces (ranges of allocation?), where a some services could be associated with a range of possible ports. For example my service could have a public API on port 7000 and a more restricted API on port 7001. Both are related to the same service and could be grouped together.

These sub-spaces would ultimately be flattened out and checked like any other values in that space, but would allow grouping of related values.

Copy link
Author

Choose a reason for hiding this comment

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


A function that receives the following parameters:
- `enableDescription`: The description of the enable option for one of the resources. Can also be a function that receives the value name and returns the description.
- `valueKey`: Key of the allocated value, like `value` or `port`. By default is `"value"`.
- `valueType`: Type of the value as the `type` parameter of `mkOption`. By default, as `mkOption`, is `null`.
- `valueApply`: Apply function passed to the value `mkOption`. By default, as `mkOption`, is `null`.
- `valueLiteral`: User friendly string representation of the value. By default is string-enclosed value passed to `keyFunc`.
- `valueDescription`: The description of the value option for one of the resources. Can also be a function that receives the value name and returns the description.
- `firstValue`: First item allocated. By default is `0`.
- `keyFunc`: Function that transforms the value to string in a way that uniquely identified the value for conflict checking. By default is `toString`.
- `succFunc`: Get the next value in the allocation space, like the next port or the next item of some item. This parameter is required.
- `validateFunc`: Function that returns if some value is valid. By default is the `valueType.check` function.
- `cfg`: As most of the module definitions in NixOS, receives the resolved reference of the option being defined.
- `keyPath`: Path in the module system to the option being defined. Used to give better error messages.
- `example`, `internal`, `relatedPackages`, `visible` and `description`: Just passed through to the outer `mkOption`.

This function will return a NixOS module system module that will follow the following rough schema:

```
<keyPath> = {
<name> = {
enable: boolean = false;
<valueKey>: <valueType> = null;
};
}
```
The values checking will happen in the following order:

- If any `<keyPath>.<name>.enable` is true and `<keyPath>.<name>.<valueKey>` is `null` it will suggest the next value available.

- If any more than one `<keyPath>.<name>` has the same `<keyFunc> <keyPath>.<name>.<valueKey>` it will suggest that one of the values is changed to the suggested value.

- If any `<keyPath>.<name>.enable` is true and `<validateFunc> <keyPath>.<name>.<keyFunc>` is `false` then it will list all the invalid value keys and suggest to change the first value key to the suggested value.

Only one suggested value is generated per evaluation in one module, so it will give up on first fail.

# Examples and Interactions
[examples-and-interactions]: #examples-and-interactions

This is an example of a port allocator using the function, plus a usage example:

```nix
Copy link

Choose a reason for hiding this comment

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

Hello!

I don't really understand this example :/

I don't really see where the magic happen here, what does the mkAllocationModule actually do / how is it useful in the example below.
From a birds eye view I see you set an option and use it just below, but I don't see a demonstration of the automatic port allocation the module seems to feature. Or am I reading it wrong?

I think the example should be more developed, and explicitly show and explain how it can be useful to have a system like this.


I like the initial description, it looks like a great idea!
But as a generic reader I think the example should prove to me and convince me of the idea if I had any doubts!

Copy link
Author

Choose a reason for hiding this comment

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

This implementation suggests a value if it's missing or invalid. If all values are existent, valid and no conflicts found then it accepts.

Otherwise it fails and suggests a valid value in the scope. In this case, a port.

The first implementation didn't have this "confirmation" so when you added another service, as attrsets are sorted in dictionary order, some services were restarted only because of the port changed, because that name ordering shift.

This way this problem do not happen.

{ config, lib }:
let
inherit (lib) types;
inherit (__future__) mkAllocatorModule;
in {
options.networking.ports = mkAllocatorModule {
valueKey = "port";
valueType = types.port;
cfg = config.networking.ports;
description = "Build time port allocations for services that are only used internally";
Copy link

Choose a reason for hiding this comment

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

(nit)
I initially read this as "Build (time port allocations) for services"
instead of "(Build time) (port allocations) for services"
and not understanding what it was ^^

Using build-time as a single word is easier to read I think

Suggested change
description = "Build time port allocations for services that are only used internally";
description = "Build-time port allocations for services that are only used internally";

enableDescription = name: "Enable automatic port allocation for service ${name}";
valueDescription = name: "Allocated port for service ${name}";

firstValue = 49151;
succFunc = x: x - 1;
valueLiteral = toString;
validateFunc = x: (types.port.check x) && (x > 1024);
keyPath = "networking.ports";
example = literalExpression ''{
app = {
enable = true;
port = 42069; # guided
};
}'';
};

config.environment.etc = lib.pipe config.networking.ports [
(attrNames)
(foldl' (x: y: x // {
"ports/${y}" = {
inherit (config.networking.ports.${y}) enable;
text = toString config.networking.ports.${y}.port;
};
}) {})
];
config.networking.ports = {
eoq = {
enable = false;
port = 22;
};
trabson = {
enable = true;
port = 49139;
};
};
}
```

# Drawbacks
[drawbacks]: #drawbacks

Evaluation time: the validation will need to happen everytime the module is used and the time it takes may be a problem. Allocators with many values can use a lot of recursion. IFD with an imperactive or a tail call optimized functional programming language for the validation phase may help.

# Alternatives
[alternatives]: #alternatives

Setting values by hand and hoping these values don't conflict on runtime.

Just allocate items without logic for reserving values as suggested initially by [RFC 151](https://github.com/NixOS/rfcs/pull/151).

# Unresolved questions
[unresolved]: #unresolved-questions

Is this the right abstraction for a generic allocator?

What about non primitive value allocations?

What about maybe some kind of space that has 2D conflicts that would require two keys to keep track or some kind of nesting like subnets?

Are function parameter names good enough?

# Future work
[future]: #future-work

Multiple machine based deployments.

Network allocation for NixOS cluster guests.

IPs for NixOS containers.

Port allocation for local running services.