diff mbox series

[RFC,net-next,1/5] selftests: forwarding: Introduce deferred commands

Message ID d5f8364b42f277daa9e235d23398e3dce5549e92.1724324945.git.petrm@nvidia.com
State New
Headers show
Series selftests: forwarding: Introduce deferred commands | expand

Commit Message

Petr Machata Aug. 22, 2024, 1:49 p.m. UTC
In commit 8510801a9dbd ("selftests: drv-net: add ability to schedule
cleanup with defer()"), a defer helper was added to Python selftests.
The idea is to keep cleanup commands close to their dirtying counterparts,
thereby making it more transparent what is cleaning up what, making it
harder to miss a cleanup, and make the whole cleanup business exception
safe. All these benefits are applicable to bash as well, exception safety
can be interpreted in terms of safety vs. a SIGINT.

This patch therefore introduces a framework of several helpers that serve
to schedule cleanups in bash selftests:

- defer_scope_push(), defer_scope_pop(): Deferred statements can be batched
  together in scopes. When a scope is popped, the deferred commands
  schoduled in that scope are executed in the order opposite to order of
  their scheduling.

- defer(): Schedules a defer to the most recently pushed scope (or the
  default scope if none was pushed.)

- defer_scopes_cleanup(): Pops any unpopped scopes, including the default
  one. The selftests that use defer should run this in their cleanup
  function. This is important to get cleanups of interrupted scripts.

  Consistent use of defers however obviates the need for a separate cleanup
  function -- everything is just taken care of in defers. So this patch
  actually introduces a cleanup() helper in the forwarding lib.sh, which
  calls just pre_cleanup() and defer_scopes_cleanup(). Selftests are
  obviously still free to override the function.

- defer_scoped_fn(): Sometimes a function would like to introduce a new
  defer scope, then run whatever it is that it wants to run, and then pop
  the scope to run the deferred cleanups. The helper defer_scoped_fn() can
  be used to derive from one function its wrapper that pushes a defer scope
  before the function is called, and pops it after it returns.

The following patches will convert several selftests to this new framework.

Signed-off-by: Petr Machata <petrm@nvidia.com>
---
 tools/testing/selftests/net/forwarding/lib.sh | 83 +++++++++++++++++++
 1 file changed, 83 insertions(+)

Comments

Ido Schimmel Aug. 26, 2024, 10:35 a.m. UTC | #1
On Thu, Aug 22, 2024 at 03:49:40PM +0200, Petr Machata wrote:
> In commit 8510801a9dbd ("selftests: drv-net: add ability to schedule
> cleanup with defer()"), a defer helper was added to Python selftests.
> The idea is to keep cleanup commands close to their dirtying counterparts,
> thereby making it more transparent what is cleaning up what, making it
> harder to miss a cleanup, and make the whole cleanup business exception
> safe. All these benefits are applicable to bash as well, exception safety
> can be interpreted in terms of safety vs. a SIGINT.
> 
> This patch therefore introduces a framework of several helpers that serve
> to schedule cleanups in bash selftests:
> 
> - defer_scope_push(), defer_scope_pop(): Deferred statements can be batched
>   together in scopes. When a scope is popped, the deferred commands
>   schoduled in that scope are executed in the order opposite to order of

s/schoduled/scheduled/

>   their scheduling.
> 
> - defer(): Schedules a defer to the most recently pushed scope (or the
>   default scope if none was pushed.)
> 
> - defer_scopes_cleanup(): Pops any unpopped scopes, including the default
>   one. The selftests that use defer should run this in their cleanup
>   function. This is important to get cleanups of interrupted scripts.
> 
>   Consistent use of defers however obviates the need for a separate cleanup
>   function -- everything is just taken care of in defers. So this patch
>   actually introduces a cleanup() helper in the forwarding lib.sh, which
>   calls just pre_cleanup() and defer_scopes_cleanup(). Selftests are
>   obviously still free to override the function.
> 
> - defer_scoped_fn(): Sometimes a function would like to introduce a new
>   defer scope, then run whatever it is that it wants to run, and then pop
>   the scope to run the deferred cleanups. The helper defer_scoped_fn() can
>   be used to derive from one function its wrapper that pushes a defer scope
>   before the function is called, and pops it after it returns.
> 
> The following patches will convert several selftests to this new framework.

The intention is to make sure new tests are using these helpers?

> 
> Signed-off-by: Petr Machata <petrm@nvidia.com>
> ---
>  tools/testing/selftests/net/forwarding/lib.sh | 83 +++++++++++++++++++

Does it make sense to place these helpers in net/lib.sh?
Przemek Kitszel Aug. 26, 2024, 1:09 p.m. UTC | #2
On 8/22/24 15:49, Petr Machata wrote:
> In commit 8510801a9dbd ("selftests: drv-net: add ability to schedule
> cleanup with defer()"), a defer helper was added to Python selftests.
> The idea is to keep cleanup commands close to their dirtying counterparts,
> thereby making it more transparent what is cleaning up what, making it
> harder to miss a cleanup, and make the whole cleanup business exception
> safe. All these benefits are applicable to bash as well, exception safety
> can be interpreted in terms of safety vs. a SIGINT.
> 
> This patch therefore introduces a framework of several helpers that serve
> to schedule cleanups in bash selftests:

Thank you for working on that, it would be great to have such
improvement for bash scripts in general, not limited to kselftests!

>   tools/testing/selftests/net/forwarding/lib.sh | 83 +++++++++++++++++++

Make it a new file in more generic location, add a comment section with
some examples and write down any assumptions there, perhaps defer.sh?

> - defer_scope_push(), defer_scope_pop(): Deferred statements can be batched >    together in scopes. When a scope is popped, the deferred commands
>    schoduled in that scope are executed in the order opposite to order of
>    their scheduling.

tldr of this sub-comment at the end

such API could be used in two variants:

1)
function test_executor1() {
	for t in tests; do
		defer_scope_push()
		exec_test1 $t
		defer_scope_pop()
	done
}

2)
function test_executor2() {
	for t in tests; do
		exec_test2 $t
	done
}
function exec_test2() {
	defer_scope_push()
	do_stuff "$@"
	defer_scope_pop()
}

That fractals down in the same way for "subtests", or some special stuff
like "make a zip" sub/task that will be used. And it could be misused as
a mix of the two variants.
I believe that the 1) is the better way, rationale: you write normal
code that does what needs to be done, using defer(), and caller (that
knows better) decides whether to sub-scope.
As this defer is very similar to golang's in intention, I would give
yet another analogy from golang's world. It's similar to concurrency, 
you write normal code that could be parallelized via "go" keyword,
instead of writing async code that needs to be awaited for.

