-
-
Notifications
You must be signed in to change notification settings - Fork 161
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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. | ||||||
|
||||||
# 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. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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?). There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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: 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) There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 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! There was a problem hiding this comment. Choose a reason for hiding this commentThe 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"; | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (nit) Using
Suggested change
|
||||||
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. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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