Thread groups
#############

2021-07-13 - first draft
==========

Objective
---------
- support multi-socket systems with limited cache-line bouncing between
  physical CPUs and/or L3 caches

- overcome the 64-thread limitation

- Support a reasonable number of groups. I.e. if modern CPUs arrive with
  core complexes made of 8 cores, with 8 CC per chip and 2 chips in a
  system, it makes sense to support 16 groups.


Non-objective
-------------
- no need to optimize to the last possible cycle. I.e. some algos like
  leastconn will remain shared across all threads, servers will keep a
  single queue, etc. Global information remains global.

- no stubborn enforcement of FD sharing. Per-server idle connection lists
  can become per-group; listeners can (and should probably) be per-group.
  Other mechanisms (like SO_REUSEADDR) can already overcome this.

- no need to go beyond 64 threads per group.


Identified tasks
================

General
-------
Everywhere tid_bit is used we absolutely need to find a complement using
either the current group or a specific one. Thread debugging will need to
be extended as masks are extensively used.


Scheduler
---------
The global run queue and global wait queue must become per-group. This
means that a task may only be queued into one of them at a time. It
sounds like tasks may only belong to a given group, but doing so would
bring back the original issue that it's impossible to perform remote wake
ups.

We could probably ignore the group if we don't need to set the thread mask
in the task. the task's thread_mask is never manipulated using atomics so
it's safe to complement it with a group.

The sleeping_thread_mask should become per-group. Thus possibly that a
wakeup may only be performed on the assigned group, meaning that either
a task is not assigned, in which case it be self-assigned (like today),
otherwise the tg to be woken up will be retrieved from the task itself.

Task creation currently takes a thread mask of either tid_bit, a specific
mask, or MAX_THREADS_MASK. How to create a task able to run anywhere
(checks, Lua, ...) ?

Profiling
---------
There should be one task_profiling_mask per thread group. Enabling or
disabling profiling should be made per group (possibly by iterating).

Thread isolation
----------------
Thread isolation is difficult as we solely rely on atomic ops to figure
who can complete. Such operation is rare, maybe we could have a global
read_mostly flag containing a mask of the groups that require isolation.
Then the threads_want_rdv_mask etc can become per-group. However setting
and clearing the bits will become problematic as this will happen in two
steps hence will require careful ordering.

FD
--
Tidbit is used in a number of atomic ops on the running_mask. If we have
one fdtab[] per group, the mask implies that it's within the group.
Theoretically we should never face a situation where an FD is reported nor
manipulated for a remote group.

There will still be one poller per thread, except that this time all
operations will be related to the current thread_group. No fd may appear
in two thread_groups at once, but we can probably not prevent that (e.g.
delayed close and reopen). Should we instead have a single shared fdtab[]
(less memory usage also) ? Maybe adding the group in the fdtab entry would
work, but when does a thread know it can leave it ? Currently this is
solved by running_mask and by update_mask. Having two tables could help
with this (each table sees the FD in a different group with a different
mask) but this looks overkill.

There's polled_mask[] which needs to be decided upon. Probably that it
should be doubled as well. Note, polled_mask left fdtab[] for cacheline
alignment reasons in commit cb92f5cae4.

If we have one fdtab[] per group, what *really* prevents from using the
same FD in multiple groups ? _fd_delete_orphan() and fd_update_events()
need to check for no-thread usage before closing the FD. This could be
a limiting factor. Enabling could require to wake every poller.

Shouldn't we remerge fdinfo[] with fdtab[] (one pointer + one int/short,
used only during creation and close) ?