Going back to the use case variants, there is no much sense to have
push() and pop() dispersed by much from each other, thus I would like
to introduce an API that just combines the two instead:

new_scope exec_test1 $t
(name discussion below)

> 
> - defer(): Schedules a defer to the most recently pushed scope (or the
>    default scope if none was pushed. >
> - defer_scopes_cleanup(): Pops any unpopped scopes, including the default
>    one. The selftests that use defer should run this in their cleanup
>    function. This is important to get cleanups of interrupted scripts.

this should be *the* trap(1)

with that said, it should be internal to the defer.sh script and it
should be obvious that developers must not introduce their own trap
(as of now we have ~330 in kselftests, ~270 of which in networking)

> 
>    Consistent use of defers however obviates the need for a separate cleanup
>    function -- everything is just taken care of in defers. So this patch
>    actually introduces a cleanup() helper in the forwarding lib.sh, which
>    calls just pre_cleanup() and defer_scopes_cleanup(). Selftests are
>    obviously still free to override the function.
> 
> - defer_scoped_fn(): Sometimes a function would like to introduce a new
>    defer scope, then run whatever it is that it wants to run, and then pop
>    the scope to run the deferred cleanups. The helper defer_scoped_fn() can
>    be used to derive from one function its wrapper that pushes a defer scope
>    before the function is called, and pops it after it returns.

It is basically a helper I would like to see as new_scope() mentioned
above, but it takes it upside down - it should really be the caller that
sub-scopes.


I think that the name of the new_scope() would be better, still concise,
but more precise as:
subscope_defer(),
trapped(), or
sub_trap().

I have no idea how to make a sub-trapped, SIGSEGV isolated scope of bash
execution that has ability to still edit outer scope variables. Perhaps
we could relax the need for edit to have easier implementation? It is
"all ok or failure/rollback" mode of operation anyway most of the time.

After the above parts will be discussed out I will look more into the
details of the code more deeply.
Petr Machata Aug. 26, 2024, 2:25 p.m. UTC | #3
Ido Schimmel <idosch@nvidia.com> writes:

> On Thu, Aug 22, 2024 at 03:49:40PM +0200, Petr Machata wrote:
>> The following patches will convert several selftests to this new framework.
>
> The intention is to make sure new tests are using these helpers?

Well, I sent this as RFC because I'm not sure how far to push it. I
think it would be ideal if this were adopted, because then cleanups
either always work, or are always broken, and we don't get partial or
forgotten cleanups. That's for new tests, I do not foresee converting
the existing selftests beyond a couple examples.

>> 
>> Signed-off-by: Petr Machata <petrm@nvidia.com>
>> ---
>>  tools/testing/selftests/net/forwarding/lib.sh | 83 +++++++++++++++++++
>
> Does it make sense to place these helpers in net/lib.sh?

Yeah, it does.
Petr Machata Aug. 26, 2024, 3:20 p.m. UTC | #4
Przemek Kitszel <przemyslaw.kitszel@intel.com> writes:

> On 8/22/24 15:49, Petr Machata wrote:
>> In commit 8510801a9dbd ("selftests: drv-net: add ability to schedule
>> cleanup with defer()"), a defer helper was added to Python selftests.
>> The idea is to keep cleanup commands close to their dirtying counterparts,
>> thereby making it more transparent what is cleaning up what, making it
>> harder to miss a cleanup, and make the whole cleanup business exception
>> safe. All these benefits are applicable to bash as well, exception safety
>> can be interpreted in terms of safety vs. a SIGINT.
>> This patch therefore introduces a framework of several helpers that serve
>> to schedule cleanups in bash selftests:
>
> Thank you for working on that, it would be great to have such
> improvement for bash scripts in general, not limited to kselftests!
>
>>   tools/testing/selftests/net/forwarding/lib.sh | 83 +++++++++++++++++++
>
> Make it a new file in more generic location, add a comment section with
> some examples and write down any assumptions there, perhaps defer.sh?

I can do it, but it's gonna be more pain in setting up those
TEST_INCLUDES. People will forget. It will be a nuisance.

I'm thinking of just moving it to net/lib.sh, from forwarding.

>> - defer_scope_push(), defer_scope_pop(): Deferred statements can be batched > together in scopes.
>> When a scope is popped, the deferred commands
>>    schoduled in that scope are executed in the order opposite to order of
>>    their scheduling.
>
> tldr of this sub-comment at the end
>
> such API could be used in two variants:
>
> 1)
> function test_executor1() {
> 	for t in tests; do
> 		defer_scope_push()
> 		exec_test1 $t
> 		defer_scope_pop()
> 	done
> }
>
> 2)
> function test_executor2() {
> 	for t in tests; do
> 		exec_test2 $t
> 	done
> }
> function exec_test2() {
> 	defer_scope_push()
> 	do_stuff "$@"
> 	defer_scope_pop()
> }
>
> That fractals down in the same way for "subtests", or some special stuff
> like "make a zip" sub/task that will be used. And it could be misused as
> a mix of the two variants.
> I believe that the 1) is the better way, rationale: you write normal
> code that does what needs to be done, using defer(), and caller (that
> knows better) decides whether to sub-scope.

