-
Notifications
You must be signed in to change notification settings - Fork 2.1k
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
threadpool: throttled big group #1778
Conversation
src/util/threadpool.rs
Outdated
use std::marker::PhantomData; | ||
|
||
pub struct Task<T> { | ||
// The task's number in the pool.Each task has a unique number, |
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.
space after period
|
||
impl<T> Ord for Task<T> { | ||
fn cmp(&self, right: &Task<T>) -> Ordering { | ||
self.id.cmp(&right.id).reverse() |
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.
reverse ordering?
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.
We reverse ordering here since the heap
would pops the largest item number first while we need to pop the task with the smallest id
first. @andelf
src/util/threadpool.rs
Outdated
BigGroupThrottledQueue { | ||
group_concurrency: HashMap::default(), | ||
waiting_queue: HashMap::default(), | ||
pending_tasks: BinaryHeap::new(), |
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.
consistency between new()
and default()
.
maybe new()
is better.
src/util/threadpool.rs
Outdated
group_concurrency: HashMap::default(), | ||
waiting_queue: HashMap::default(), | ||
pending_tasks: BinaryHeap::new(), | ||
group_concurrency_on_busy: group_concurrency_on_busy, |
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.
s/on/when/
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.
Since on
share the same meaning with when
here, and on
is more short. I prefer on
here @andelf
src/util/threadpool.rs
Outdated
} | ||
} | ||
|
||
// Try push into pending. Return none on success,return Some(task) on failed. |
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.
logically wrong usage for Option. you may use Result<(), ...>
instead.
btw, is this thread-safe?
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.
Only one thread can own the lock of BigGroupThrottledQueue
at the same time. @andelf
src/util/threadpool.rs
Outdated
} | ||
|
||
/// `ThreadPool` is used to execute tasks in parallel. | ||
/// Each task would be pushed into the pool,and when a thread |
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.
space after comma
src/util/threadpool.rs
Outdated
} | ||
|
||
ThreadPool { | ||
meta: meta.clone(), |
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.
is clone()
necessary?
src/util/threadpool.rs
Outdated
// return false when get stop msg | ||
#[inline] | ||
fn wait(&self) -> bool { | ||
// try to receive notify |
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.
notification
src/util/threadpool.rs
Outdated
|
||
fn run(&mut self) { | ||
// start the worker. | ||
// loop break on receive stop message. |
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.
breaks
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.
receiving
src/util/threadpool.rs
Outdated
// loop break on receive stop message. | ||
while self.wait() { | ||
// handle task | ||
// since `tikv` would be down on any panic happens, |
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.
do not format tikv since it's not a function, variable or type
src/util/threadpool.rs
Outdated
for (group_id, tasks) in &self.waiting_queue { | ||
let front_task_id = tasks[0].id; | ||
assert!(self.group_concurrency.contains_key(group_id)); | ||
let count = self.group_concurrency[group_id]; |
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.
can we ensure the group_id exist here?
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.
Yes. The task should be pushed into pending_tasks
if group_id
not in self.group_concurrency
or self.group_concurrency[group_id]<group_concurrency_on_busy
. So we can ensure the group_id exist here @siddontang
src/util/threadpool.rs
Outdated
} | ||
|
||
#[inline] | ||
fn pop_task_from_waiting_queue(&mut self) -> Option<Task<T>> { |
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.
pop_from_waiting_queue
src/util/threadpool.rs
Outdated
pub trait ScheduleQueue<T> { | ||
fn pop(&mut self) -> Option<Task<T>>; | ||
fn push(&mut self, task: Task<T>); | ||
fn finish(&mut self, group_id: T); |
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 does finish mean here?
src/util/threadpool.rs
Outdated
|
||
// each thread has a worker. | ||
struct Worker<Q, T> { | ||
job_rever: Arc<Mutex<Receiver<bool>>>, |
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.
Use Mutex + Condvar
instead.
src/util/threadpool.rs
Outdated
} | ||
} | ||
|
||
struct ThreadPoolMeta<Q, T> { |
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.
TaskPool
seems more appropriate to me.
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
|
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.
The paragraph "Unless required ... under the License" is divided into several rows.
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.
we divide it into several rows in order to make a line not too long. It's ok in source code @Wenting0905
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.
oh i see
src/util/threadpool.rs
Outdated
// group_id => running_num+pending num. It means there may | ||
// `group_concurrency[group_id]` tasks of the group are running. | ||
group_concurrency: HashMap<T, usize>, | ||
// max num of threads each group can run on when pool is busy. |
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.
The max number of threads that each group can run when the pool is busy.
src/util/threadpool.rs
Outdated
// `group_concurrency[group_id]` tasks of the group are running. | ||
group_concurrency: HashMap<T, usize>, | ||
// max num of threads each group can run on when pool is busy. | ||
// each value in group_concurrency shouldn't bigger than this value. |
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.
Each value in 'group_concurrency' shouldn't be bigger than this value.
src/util/threadpool.rs
Outdated
} | ||
let group_id = group_id.unwrap(); | ||
let task = self.pop_from_waiting_queue_with_group_id(&group_id); | ||
// update group_concurrency since current task is going to run. |
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.
since the current task
src/util/threadpool.rs
Outdated
let task = waiting_tasks.pop_front().unwrap(); | ||
(waiting_tasks.is_empty(), task) | ||
}; | ||
// if waiting tasks for group is empty, remove from waiting_tasks. |
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.
remove it
src/util/threadpool.rs
Outdated
task | ||
} | ||
|
||
// pop_group_id_from_waiting_queue returns next task's group_id. |
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.
returns the next
src/util/threadpool.rs
Outdated
}); | ||
} | ||
|
||
// push 2 txn3 into pool, each need 2*sleep_duration. |
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.
push 2 txn3 into pool and each needs 2*sleep_duration.
src/util/threadpool.rs
Outdated
} | ||
|
||
// txn11,txn12,txn13,txn14,txn21,txn22,txn31,txn32 | ||
// first 4 task during [0,sleep_duration] should be |
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.
first 4 tasks
src/util/threadpool.rs
Outdated
|
||
// txn11,txn12,txn13,txn14,txn21,txn22,txn31,txn32 | ||
// first 4 task during [0,sleep_duration] should be | ||
// {txn11,txn12,txn21,txn22}.Since txn1 finished before than txn2, |
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.
- space after period
- Since txn1 is finished before txn2,
src/util/threadpool.rs
Outdated
// txn11,txn12,txn13,txn14,txn21,txn22,txn31,txn32 | ||
// first 4 task during [0,sleep_duration] should be | ||
// {txn11,txn12,txn21,txn22}.Since txn1 finished before than txn2, | ||
// 4 task during [sleep_duration,2*sleep_duration] should be |
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.
4 tasks
src/util/threadpool.rs
Outdated
fn test_fair_group_queue() { | ||
let max_pending_task_each_group = 2; | ||
let mut queue = BigGroupThrottledQueue::new(max_pending_task_each_group); | ||
// push 4 group1 into queue |
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.
delete one space before 4
src/util/threadpool.rs
Outdated
} | ||
while let Some(t) = self.threads.pop() { | ||
if let Err(e) = t.join() { | ||
return Err(format!("{:?}", e)); |
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 about other threads?
PTAL |
src/util/threadpool.rs
Outdated
// The task's number in the pool. Each task has a unique number, | ||
// and it's always bigger than preceding ones. | ||
id: u64, | ||
// the task's group_id. |
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.
Group which the task belongs to.
src/util/threadpool.rs
Outdated
waiting_queue: HashMap<T, VecDeque<Task<T>>>, | ||
// group_id => running_num+pending num. It means there may | ||
// `group_concurrency[group_id]` tasks of the group are running. | ||
group_concurrency: HashMap<T, usize>, |
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.
s/group_concurrency/group_concurrency_stat
src/util/threadpool.rs
Outdated
while let Some(task) = self.get_next_task() { | ||
// handle task | ||
// since tikv would be down when any panic happens, | ||
// we do't need to process panic case here. |
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.
s/do't/don't/
src/util/threadpool.rs
Outdated
// handle task | ||
// since tikv would be down when any panic happens, | ||
// we do't need to process panic case here. | ||
task.task.call_box(()); |
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.
Prefer (task.task)()
.
src/util/threadpool.rs
Outdated
// we do't need to process panic case here. | ||
task.task.call_box(()); | ||
self.on_task_finished(&task.group_id); | ||
self.task_count.fetch_sub(1, AOrdering::SeqCst); |
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's A
?
src/util/threadpool.rs
Outdated
builder = builder.name(name.clone()); | ||
let tasks = task_pool.clone(); | ||
let task_num = task_count.clone(); | ||
let thread = builder.spawn(move || { |
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.
Chain L306, L307 with L310.
src/util/threadpool.rs
Outdated
|
||
fn pop(&mut self) -> Option<Task<T>> { | ||
if let Some(task) = self.pending_tasks.pop() { | ||
let count = self.group_concurrency.entry(task.group_id.clone()).or_insert(0); |
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.
Seems always exist.
src/util/threadpool.rs
Outdated
let mut next_group = None; | ||
for (group_id, tasks) in &self.waiting_queue { | ||
let front_task_id = tasks[0].id; | ||
assert!(self.group_concurrency.contains_key(group_id)); |
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.
Unnecessary.
src/util/threadpool.rs
Outdated
// (group_id,count,task_id) the best current group's info with it's group_id, | ||
// running tasks count, front task's id in waiting queue. | ||
let mut next_group = None; | ||
for (group_id, tasks) in &self.waiting_queue { |
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.
Use Iterator::min
instead.
src/util/threadpool.rs
Outdated
|
||
pub struct Task<T> { | ||
// The task's number in the pool. Each task has a unique number, | ||
// and it's always bigger than preceding ones. |
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.
s/task number/tast id/
It will be fine to use just task id.
} | ||
} | ||
|
||
impl<T> Eq for Task<T> {} |
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.
Why the implementation is empty?
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.
src/util/threadpool.rs
Outdated
// `BigGroupThrottledQueue` tries to throttle group's concurrency to | ||
// `group_concurrency_on_busy` when it's busy. | ||
// When one worker asks a task to run, it schedules in the following way: | ||
// 1. Find out which group has a running number that is smaller than |
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.
Please take a look at these comments. @Wenting0905
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.
LGTM
src/util/threadpool.rs
Outdated
// more than `group_concurrency_on_busy`), the rest of the group's tasks | ||
// would be pushed into `waiting_queue[group_id]` | ||
waiting_queue: HashMap<T, VecDeque<Task<T>>>, | ||
// group_id => running_num+pending num. It means there may |
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.
there may?
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.
Yes. This is a subtle optimization to improve the efficiency in schedule next task. We may need waiting_queue
only in the normal implementation, while it always need to iterator all groups in waiting_queue
to find the optimal task.
In this implementation, we add a pending_heap, when a new task comes:
- If the total number of the group's tasks in pending_heap or running is smaller than
group_concurrency_on_busy
, push it into pending heap. - Otherwise, push the task into waiting_queue.
And when try to get a new task to run:
- If the heap is not empty, pop the task in front.
- Otherwise, find the optimal task according our rules in
waiting_queue
.
Here group_concurrency[group_id]
save the total number of tasks which are in pending_tasks
(pending_heap) or running. It also means there may group_concurrency[group_id]
tasks of the group are running. @hhkbp2
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.
Sorry for the crudeness of previous comment. :)
It meant to point out that there may be a syntax error for the comment. Try to revise it like
"It means at most group_concurrency[group_id]
tasks of the group may be running."
LGTM |
src/util/threadpool.rs
Outdated
} | ||
|
||
// `BigGroupThrottledQueue` tries to throttle group's concurrency to | ||
// `group_concurrency_on_busy` when is busy. |
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.
when it's busy
src/util/threadpool.rs
Outdated
.map(|(group_id, waiting_queue)| { | ||
(self.group_concurrency[group_id], waiting_queue[0].id, group_id) | ||
}) | ||
.min(); |
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.
FYI, it could use data structure like SkipList/Heap which is fast for insertion/deletion/ordered access to track the relationship of (lowest concurrency, low id) -> group_id, to avoid the iteration.
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.
Don't make it more complicated, ordermap can satisfy the need.
src/util/threadpool.rs
Outdated
// `group_concurrency_on_busy`(which means the number of on-going tasks is | ||
// more than `group_concurrency_on_busy`), the rest of the group's tasks | ||
// would be pushed into `waiting_queue[group_id]` | ||
waiting_queue: HashMap<T, VecDeque<Task<T>>>, |
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.
Maybe we should name waiting_queue
to big_task_waiting_queue
. I always confused by the pending_tasks
and waiting_queue
.
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.
Maybe queue1
queue2
is more clear.
src/util/threadpool.rs
Outdated
|
||
#[inline] | ||
fn pop_from_waiting_queue_with_group_id(&mut self, group_id: &T) -> Task<T> { | ||
let (waiting_tasks_is_empty, task) = { |
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.
s/waiting_tasks_is_empty/empty_after_pop
src/util/threadpool.rs
Outdated
} | ||
|
||
#[inline] | ||
fn pop_from_waiting_queue_with_group_id(&mut self, group_id: &T) -> Task<T> { |
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.
s/with/by/
src/util/threadpool.rs
Outdated
|
||
struct TaskPool<Q, T> { | ||
next_task_id: u64, | ||
tasks: Q, |
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.
s/tasks/task_queue
src/util/threadpool.rs
Outdated
} | ||
} | ||
|
||
|
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.
remove blank line
src/util/threadpool.rs
Outdated
pub fn execute<F>(&mut self, group_id: T, job: F) | ||
where F: FnOnce() + Send + 'static | ||
{ | ||
self.task_count.fetch_add(1, AtomicOrdering::SeqCst); |
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.
Move this line after L309.
src/util/threadpool.rs
Outdated
} | ||
if let Some(task) = task_pool.pop_task() { | ||
// to reduce lock's time. | ||
task_pool.on_task_started(&task.group_id); |
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.
Please add comments why call on_task_started at here not before L387.
-> Worker<Q, T> { | ||
Worker { | ||
task_pool: task_pool, | ||
task_count: task_count, |
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.
Seems task_count
is not used, can we drop it?
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.
task_count
is needed when one task is finished. @zhangjinpeng1987
src/util/threadpool.rs
Outdated
group_concurrency: HashMap::new(), | ||
waiting_queue: HashMap::new(), | ||
pending_tasks: BinaryHeap::new(), | ||
group_concurrency_on_busy: group_concurrency_on_busy, |
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.
s/group_concurrency_on_busy/group_concurrency_limit
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.
Who is busy?
src/util/threadpool.rs
Outdated
if statistics.total() >= self.group_concurrency_limit { | ||
return Err(PushError(task)); | ||
} | ||
statistics.queue1_count += 1; |
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.
Move this line below L144
src/util/threadpool.rs
Outdated
} | ||
|
||
#[inline] | ||
fn pop_from_waiting_queue_by_group_id(&mut self, group_id: &T) -> Task<T> { |
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.
pop_from_queue2_by_group_id
src/util/threadpool.rs
Outdated
// Try push into high priority queue. Return none on success,return PushError(task) on failed. | ||
#[inline] | ||
fn try_push_into_high_pri_queue(&mut self, task: Task<T>) -> Result<(), PushError<Task<T>>> { | ||
let statistics = self.group_statistics |
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.
let mut statistics.. ?
src/util/threadpool.rs
Outdated
// If the value of `group_statistics[group_id]` is not big enough, pop | ||
// a task from `low_pri_queue[group_id]` and push it into `high_pri_queue`. | ||
let group_task = self.pop_from_low_pri_queue_by_group_id(group_id); | ||
assert!(self.try_push_into_high_pri_queue(group_task).is_ok()); |
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.
unwrap
LGTM @hhkbp2 PTAL again. |
src/util/threadpool.rs
Outdated
|
||
struct GroupStatisticsItem { | ||
running_count: usize, | ||
high_pri_queue_count: usize, |
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.
s/pri/priority/
Use full name unless it's really too long.
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.
= =
LGTM |
Hi all,
This PR creates a new
threadpool
which tries to throttle the group's concurrency to a specified number when it's busy.Each task uses the attribute
group_id
to identify which group it belongs to. When one thread asks a new task to run, it schedules according to the following rules:group_concurrency_on_busy
.If no group meets the first point, choose according to the following rules:
@BusyJay @zhangjinpeng1987 @disksing PTAL