[SPARK-3795] Heuristics for dynamically scaling executors

This is part of a bigger effort to provide elastic scaling of executors within a Spark application ([SPARK-3174](https://issues.apache.org/jira/browse/SPARK-3174)). This PR does not provide any functionality by itself; it is a skeleton that is missing a mechanism to be added later in [SPARK-3822](https://issues.apache.org/jira/browse/SPARK-3822).

Comments and feedback are most welcome. For those of you reviewing this in detail, I highly recommend doing it through your favorite IDE instead of through the diff here.
I understand that. In fact `YarnAllocator` already has a thread that periodically heartbeats requests to the RM. The acknowledgment here refers to the `AM` acknowledging that it has received this request from the driver, but will asynchronously forward the request to the `RM` later. The assumption here is that once the `AM` receives the request, it is guaranteed to eventually forward it to the `RM` after translating it according to the AM-RM interface (through some retry logic already handled in `YarnAllocator`). In other words, in Yarn mode the driver treats the `AM/RM` pair as the "cluster manager", and all it wants is an acknowledgment that the request has been received (but not necessarily fulfilled).