But the caller does not know better. The cleanups can't be done
"sometime", but at a predictable place, so that they don't end up
interfering with other work. The callee knows where it needs the
cleanups to happen. The caller shouldn't have to know.

> As this defer is very similar to golang's in intention, I would give
> yet another analogy from golang's world. It's similar to concurrency, you write normal code that
> could be parallelized via "go" keyword,
> instead of writing async code that needs to be awaited for.

Notice how in go, defer also runs at function exit. Similarly with C++
destructors, run on scope exit. There's no caller-defined "collection
point".

Putting off until "sometime" works for memory. Things like garbage
collection, obstacks, autorelease pools, etc. work, because there's
plenty of memory and we don't mind keeping stuff around until later. But
that doesn't work for the sort of cleanups that selftests typically need
to do.

> Going back to the use case variants, there is no much sense to have
> push() and pop() dispersed by much from each other, thus I would like
> to introduce an API that just combines the two instead:
>
> new_scope exec_test1 $t
> (name discussion below)
>
>> - defer(): Schedules a defer to the most recently pushed scope (or the
>>    default scope if none was pushed. >
>> - defer_scopes_cleanup(): Pops any unpopped scopes, including the default
>>    one. The selftests that use defer should run this in their cleanup
>>    function. This is important to get cleanups of interrupted scripts.
>
> this should be *the* trap(1)
>
> with that said, it should be internal to the defer.sh script and it
> should be obvious that developers must not introduce their own trap
> (as of now we have ~330 in kselftests, ~270 of which in networking)

Yeah, we have 100+ tests that use their own traps in forwarding alone.
That ship has sailed.

I agree that the defer module probably has the "right" to own the exit
trap. Any other cleanups can be expressed in terms of defer, and I don't
know if there are legitimate uses of exit trap with that taken out. But
that's for sometime.

>>    Consistent use of defers however obviates the need for a separate cleanup
>>    function -- everything is just taken care of in defers. So this patch
>>    actually introduces a cleanup() helper in the forwarding lib.sh, which
>>    calls just pre_cleanup() and defer_scopes_cleanup(). Selftests are
>>    obviously still free to override the function.
>> - defer_scoped_fn(): Sometimes a function would like to introduce a new
>>    defer scope, then run whatever it is that it wants to run, and then pop
>>    the scope to run the deferred cleanups. The helper defer_scoped_fn() can
>>    be used to derive from one function its wrapper that pushes a defer scope
>>    before the function is called, and pops it after it returns.
>
> It is basically a helper I would like to see as new_scope() mentioned
> above, but it takes it upside down - it should really be the caller that
> sub-scopes.
>
> I think that the name of the new_scope() would be better, still concise,
> but more precise as:
> subscope_defer(),
> trapped(), or
> sub_trap().
>
> I have no idea how to make a sub-trapped, SIGSEGV isolated scope of bash
> execution that has ability to still edit outer scope variables. Perhaps
> we could relax the need for edit to have easier implementation? It is
> "all ok or failure/rollback" mode of operation anyway most of the time.

I'm not sure what you have in mind.

> After the above parts will be discussed out I will look more into the
> details of the code more deeply.
Jakub Kicinski Aug. 26, 2024, 8:03 p.m. UTC | #5
On Mon, 26 Aug 2024 16:25:47 +0200 Petr Machata wrote:
> >>  tools/testing/selftests/net/forwarding/lib.sh | 83 +++++++++++++++++++  
> >
> > Does it make sense to place these helpers in net/lib.sh?  
> 
> Yeah, it does.

Would it further make sense to split them to their own file
(net/lib_defer.sh?) and source that in net/lib.sh?

Should be pretty self-contained.
Jakub Kicinski Aug. 26, 2024, 8:04 p.m. UTC | #6
On Mon, 26 Aug 2024 13:03:43 -0700 Jakub Kicinski wrote:
> > > Does it make sense to place these helpers in net/lib.sh?    
> > 
> > Yeah, it does.  
> 
> Would it further make sense to split them to their own file
> (net/lib_defer.sh?) and source that in net/lib.sh?
> 
> Should be pretty self-contained.

Just saw your reply to Przemek, makes sense.
Przemek Kitszel Aug. 27, 2024, 6:21 a.m. UTC | #7
On 8/26/24 17:20, Petr Machata wrote:
> 
> Przemek Kitszel <przemyslaw.kitszel@intel.com> writes:
> 
>> On 8/22/24 15:49, Petr Machata wrote:
>>> In commit 8510801a9dbd ("selftests: drv-net: add ability to schedule
>>> cleanup with defer()"), a defer helper was added to Python selftests.
>>> The idea is to keep cleanup commands close to their dirtying counterparts,
>>> thereby making it more transparent what is cleaning up what, making it
>>> harder to miss a cleanup, and make the whole cleanup business exception
>>> safe. All these benefits are applicable to bash as well, exception safety
>>> can be interpreted in terms of safety vs. a SIGINT.
>>> This patch therefore introduces a framework of several helpers that serve
>>> to schedule cleanups in bash selftests:
>>
>> Thank you for working on that, it would be great to have such
>> improvement for bash scripts in general, not limited to kselftests!
>>
>>>    tools/testing/selftests/net/forwarding/lib.sh | 83 +++++++++++++++++++
>>
>> Make it a new file in more generic location, add a comment section with
>> some examples and write down any assumptions there, perhaps defer.sh?
> 
> I can do it, but it's gonna be more pain in setting up those
> TEST_INCLUDES. People will forget. It will be a nuisance.
> 
> I'm thinking of just moving it to net/lib.sh, from forwarding.

what about separate file, but included from net/lib.sh?

> 
>>> - defer_scope_push(), defer_scope_pop(): Deferred statements can be batched > together in scopes.
>>> When a scope is popped, the deferred commands
>>>     schoduled in that scope are executed in the order opposite to order of
>>>     their scheduling.
>>
>> tldr of this sub-comment at the end
>>
>> such API could be used in two variants:
>>
>> 1)
>> function test_executor1() {
>> 	for t in tests; do
>> 		defer_scope_push()
>> 		exec_test1 $t
>> 		defer_scope_pop()
>> 	done
>> }
>>
>> 2)
>> function test_executor2() {
>> 	for t in tests; do
>> 		exec_test2 $t
>> 	done
>> }
>> function exec_test2() {
>> 	defer_scope_push()
>> 	do_stuff "$@"
>> 	defer_scope_pop()
>> }
>>
>> That fractals down in the same way for "subtests", or some special stuff
>> like "make a zip" sub/task that will be used. And it could be misused as
>> a mix of the two variants.
>> I believe that the 1) is the better way, rationale: you write normal
>> code that does what needs to be done, using defer(), and caller (that
>> knows better) decides whether to sub-scope.
> 
> But the caller does not know better. The cleanups can't be done
> "sometime", but at a predictable place, so that they don't end up
> interfering with other work. The callee knows where it needs the
> cleanups to happen. The caller shouldn't have to know.