Other problem, if we have one fdtab[] per TG, disabling/enabling an FD
(e.g. pause/resume on listener) can become a problem if it's not necessarily
on the current TG. We'll then need a way to figure that one. It sounds like
FDs from listeners and receivers are very specific and suffer from problems
all other ones under high load do not suffer from. Maybe something specific
ought to be done for them, if we can guarantee there is no risk of accidental
reuse (e.g. locate the TG info in the receiver and have a "MT" bit in the
FD's flags). The risk is always that a close() can result in instant pop-up
of the same FD on any other thread of the same process.

Observations: right now fdtab[].thread_mask more or less corresponds to a
declaration of interest, it's very close to meaning "active per thread". It is
in fact located in the FD while it ought to do nothing there, as it should be
where the FD is used as it rules accesses to a shared resource that is not
the FD but what uses it. Indeed, if neither polled_mask nor running_mask have
a thread's bit, the FD is unknown to that thread and the element using it may
only be reached from above and not from the FD. As such we ought to have a
thread_mask on a listener and another one on connections. These ones will
indicate who uses them. A takeover could then be simplified (atomically set
exclusivity on the FD's running_mask, upon success, takeover the connection,
clear the running mask). Probably that the change ought to be performed on
the connection level first, not the FD level by the way. But running and
polled are the two relevant elements, one indicates userland knowledge,
the other one kernel knowledge. For listeners there's no exclusivity so it's
a bit different but the rule remains the same that we don't have to know
what threads are *interested* in the FD, only its holder.

Not exact in fact, see FD notes below.

activity
--------
There should be one activity array per thread group. The dump should
simply scan them all since the cumuled values are not very important
anyway.

applets
-------
They use tid_bit only for the task. It looks like the appctx's thread_mask
is never used (now removed). Furthermore, it looks like the argument is
*always* tid_bit.

CPU binding
-----------
This is going to be tough. It will be needed to detect that threads overlap
and are not bound (i.e. all threads on same mask). In this case, if the number
of threads is higher than the number of threads per physical socket, one must
try hard to evenly spread them among physical sockets (e.g. one thread group
per physical socket) and start as many threads as needed on each, bound to
all threads/cores of each socket. If there is a single socket, the same job
may be done based on L3 caches. Maybe it could always be done based on L3
caches. The difficulty behind this is the number of sockets to be bound: it
is not possible to bind several FDs per listener. Maybe with a new bind
keyword we can imagine to automatically duplicate listeners ? In any case,
the initially bound cpumap (via taskset) must always be respected, and
everything should probably start from there.

Frontend binding
----------------
We'll have to define a list of threads and thread-groups per frontend.
Probably that having a group mask and a same thread-mask for each group
would suffice.

Threads should have two numbers:
  - the per-process number (e.g. 1..256)
  - the per-group number (1..64)

The "bind-thread" lines ought to use the following syntax:
  - bind 45      ## bind to process' thread 45
  - bind 1/45    ## bind to group 1's thread 45
  - bind all/45  ## bind to thread 45 in each group
  - bind 1/all   ## bind to all threads in group 1
  - bind all     ## bind to all threads
  - bind all/all ## bind to all threads in all groups (=all)
  - bind 1/65    ## rejected
  - bind 65      ## OK if there are enough
  - bind 35-45   ## depends. Rejected if it crosses a group boundary.

The global directive "nbthread 28" means 28 total threads for the process. The
number of groups will sub-divide this. E.g. 4 groups will very likely imply 7
threads per group. At the beginning, the nbgroup should be manual since it
implies config adjustments to bind lines.

There should be a trivial way to map a global thread to a group and local ID
and to do the opposite.


Panic handler + watchdog
------------------------
Will probably depend on what's done for thread_isolate

Per-thread arrays inside structures
-----------------------------------
- listeners have a thr_conn[] array, currently limited to MAX_THREADS. Should
  we simply bump the limit ?
- same for servers with idle connections.
=> doesn't seem very practical.
- another solution might be to point to dynamically allocated arrays of
  arrays (e.g. nbthread * nbgroup) or a first level per group and a second
  per thread.
=> dynamic allocation based on the global number

Other
-----
- what about dynamic thread start/stop (e.g. for containers/VMs) ?
  E.g. if we decide to start $MANY threads in 4 groups, and only use
  one, in the end it will not be possible to use less than one thread
  per group, and at most 64 will be present in each group.


FD Notes
--------
  - updt_fd_polling() uses thread_mask to figure where to send the update,
    the local list or a shared list, and which bits to set in update_mask.
    This could be changed so that it takes the update mask in argument. The
    call from the poller's fork would just have to broadcast everywhere.

  - pollers use it to figure whether they're concerned or not by the activity
    update. This looks important as otherwise we could re-enable polling on
    an FD that changed to another thread.

  - thread_mask being a per-thread active mask looks more exact and is
    precisely used this way by _update_fd(). In this case using it instead
    of running_mask to gauge a change or temporarily lock it during a
    removal could make sense.

  - running should be conditioned by thread. Polled not (since deferred
    or migrated). In this case testing thread_mask can be enough most of
    the time, but this requires synchronization that will have to be
    extended to tgid.. But migration seems a different beast that we shouldn't
    care about here: if first performed at the higher level it ought to
    be safe.

In practice the update_mask can be dropped to zero by the first fd_delete()
as the only authority allowed to fd_delete() is *the* owner, and as soon as
all running_mask are gone, the FD will be closed, hence removed from all
pollers. This will be the only way to make sure that update_mask always
refers to the current tgid.

However, it may happen that a takeover within the same group causes a thread
to read the update_mask late, while the FD is being wiped by another thread.
That other thread may close it, causing another thread in another group to
catch it, and change the tgid and start to update the update_mask. This means
that it would be possible for a thread entering do_poll() to see the correct
tgid, then the fd would be closed, reopened and reassigned to another tgid,
and the thread would see its bit in the update_mask, being confused. Right
now this should already happen when the update_mask is not cleared, except
that upon wakeup a migration would be detected and that would be all.

Thus we might need to set the running bit to prevent the FD from migrating
before reading update_mask, which also implies closing on fd_clr_running() == 0 :-(

Also even fd_update_events() leaves a risk of updating update_mask after
clearing running, thus affecting the wrong one. Probably that update_mask
should be updated before clearing running_mask there. Also, how about not
creating an update on a close ? Not trivial if done before running, unless
thread_mask==0.

###########################################################

Current state:


Mux / takeover / fd_delete() code                |||  poller code
-------------------------------------------------|||---------------------------------------------------
                                                 \|/
mux_takeover():                                   | fd_set_running():
   if (fd_takeover()<0)                           |    old = {running, thread};
     return fail;                                 |    new = {tid_bit, tid_bit};
   ...                                            |
fd_takeover():                                    |    do {
   atomic_or(running, tid_bit);                   |       if (!(old.thread & tid_bit))
   old = {running, thread};                       |          return -1;
   new = {tid_bit, tid_bit};                      |       new = { running | tid_bit, old.thread }	 
   if (owner != expected) {                       |    } while (!dwcas({running, thread}, &old, &new));
      atomic_and(runnning, ~tid_bit);             |
      return -1; // fail                          | fd_clr_running():
   }                                              |    return atomic_and_fetch(running, ~tid_bit);
                                                  |
   while (old == {tid_bit, !=0 })                 | poll():
      if (dwcas({running, thread}, &old, &new)) { |    if (!owner)
         atomic_and(runnning, ~tid_bit);          |       continue;
         return 0; // success                     |
      }                                           |    if (!(thread_mask & tid_bit)) {
   }                                              |       epoll_ctl_del();
                                                  |       continue;
   atomic_and(runnning, ~tid_bit);                |    }
   return -1; // fail                             |
                                                  |    // via fd_update_events()
fd_delete():                                      |    if (fd_set_running() != -1) {
   atomic_or(running, tid_bit);                   |       iocb();
   atomic_store(thread, 0);                       |       if (fd_clr_running() == 0 && !thread_mask)
   if (fd_clr_running(fd) = 0)                    |         fd_delete_orphan();
        fd_delete_orphan();                       |    }


The idle_conns_lock prevents the connection from being *picked* and released
while someone else is reading it. What it does is guarantee that on idle
connections, the caller of the IOCB will not dereference the task's context
while the connection is still in the idle list, since it might be picked then
freed at the same instant by another thread. As soon as the IOCB manages to
get that lock, it removes the connection from the list so that it cannot be
taken over anymore. Conversely, the mux's takeover() code runs under that
lock so that if it frees the connection and task, this will appear atomic
to the IOCB. The timeout task (which is another entry point for connection
deletion) does the same. Thus, when coming from the low-level (I/O or timeout):
  - task always exists, but ctx checked under lock validates; conn removal
    from list prevents takeover().
  - t->context is stable, except during changes under takeover lock. So
    h2_timeout_task may well run on a different thread than h2_io_cb().

Coming from the top:
  - takeover() done under lock() clears task's ctx and possibly closes the FD
    (unless some running remains present).

Unlikely but currently possible situations:
  - multiple pollers (up to N) may have an idle connection's FD being
    polled, if the connection was passed from thread to thread. The first
    event on the connection would wake all of them. Most of them would
    see fdtab[].owner set (the late ones might miss it). All but one would
    see that their bit is missing from fdtab[].thread_mask and give up.
    However, just after this test, others might take over the connection,
    so in practice if terribly unlucky, all but 1 could see their bit in
    thread_mask just before it gets removed, all of them set their bit
    in running_mask, and all of them call iocb() (sock_conn_iocb()).
    Thus all of them dereference the connection and touch the subscriber
    with no protection, then end up in conn_notify_mux() that will call
    the mux's wake().

  - multiple pollers (up to N-1) might still be in fd_update_events()
    manipulating fdtab[].state. The cause is that the "locked" variable
    is determined by atleast2(thread_mask) but that thread_mask is read
    at a random instant (i.e. it may be stolen by another one during a
    takeover) since we don't yet hold running to prevent this from being
    done. Thus we can arrive here with thread_mask==something_else (1bit),
    locked==0 and fdtab[].state assigned non-atomically.

  - it looks like nothing prevents h2_release() from being called on a
    thread (e.g. from the top or task timeout) while sock_conn_iocb()
    dereferences the connection on another thread. Those killing the
    connection don't yet consider the fact that it's an FD that others
    might currently be waking up on.

###################

pb with counter:

users count doesn't say who's using the FD and two users can do the same
close in turn. The thread_mask should define who's responsible for closing
the FD, and all those with a bit in it ought to do it.


2021-08-25 - update with minimal locking on tgid value
==========

  - tgid + refcount at once using CAS
  - idle_conns lock during updates
  - update:
    if tgid differs => close happened, thus drop update
    otherwise normal stuff. Lock tgid until running if needed.
  - poll report:
    if tgid differs => closed
    if thread differs => stop polling (migrated)
    keep tgid lock until running
  - test on thread_id:
    if (xadd(&tgid,65536) != my_tgid) {
      // was closed
      sub(&tgid, 65536)
      return -1
    }
    if !(thread_id & tidbit) => migrated/closed
    set_running()
    sub(tgid,65536)
  - note: either fd_insert() or the final close() ought to set
    polled and update to 0.

2021-09-13 - tid / tgroups etc.
==========

  - tid currently is the thread's global ID. It's essentially used as an index
    for arrays. It must be clearly stated that it works this way.

  - tid_bit makes no sense process-wide, so it must be redefined to represent
    the thread's tid within its group. The name is not much welcome though, but
    there are 286 of it that are not going to be changed that fast.

  - just like "ti" is the thread_info, we need to have "tg" pointing to the
    thread_group.

  - other less commonly used elements should be retrieved from ti->xxx. E.g.
    the thread's local ID.

  - lock debugging must reproduce tgid

  - an offset might be placed in the tgroup so that even with 64 threads max
    we could have completely separate tid_bits over several groups.

2021-09-15 - bind + listen() + rx
==========

  - thread_mask (in bind_conf->rx_settings) should become an array of
    MAX_TGROUP longs.
  - when parsing "thread 123" or "thread 2/37", the proper bit is set,
    assuming the array is either a contiguous bitfield or a tgroup array.
    An option RX_O_THR_PER_GRP or RX_O_THR_PER_PROC is set depending on
    how the thread num was parsed, so that we reject mixes.
  - end of parsing: entries translated to the cleanest form (to be determined)
  - binding: for each socket()/bind()/listen()... just perform one extra dup()
    for each tgroup and store the multiple FDs into an FD array indexed on
    MAX_TGROUP. => allows to use one FD per tgroup for the same socket, hence
    to have multiple entries in all tgroup pollers without requiring the user
    to duplicate the bind line.

2021-09-15 - global thread masks
==========

Some global variables currently expect to know about thread IDs and it's
uncertain what must be done with them:
  - global_tasks_mask  /* Mask of threads with tasks in the global runqueue */
    => touched under the rq lock. Change it per-group ? What exact use is made ?

  - sleeping_thread_mask /* Threads that are about to sleep in poll() */
    => seems that it can be made per group

  - all_threads_mask: a bit complicated, derived from nbthread and used with
    masks and with my_ffsl() to wake threads up. Should probably be per-group
    but we might miss something for global.

  - stopping_thread_mask: used in combination with all_threads_mask, should
    move per-group.

  - threads_harmless_mask: indicates all threads that are currently harmless in
    that they promise not to access a shared resource. Must be made per-group
    but then we'll likely need a second stage to have the harmless groups mask.
    threads_idle_mask, threads_sync_mask, threads_want_rdv_mask go with the one
    above. Maybe the right approach will be to request harmless on a group mask
    so that we can detect collisions and arbiter them like today, but on top of
    this it becomes possible to request harmless only on the local group if
    desired. The subtlety is that requesting harmless at the group level does
    not mean it's achieved since the requester cannot vouch for the other ones
    in the same group.

In addition, some variables are related to the global runqueue:
  __decl_aligned_spinlock(rq_lock); /* spin lock related to run queue */
  struct eb_root rqueue;      /* tree constituting the global run queue, accessed under rq_lock */
  unsigned int grq_total;     /* total number of entries in the global run queue, atomic */
  static unsigned int global_rqueue_ticks;  /* insertion count in the grq, use rq_lock */

And others to the global wait queue:
  struct eb_root timers;      /* sorted timers tree, global, accessed under wq_lock */
  __decl_aligned_rwlock(wq_lock);   /* RW lock related to the wait queue */
  struct eb_root timers;      /* sorted timers tree, global, accessed under wq_lock */


2021-09-29 - group designation and masks
==========

Neither FDs nor tasks will belong to incomplete subsets of threads spanning
over multiple thread groups. In addition there may be a difference between
configuration and operation (for FDs). This allows to fix the following rules:

  group  mask   description
    0     0     bind_conf: groups & thread not set. bind to any/all
                task: it would be nice to mean "run on the same as the caller".

    0    xxx    bind_conf: thread set but not group: thread IDs are global
                FD/task: group 0, mask xxx

    G>0   0     bind_conf: only group is set: bind to all threads of group G
                FD/task: mask 0 not permitted (= not owned). May be used to
                mention "any thread of this group", though already covered by
                G/xxx like today.

    G>0  xxx    bind_conf: Bind to these threads of this group
                FD/task: group G, mask xxx

It looks like keeping groups starting at zero internally complicates everything
though. But forcing it to start at 1 might also require that we rescan all tasks
to replace 0 with 1 upon startup. This would also allow group 0 to be special and
be used as the default group for any new thread creation, so that group0.count
would keep the number of unassigned threads. Let's try:

  group  mask   description
    0     0     bind_conf: groups & thread not set. bind to any/all
                task: "run on the same group & thread as the caller".

    0    xxx    bind_conf: thread set but not group: thread IDs are global
                FD/task: invalid. Or maybe for a task we could use this to
                mean "run on current group, thread XXX", which would cover
                the need for health checks (g/t 0/0 while sleeping, 0/xxx
                while running) and have wake_expired_tasks() detect 0/0 and
                wake them up to a random group.

    G>0   0     bind_conf: only group is set: bind to all threads of group G
                FD/task: mask 0 not permitted (= not owned). May be used to
                mention "any thread of this group", though already covered by
                G/xxx like today.

    G>0  xxx    bind_conf: Bind to these threads of this group
                FD/task: group G, mask xxx

With a single group declared in the config, group 0 would implicitly find the
first one.


The problem with the approach above is that a task queued in one group+thread's
wait queue could very well receive a signal from another thread and/or group,
and that there is no indication about where the task is queued, nor how to
dequeue it. Thus it seems that it's up to the application itself to unbind/
rebind a task. This contradicts the principle of leaving a task waiting in a
wait queue and waking it anywhere.

Another possibility might be to decide that a task having a defined group but
a mask of zero is shared and will always be queued into its group's wait queue.
However, upon expiry, the scheduler would notice the thread-mask 0 and would
broadcast it to any group.

Right now in the code we have:
  - 18 calls of task_new(tid_bit)
  - 18 calls of task_new(MAX_THREADS_MASK)
  - 2 calls with a single bit

Thus it looks like "task_new_anywhere()", "task_new_on()" and
"task_new_here()" would be sufficient.