The caller should not have to know what will be cleaned, but knows that
they are done with callee.
OTOH, callee has no idea about the "other work".

> 
>> As this defer is very similar to golang's in intention, I would give
>> yet another analogy from golang's world. It's similar to concurrency, you write normal code that
>> could be parallelized via "go" keyword,
>> instead of writing async code that needs to be awaited for.
> 
> Notice how in go, defer also runs at function exit. Similarly with C++
> destructors, run on scope exit. There's no caller-defined "collection
> point".
> 
> Putting off until "sometime" works for memory. Things like garbage
> collection, obstacks, autorelease pools, etc. work, because there's
> plenty of memory and we don't mind keeping stuff around until later. But
> that doesn't work for the sort of cleanups that selftests typically need
> to do.

That's true. But I still believe that it's the caller (or better, "glue
code") responsibility to take care of cleanup schedule.

> 
>> Going back to the use case variants, there is no much sense to have
>> push() and pop() dispersed by much from each other, thus I would like
>> to introduce an API that just combines the two instead:
>>
>> new_scope exec_test1 $t
>> (name discussion below)
>>
>>> - defer(): Schedules a defer to the most recently pushed scope (or the
>>>     default scope if none was pushed. >
>>> - defer_scopes_cleanup(): Pops any unpopped scopes, including the default
>>>     one. The selftests that use defer should run this in their cleanup
>>>     function. This is important to get cleanups of interrupted scripts.
>>
>> this should be *the* trap(1)
>>
>> with that said, it should be internal to the defer.sh script and it
>> should be obvious that developers must not introduce their own trap
>> (as of now we have ~330 in kselftests, ~270 of which in networking)
> 
> Yeah, we have 100+ tests that use their own traps in forwarding alone.
> That ship has sailed.
> 
> I agree that the defer module probably has the "right" to own the exit
> trap. Any other cleanups can be expressed in terms of defer, and I don't
> know if there are legitimate uses of exit trap with that taken out. But
> that's for sometime.

There could be multiple traps for ERR/EXIT/etc conditions, but for
simplicity it's best to rely on just EXIT trap.
So we should convert current scripts one by one to use your new API.

> 
>>>     Consistent use of defers however obviates the need for a separate cleanup
>>>     function -- everything is just taken care of in defers. So this patch
>>>     actually introduces a cleanup() helper in the forwarding lib.sh, which
>>>     calls just pre_cleanup() and defer_scopes_cleanup(). Selftests are
>>>     obviously still free to override the function.
>>> - defer_scoped_fn(): Sometimes a function would like to introduce a new
>>>     defer scope, then run whatever it is that it wants to run, and then pop
>>>     the scope to run the deferred cleanups. The helper defer_scoped_fn() can
>>>     be used to derive from one function its wrapper that pushes a defer scope
>>>     before the function is called, and pops it after it returns.
>>
>> It is basically a helper I would like to see as new_scope() mentioned
>> above, but it takes it upside down - it should really be the caller that
>> sub-scopes.
>>
>> I think that the name of the new_scope() would be better, still concise,
>> but more precise as:
>> subscope_defer(),
>> trapped(), or
>> sub_trap().

here I mean that "scope" is too broad without the word "trap" or "defer"
in name

>>
>> I have no idea how to make a sub-trapped, SIGSEGV isolated scope of bash
>> execution that has ability to still edit outer scope variables. Perhaps
>> we could relax the need for edit to have easier implementation? It is
>> "all ok or failure/rollback" mode of operation anyway most of the time.
> 
> I'm not sure what you have in mind.

	foo=1
	function bumpfoo {
		maybe-crash
		foo=2
	}
	new-defer-scope bumpfoo
	echo $foo

do you want this to print 2 or 1?


> 
>> After the above parts will be discussed out I will look more into the
>> details of the code more deeply.
>
Petr Machata Aug. 27, 2024, 8:53 a.m. UTC | #8
Przemek Kitszel <przemyslaw.kitszel@intel.com> writes:

> On 8/26/24 17:20, Petr Machata wrote:
>> Przemek Kitszel <przemyslaw.kitszel@intel.com> writes:
>> 
>>> On 8/22/24 15:49, Petr Machata wrote:
>>>> In commit 8510801a9dbd ("selftests: drv-net: add ability to schedule
>>>> cleanup with defer()"), a defer helper was added to Python selftests.
>>>> The idea is to keep cleanup commands close to their dirtying counterparts,
>>>> thereby making it more transparent what is cleaning up what, making it
>>>> harder to miss a cleanup, and make the whole cleanup business exception
>>>> safe. All these benefits are applicable to bash as well, exception safety
>>>> can be interpreted in terms of safety vs. a SIGINT.
>>>> This patch therefore introduces a framework of several helpers that serve
>>>> to schedule cleanups in bash selftests:
>>>
>>> Thank you for working on that, it would be great to have such
>>> improvement for bash scripts in general, not limited to kselftests!
>>>
>>>>    tools/testing/selftests/net/forwarding/lib.sh | 83 +++++++++++++++++++
>>>
>>> Make it a new file in more generic location, add a comment section with
>>> some examples and write down any assumptions there, perhaps defer.sh?
>> I can do it, but it's gonna be more pain in setting up those
>> TEST_INCLUDES. People will forget. It will be a nuisance.
>> I'm thinking of just moving it to net/lib.sh, from forwarding.
>
> what about separate file, but included from net/lib.sh?

Unfortunately that would be even worse. Then you need to remember to put
the file into TEST_INCLUDES despite seemingly not using it.

Like ideally we'd have automation for this. But I don't know how to do that
without actually parsing the bash files, and that's just asking for
trouble. Maybe after the defer stuff we also need a module system :-/

>>>> - defer_scope_push(), defer_scope_pop(): Deferred statements can be batched > together in scopes.
>>>> When a scope is popped, the deferred commands
>>>>     schoduled in that scope are executed in the order opposite to order of
>>>>     their scheduling.
>>>
>>> tldr of this sub-comment at the end
>>>
>>> such API could be used in two variants:
>>>
>>> 1)
>>> function test_executor1() {
>>> 	for t in tests; do
>>> 		defer_scope_push()
>>> 		exec_test1 $t
>>> 		defer_scope_pop()
>>> 	done
>>> }
>>>
>>> 2)
>>> function test_executor2() {
>>> 	for t in tests; do
>>> 		exec_test2 $t
>>> 	done
>>> }
>>> function exec_test2() {
>>> 	defer_scope_push()
>>> 	do_stuff "$@"
>>> 	defer_scope_pop()
>>> }
>>>
>>> That fractals down in the same way for "subtests", or some special stuff
>>> like "make a zip" sub/task that will be used. And it could be misused as
>>> a mix of the two variants.
>>> I believe that the 1) is the better way, rationale: you write normal
>>> code that does what needs to be done, using defer(), and caller (that
>>> knows better) decides whether to sub-scope.
>> But the caller does not know better. The cleanups can't be done
>> "sometime", but at a predictable place, so that they don't end up
>> interfering with other work. The callee knows where it needs the
>> cleanups to happen. The caller shouldn't have to know.
>
> The caller should not have to know what will be cleaned, but knows that
> they are done with callee.
>
> OTOH, callee has no idea about the "other work".

Nor should it have to. It just needs to dispose of all responsibilities it
has acquired (read: clean up what it has dirtied, or what others have
dirtied for it). That's done by closing the defer scope.

But let me take a step back. I've been going back and forth on this
basically since yesterday.

In practice, the caller-defined scopes lead to nicer code.

If run_tests creates an implicit scope per test, most of the tests can just
issue their defers without thinking about it too much.

For cases where the implicit scope is not enough, the caller has to know
that a certain function needs to be run in a dedicated scope or else it
will interfere with something else that it's running. That's not great, it
complicates the caller-callee contract in a way that's not captured
anywhere in the syntax. But I suspect it's going to be just fine, these
scripts are not exactly complex, and if there's an interference, I figure
it will be easy to notice.

The major upside is that we avoid the need to pepper the code with
defer_scoped_fn.

So I'll drop defer_scoped_fn and add in_defer_scope:

in_defer_scope()
{
	local ret

	defer_scope_push
	"$@"
	ret=$?
	defer_scope_pop

	return ret
}

>>> Going back to the use case variants, there is no much sense to have
>>> push() and pop() dispersed by much from each other, thus I would like
>>> to introduce an API that just combines the two instead:
>>>
>>> new_scope exec_test1 $t
>>> (name discussion below)
>>>
>>>> - defer(): Schedules a defer to the most recently pushed scope (or the
>>>>     default scope if none was pushed. >
>>>> - defer_scopes_cleanup(): Pops any unpopped scopes, including the default
>>>>     one. The selftests that use defer should run this in their cleanup
>>>>     function. This is important to get cleanups of interrupted scripts.
>>>
>>> this should be *the* trap(1)
>>>
>>> with that said, it should be internal to the defer.sh script and it
>>> should be obvious that developers must not introduce their own trap
>>> (as of now we have ~330 in kselftests, ~270 of which in networking)
>> Yeah, we have 100+ tests that use their own traps in forwarding alone.
>> That ship has sailed.
>> I agree that the defer module probably has the "right" to own the exit
>> trap. Any other cleanups can be expressed in terms of defer, and I don't
>> know if there are legitimate uses of exit trap with that taken out. But
>> that's for sometime.
>
> There could be multiple traps for ERR/EXIT/etc conditions, but for
> simplicity it's best to rely on just EXIT trap.
> So we should convert current scripts one by one to use your new API.

I'd just grandfather those in, but having this stuff consolidated would
obviously be nice.

I think in practice we just need to add the trap registration to
forwarding.sh, and per bash script do something like:

-trap cleanup EXIT
 setup_prepare
+defer cleanup
 setup_wait

It should be fairly mechanical most of the time. But the defer stuff works
without it as well, so we can take care of that later on.

>>>>     Consistent use of defers however obviates the need for a separate cleanup
>>>>     function -- everything is just taken care of in defers. So this patch
>>>>     actually introduces a cleanup() helper in the forwarding lib.sh, which
>>>>     calls just pre_cleanup() and defer_scopes_cleanup(). Selftests are
>>>>     obviously still free to override the function.
>>>> - defer_scoped_fn(): Sometimes a function would like to introduce a new
>>>>     defer scope, then run whatever it is that it wants to run, and then pop
>>>>     the scope to run the deferred cleanups. The helper defer_scoped_fn() can
>>>>     be used to derive from one function its wrapper that pushes a defer scope
>>>>     before the function is called, and pops it after it returns.
>>>
>>> It is basically a helper I would like to see as new_scope() mentioned
>>> above, but it takes it upside down - it should really be the caller that
>>> sub-scopes.
>>>
>>> I think that the name of the new_scope() would be better, still concise,
>>> but more precise as:
>>> subscope_defer(),
>>> trapped(), or
>>> sub_trap().
>
> here I mean that "scope" is too broad without the word "trap" or "defer"
> in name
>
>>>
>>> I have no idea how to make a sub-trapped, SIGSEGV isolated scope of bash
>>> execution that has ability to still edit outer scope variables. Perhaps
>>> we could relax the need for edit to have easier implementation? It is
>>> "all ok or failure/rollback" mode of operation anyway most of the time.
>> I'm not sure what you have in mind.
>
> 	foo=1
> 	function bumpfoo {
> 		maybe-crash
> 		foo=2
> 	}
> 	new-defer-scope bumpfoo
> 	echo $foo
>
> do you want this to print 2 or 1?

Oh, that's what you mean by relaxing the edits. Yeah, I think I'd want that
to print 2 if at all possible. I think in_ns() is the only helper that
violates this.
Jakub Kicinski Aug. 27, 2024, 2:17 p.m. UTC | #9
On Tue, 27 Aug 2024 10:53:53 +0200 Petr Machata wrote:
> >> I can do it, but it's gonna be more pain in setting up those
> >> TEST_INCLUDES. People will forget. It will be a nuisance.
> >> I'm thinking of just moving it to net/lib.sh, from forwarding.  
> >
> > what about separate file, but included from net/lib.sh?  
> 
> Unfortunately that would be even worse. Then you need to remember to put
> the file into TEST_INCLUDES despite seemingly not using it.
> 
> Like ideally we'd have automation for this. But I don't know how to do that
> without actually parsing the bash files, and that's just asking for
> trouble. Maybe after the defer stuff we also need a module system :-/

FWIW we could throw it into net/lib, which has a fake target, see:

b86761ff6374 ("selftests: net: add scaffolding for Netlink tests in Python")
Petr Machata Aug. 27, 2024, 3:37 p.m. UTC | #10
Jakub Kicinski <kuba@kernel.org> writes:

> On Tue, 27 Aug 2024 10:53:53 +0200 Petr Machata wrote:
>> >> I can do it, but it's gonna be more pain in setting up those
>> >> TEST_INCLUDES. People will forget. It will be a nuisance.
>> >> I'm thinking of just moving it to net/lib.sh, from forwarding.  
>> >
>> > what about separate file, but included from net/lib.sh?  
>> 
>> Unfortunately that would be even worse. Then you need to remember to put
>> the file into TEST_INCLUDES despite seemingly not using it.
>> 
>> Like ideally we'd have automation for this. But I don't know how to do that
>> without actually parsing the bash files, and that's just asking for
>> trouble. Maybe after the defer stuff we also need a module system :-/
>
> FWIW we could throw it into net/lib, which has a fake target, see:
>
> b86761ff6374 ("selftests: net: add scaffolding for Netlink tests in Python")

Oh, I see net/lib is the default dependency of everything net.
This could work. I'll check it out.
diff mbox series

Patch

diff --git a/tools/testing/selftests/net/forwarding/lib.sh b/tools/testing/selftests/net/forwarding/lib.sh
index 67f38dd1f36b..21cd6a2e3344 100644
--- a/tools/testing/selftests/net/forwarding/lib.sh
+++ b/tools/testing/selftests/net/forwarding/lib.sh
@@ -1369,6 +1369,12 @@  tests_run()
 	done
 }
 
+cleanup()
+{
+	pre_cleanup
+	defer_scopes_cleanup
+}
+
 multipath_eval()
 {
 	local desc="$1"
@@ -1423,6 +1429,83 @@  in_ns()
 	EOF
 }
 
+# map[(defer_scope,cleanup_id) -> cleanup_command]
+declare -A DEFERS
+# map[defer_scope -> # cleanup_commands]
+declare -a NDEFERS=(0)
+DEFER_SCOPE=0
+
+defer_scope_push()
+{
+	((DEFER_SCOPE++))
+	NDEFERS[${DEFER_SCOPE}]=0
+}
+
+defer_scope_pop()
+{
+	local defer_key
+	local defer_ix
+
+	for ((defer_ix=${NDEFERS[${DEFER_SCOPE}]}; defer_ix-->0; )); do
+		defer_key=${DEFER_SCOPE},$defer_ix
+		${DEFERS[$defer_key]}
+		unset DEFERS[$defer_key]
+	done
+
+	NDEFERS[${DEFER_SCOPE}]=0
+	((DEFER_SCOPE--))
+}
+
+defer()
+{
+	local defer_key=${DEFER_SCOPE},${NDEFERS[${DEFER_SCOPE}]}
+	local defer="$@"
+
+	DEFERS[$defer_key]="$defer"
+	NDEFERS[${DEFER_SCOPE}]=$((${NDEFERS[${DEFER_SCOPE}]} + 1))
+}
+
+defer_scopes_cleanup()
+{
+	while ((DEFER_SCOPE >= 0)); do
+		defer_scope_pop
+	done
+}
+
+defer_scoped_fn()
+{
+	local name=$1; shift;
+	local mangle=__defer_scoped__
+
+	declare -f $name >/dev/null
+	if (($?)); then
+		echo "Cannot make non-existent function '$name' defer-scoped" \
+			> /dev/stderr
+		exit 1
+	fi
+
+	declare -f $mangle$name
+	if ((! $?)); then
+		echo "The function '$name' appears to already be defer-scoped" \
+			> /dev/stderr
+		exit 1
+	fi
+
+	eval "$mangle$(declare -f $name)"
+	local body="
+		$name() {
+			local ret;
+			defer_scope_push;
+			$mangle$name \"\$@\";
+			ret=\$?;
+			defer_scope_pop;
+			return \$ret;
+		}
+	"
+	unset $name
+	eval "$body"
+}
+
 ##############################################################################
 # Tests