Event(3)
π¦ Event(3) π¦
okay so this is the Event(3) page from the User Contributed Perl Documentation, and it is literally the entire guide to event loop processing. grab a strawberry drink bestie, we are going in π
β¨ NAME β¨
Event β event loop processing
β¨ SYNOPSIS β¨
use Event qw(loop unloop);
# initialize application
Event->flavor(attribute => value, ...);
my $ret = loop();
# and some callback will call
unloop('ok');
β¨ DESCRIPTION β¨
π¨ ALERT: Marc Lehmann may have taken over the future of event loops in Perl. Check out his libev library and EV Perl module. 25 Aug 2009
not the drama right at the top π but fr, that's the note, word for word, dated 25 Aug 2009.
The Event module gives you one central facility to watch for all kinds of events and to invoke a callback when those events occur. The whole idea is to delay handling events so they can be dispatched in priority order at a moment when it's actually safe for callbacks to execute. main character scheduling energy, honestly β¨
Events (the normal-English kind) get detected by watchers, which reify them into events (the special Event-module kind). To keep it clear, the first kind may be called "source events" and the second kind "target events". Source events β like a signal arriving β happen whether or not anyone's watching them. If a source event occurs while a watcher is actively watching, then that watcher generates a corresponding target event. Target events are only created by watchers. And if several watchers are interested in the same source event, each one generates its own target event β so any particular source event may result in zero, one, two, or literally any number of target events, matching exactly the number of watchers that were actively watching for it π
Target events are queued to be processed in priority order (priority being determined by the creating watcher) and in FIFO order among events of the same priority. Queued ("pending") events can, in some cases, be cancelled before they get processed. A queued event is processed by being passed to the callback function (or method on a particular object or class) that was specified to the watcher.
A watcher, once created, operates autonomously β the Event user doesn't have to retain any reference to it. That said, keeping a reference makes it possible to modify most of the watcher's characteristics. A watcher can be switched between active and inactive states, and when it's inactive it does not generate target events. slay π
Some types of source event are not reified as target events immediately. Signals received, for example, are counted initially, and the counted signals get reified at certain execution points. So signal events may be processed out of order, and β if handled carelessly β on the wrong side of a state change in event handling π. A useful way to view this: the occurrence of the source event is not actually the arrival of the signal, it's triggered by the counting of the signal.
Reification can be forced when necessary. The schedule on which some other events are created is non-obvious, especially with watchers that watch for a condition rather than an event. In some cases, target events are generated on a schedule that depends on the operation of the event loop. iykyk π
πΈ PERL API πΈ
Events (the occurrence of such) are noticed and queued by 'event watchers'. The creation and configuration of event watchers is the primary topic of the rest of this document.
The following functions control or interrogate the event loop as a whole:
$result = loop([$timeout])β enters a loop that callsone_event()untilunloop()is called. Whatever you pass tounloop()is the return value ofloop(). Loops can be nested, bestie β it's loops all the way down πͺunloop($result)β makes the inner-mostloop()return with$result.unloop_all($result)β causes all pendingloop()s to return immediately. This is not implemented with"die". It works as ifunloop($result)were called for all nested loops.sweep([$max_prio])β queues all pending events and dispatches any with priority strictly less than$max_prio(the highest priority is 0). The default is to process all events except idle events. (While idle events are ignored bysweep, idle watchers are not ignored. If you want to avoid triggering an idle watcher then set"max"to"undef"orstop()it.)one_event([$timeout])β if any events are outstanding then invoke the corresponding callback of the highest priority event. If there are no events available, block forever or until$timeout. Use of this API is not recommended because it is not efficient and does not trap exceptions π. However, you might wish to understand how it works:- Queue asyncronous events (signals, etc). That is, previously recorded events are reified.
- If there are any events with priority 5 or less (see
StarvePrio) then service the next one and return. - Calculate the maximum wait time (minimum time till the next timer expiration) and pass control to the poll/select system call. Upon return, queue all pending events.
- Queue asyncronous events again.
- If there are any events then service the next one and return.
- Service the next idle watcher.
StarvePrio is the priority level for which events are dispatched during step 2. It cannot be changed without a recompile. In the rare case that an event is always pending at step 2 then I/O watchers will starve β however, this is highly unlikely since async watchers should never queue events so rapidly π
all_watchers()β returns a list of all watchers (including stopped watchers).all_running()β returns a list of all watchers with actively running callbacks. Watchers are returned in order of most recent to least recent.all_idle()β returns a list of all the idle watchers. If the event queue is very busy, all the idle watchers will sit on the idle queue waiting to run. But be aware: if an idle watcher has the"max"attribute set, then it will queue a normal event when its"max"wait time is exceeded.queue_pending()β examines asynchronous source events (timers & signals) and reifies them as target events.queue_pending()is only called implicitly bysweep()andone_event(). Otherwise,queue_pending()is not called implicitly.
π NOTE: Signal watchers generate target events according to which watchers are active at the time thatqueue_pending()is called, rather than according to the time the signal is received. This is best explained by example β see the filedemo/queue_pending.t.
π Event Watcher Constructors π
All watchers are constructed in one of the following ways:
$w = Event->flavor( [attr1 => $value,]... );
$w = Event::flavor($Class, [attr1 => $value,]...);
$w = Event::flavor->new([attr1 => $value,]...);
Where flavor is substituted with the kind of watcher. Built-in types include idle, io, signal, timer, and var.
New watchers (hopefully) have reasonable defaults and can also be customized by passing extra attributes to the constructor. When created, watcher objects are "started" and are waiting for events (see "$event->start" below).
NetServer::Portal can display watchers in real-time, formatted similarly to the popular "top" program. You may find this a useful aide for debugging π
π Shared Watcher Attributes π
Watchers are configured with attributes (also known as properties). For example:
$watcher->cb(\&some_code); # set callback
warn $event->w->desc.": ".$event->hits." events happened; Wow!";
All watchers support the following attributes: cb, cbtime, debug, desc, prio, max_cb_tm, reentrant, and repeat. Watcher constructors accept the preceding and additionally: async and nice. Moreover, watchers also offer extra attributes according to their specialty β¨
π Shared Watcher Methods π
The following methods are available for all watchers:
$watcher->startβ activate the watcher. Watchers refuse tostart()without sufficient configuration information to generate events. Constructors always invokestart()unless the"parked=>1"option is requested. You will need to set the parked option if you preallocate unconfigured watchers.
Note: if there are any unreified asynchronous events that are of interest to the watcher, it will see these events even though they happened before it was started. This affects signal watchers, but there will only be existing unreified signal events if Event was already handling the signal β which it would only do if there were another active watcher for the same signal. If this situation might occur, and it would be a problem for the new watcher to see older events, call queue_pending() immediately before starting the new watcher in order to reify any outstanding events. This explanation may be more clear if read along with demo/queue_pending.t π«Ά
$watcher->againβ this is the same as the"start"except if a watcher has special repeat behavior. For example, repeating timers recalculate their alarm time using the"interval"parameter.$watcher->nowβ cause the watcher to generate an event, even if it is stopped. The callback may or may not run immediately depending upon the event's priority. If you must unconditionally invoke the callback, consider something like
$w->cb->($w);
$watcher->stopβ don't look for events any more. Running events are allowed to complete but pending events are cancelled. Note that a stopped watcher can be reactivated by calling the"start"or"again"methods.
Watchers are stopped implicitly if their new configuration deprives them of the ability to generate events. For instance:
my $io_watcher = Event->io(timeout => 1); # started
$io_watcher->timeout(undef); # stopped implicitly
$io_watcher->timeout(1); # still stopped
$io_watcher->start; # restarted
$watcher->cancelβ stop and destroy$watcher. Running events are allowed to complete but pending events are cancelled. Cancelled watchers are no longer valid except for read-only operations. For example,prio()can return the watcher's priority, butstart()will fail π$watcher->is_cancelledβ reports whether the$watcherhas been cancelled.$watcher->is_activeβ reports whether the$watcherhas been started. The return value is not affected by suspend.$watcher->is_runningβ zero if the callback is not running. Otherwise, the number of levels that the callback has been entered. This can be greater than one if a"reentrant"callback invokes"loop"(or"sweep", with lesser probability) π$watcher->is_suspendedβ tells you whether$watcheris currently suspended. Real talk: suspension is a debugging feature, so go read the wholesuspendattribute discussion below before you get any ideas π$watcher->pendingβ in scalar context it returns a boolean, like, "does this watcher have any events chillin' in the event queue, yes or no". In array context it returns a list of all of the watcher's pending events β¨
Heads up bestie π«Ά: this does not check for unreified asynchronous events. Call queue_pending() first if you want to see signals received since the last operation of the event loop. iykyk.
π¦ Watcher Types π¦
idle
Extra attributes: min => $seconds, max => $seconds
Watches for the Event system to be idle, like, to have literally no events pending. If the system is never idle, an event will be generated at least every max seconds. And while Event is idle, events get generated not more often than min seconds β it's giving "respect the boundaries" π
If neither min nor max is specified, the watcher defaults to one-shot behaviour (repeat false), otherwise it defaults to repeating. Either way, you can override the default by specifying a repeat attribute. min defaults to 0.01, and max defaults to infinity. Slay.
var
Extra attributes: var => \$var, poll => 'rw'
Var watchers generate events when the given variable is read from or written to, as specified by poll. And poll defaults to w. β
Since perl is a concise language, it's often genuinely hard to predict when a variable will be read. For this reason, variable watchers should poll only for writes unless you know what you are doing. No cap, just don't.
timer
Extra attributes: at => $time, after => $sec, interval => $sec, hard => $bool
Generate events at particular times πΈ The $time and $sec are in seconds. Fractional seconds may be used if Time::HiRes is available. at and after are mutually exclusive.
at or after specify the initial time that the event will trigger. Subsequent timer events occur at intervals specified by interval or after (in that order of preference) if either was supplied. The timer defaults to one-shot behaviour if interval was not specified, or repeating behaviour if interval was specified; in either case this can be overridden by providing repeat explicitly.
You need to know the time at the start of today if you're trying to set timers to trigger at day relative times. You can find it with:
use Time::Local;
my $TodaySeconds = int timelocal(0,0,0,(localtime)[3,4,5]);
This calculation may seem a little heavy weight. If you want to use UTC rather than local time then you can use this instead:
my $TodaySeconds = time - time % 86400;
Beware π: because of lags in the event loop, theintervaltimeout may already be in the past. If thehardflag is set, the event will be queued for execution relative to the last time the callback was invoked. But ifhardis false, the new timeout gets calculated relative to the current time.harddefaults to false.
io
Extra attributes: fd => $fd, poll => 'rwe' [timeout => $seconds, hard => $bool, timeout_cb => \&code]
The callback gets invoked when the file descriptor, fd, has data to be read, written, or pending exceptions π fd can be a GLOB, an IO::Handle object, or a file number (file descriptor). poll defaults to r.
Note that it's your option whether to have multiple watchers per file handle or to use a single watcher for all event conditions.
If timeout is set, events are also generated regularly if no actual I/O event occurs. And if timeout_cb is set then timeouts use this alternate callback instead of the main callback. Fancy.
signal
Extra attribute: signal => $str
Generates events based on signal arrival π¦ But the events are not actually generated immediately when the signal arrives: signals received are counted and reified by queue_pending() or implicitly by one_event(). Several signals of the same type may be merged into a single event. In such cases, the number of signals represented by a single event is stored in the hits attribute. Iconic behaviour, honestly.
π PRIORITY π
Priority is used to sort the event queue. Meaningful priorities range from -1 to 6 inclusive. Lower numbers mean higher priority (-1 is the highest priority and 6 is the lowest). If multiple events get queued, the ones with the highest priority are serviced first. Events with equal priority are serviced in first-in-first-out order.
use Event qw(PRIO_HIGH PRIO_NORMAL); # some constants
LEVELS: -1 0 1 2 3 4 5 6
----------------------+-------------+---------------
PRIO_HIGH PRIO_NORMAL
A negative priority causes the callback to be invoked immediately upon event occurrence. Use this with caution π While it may seem advantageous to use negative priorities, they bypass the whole point of having an event queue. Like, the entire vibe check. Gone.
Each watcher has a default priority, assigned by its constructor:
io PRIO_NORMAL
signal PRIO_HIGH
timer PRIO_NORMAL
var PRIO_NORMAL
Default priorities are stored in ${"Event::${type}::DefaultPriority"} π If the default priority is not satisfactory for your purposes, the constructor options nice, async, or prio can be used to adjust it. nice specifies an offset from the default priority; async forces the priority to -1; and prio assigns a given priority of your choice. If more than one of these options are given then prio overrides async overrides nice. Periodt.
π WATCHER CONSTRUCTOR ATTRIBUTES π
These options are only supported as constructor arguments. Like, don't try to set them later, bestie π
after => $secondsβ see the discussion of thetimerwatcher.async => $boolβ if$boolthen the watcher priority is set to-1.nice => $offsetβ offset from the default priority.parked => $yesβ by default, watcher constructors automatically invoke thestart()method. If you don't want the watcher started then requestparked=>1. It's giving "I'm just gonna sit here, unbothered" π
π WATCHER ATTRIBUTES π
at => $timeβ the expiration time in the same units as the system clock. For a timer,atwill usually be in the future. Living in the future, lowkey iconic.cb => \&codecb => [$class_or_object, $method_name]The function or method to call when an event is dispatched. The callback is invoked with$eventas its only argument β
Perhaps you are wondering what happens if something goes wrong and an untrapped die occurs within your callback? $Event::DIED is just for this purpose. See the full description of DIED below.
cbtime => $timeβ when the callback was invoked most recently.data => $anythingβ thedata()method associates arbitrary data with a watcher.
This method is not intended for implementers of watchers. If you are subclassing or implementing a watcher, consider the private() method instead. π
debug => $boolβ debugging can be activated globally or per watcher. When debugging is enabled for a particular watcher,$Event::DebugLevelis treated as two levels higher. Levels of1,2,3, or4give progressively more diagnostics onSTDERR. Spicy πΆοΈdesc => $stringβ an identifying name. If this is not passed explicitly to the constructor, it will be initialized with a string that attempts to identify the location in the source code where the watcher was constructed. Cute little breadcrumb trail πfd => $filehandleβ this attribute can accept either a perl-esque filehandle or a system call derived file descriptor number.hard => $boolβ determines how repeating timers (or timeouts) are recalculated. The timer is restarted either before or after the callback depending on whether it is true or false, respectively. In long-running callbacks this can make a significant difference π₯interval => $secondsβ how long between repeating timeouts. Theatattribute is recalculated usingintervalupon callback return.max => $secondsβ the maximum number of seconds to wait before triggering the callback. Similar to atimeout.max_cb_tm => $secondsβ the maximum number of seconds to spend in a callback. If a callback uses more time then it is aborted. Defaults to1sec. This feature is normally disabled. See Event::Stats πmin => $secondsβ enforce a minimum number of seconds between triggering events.poll => $bitsβ determines which kinds of events are of interest. This attribute can be set with either strings or bit constants. The bit constants are available via'use Event::Watcher qw(R W E T);'.'r'/Rβ read'w'/Wβ write'e'/Eβ exception't'/Tβ timeout
Thus, both of these statements enable interest in read:
$w->poll($w->poll . 'r');
$w->poll($w->poll | R);
A given type of watcher may support all or a subset of the available events. No cap.
prio => $levelβ changes the watcher's priority to the given level. Events generated by a watcher usually inherit the priority of the watcher.private => $anythingβ use theprivate()method to associate arbitrary data with a watcher. This method is intended for implementers of watchers or watcher subclasses. Each caller's package accesses its own private attribute. Respect the private property π«Άreentrant => $boolβ by default, callbacks are allowed to invokesweeporloopwhich in turn may invoke the same callback again recursively. This can be useful but can also be confusing. Moreover, if you keep reentering callbacks you will quickly run out of stack space π Disable this feature per watcher by settingreentrantto false. This will cause the watcher to be suspended during recursive calls tosweeporloop.repeat => $boolβ the repeat flag controls whether the callback should either be one-shot or continue waiting for new events. The default setting depends on the type of watcher.io,signal, andvardefault to true.signal => $strβ the callback is invoked after the specified signal is received. The$strstring should be something like'INT'or'QUIT'. Also see the documentation for%SIG.
A given signal can be handled by%SIGor Event, but not both at the same time β οΈ Event handles the signal as long as there is at least one active watcher. If all watchers for the signal are cancelled or stopped then Event sets the signal handler toSIG_DFL. That's the rules, bestie.
suspend => $boolβ Stop looking for events. Running events are allowed to complete their little moment, but queued events are cancelled. π This is for debugging: if you suspend all watchers in an application then you can examine the complete state unchanged for as long as you like without worrying about timer expirations. If you actually wish to stop a watcher then use thestop()method.timeout => $secondsβ The number of seconds before a watcher times out. β°timeout_cb => \&code/timeout_cb => [$class_or_object, $method_name]β This optional attribute is for when you want timeouts serviced in a separate code path than normal events. When this attribute is unset, timeouts are serviced bycb. β¨var => $refβ A reference to the variable being watched. π
β¨ EVENT ATTRIBUTES β¨
got => $bitsβgotis available in the callback of watchers withpoll. It's in the same format aspollexcept that it gives what kind of event actually happened. In contrast,pollis just an indication of interest. πhits => $intβ Signals in quick succession can be clumped into a single event. The number of signals clumped together is indicated by this attribute. This is always one for event types which don't clump. π₯prio => $levelβ Be aware that this priority can differ from the watcher's priority. For instance, the watcher's priority may have changed since the event was generated. Moreover, the C extension API offers the freedom to queue events of arbitrary priority. π₯w => $watcherβ This method return the event's watcher. It is read-only. π«Ά
π Customization and Exceptions π
$Event::DebugLevelβ Enables progressively more debugging output. Meaningful levels range from 1 (least output) to 5 (most output). Also seedebug. π$Event::DIEDβ Whenlooporsweepis called, an exception context is established for the duration of event processing. If an exception is detected then$Event::DIEDis invoked. The default hook useswarnto output the exception. After the DIED handler completes, event processing continues as if nothing happened. π«Ά
If you'd like more detailed output you can install the verbose handler:
$Event::DIED = \&Event::verbose_exception_handler;
Or you can write your own. The handler is invoked like this:
$Event::DIED->($event, $@);
If you do not want to continue looping after an error, you can do something like this:
$Event::DIED = sub {
Event::verbose_exception_handler(@_);
Event::unloop_all();
};
Event->add_hooks(key => sub { ... }, ...);β The bulk of Event's implementation is in C for maximum performance. Theadd_hooksmethod allows insertion of perl code at key points in the optimized event processing core. While flexible, this can hurt performance significantly. If you want customization and performance, please see the C API. π
Currently support hooks are detailed as follows:
hook purpose
------------- ----------------------------------------------
prepare returns minimum time to block (timeable)
check assess state after normal return from select/poll
asynccheck check for signals, etc
callback invoked before each event callback
π οΈ C API π οΈ
Event also has a direct API for callbacks written exclusively in C. See Event::MakeMaker. π
π§΅ WHAT ABOUT THREADS? π§΅
Event loops and threads are two different solutions to the same problem: asynchronous processing. Event loops have been around since the beginning of computing. They are well understood and proven to be a good solution for many applications. π
While event loops make use of basic operating system services, the bulk of their implementation is usually outside the kernel. While an event loop may appear to do many things in parallel, it does not, even on multiprocessor hardware. Actions are always dispatched sequentially. This implies that long running callbacks must be avoided because otherwise event processing is halted. π
Event loops work well when actions are short and to the point. Long-running tasks must be broken into short steps and scheduled for execution. Some sort of a state machine is usually required. While a big, complex application server is usually simpler to implement in a multithreaded fashion, a web browser can easily get by without threads. Consider a JPEG file download and render. When some new bytes are available they are sorted to the right place on the screen. Only a little state must be kept to keep track of how much has been rendered and to process subsequent incoming bytes. π
Threads can either substitute for an event loop or complement it. Threads are similar to processes in that the operating system manages task switching for you. However, the difference is that all threads share the same address space. This is good and bad. Higher performance can be achieved but since data is shared between threads, extreme care must be taken to access or modify global data. The operating system can switch threads at any moment or can execute multiple threads simultaneously. I hope this sounds dangerous! It is! Threads can introduce maddeningly complicated and hard to debug synchronization problems. π
Threads are like rocket fuel. They are essential when you really need them but most applications would be better off with a simple event loop. Even if threads are genuinely needed, consider confining them to the parts of an application where truly scalable performance is really worth the difficulty of a multithreaded implementation. For example, most GUIs applications do not need threads and most scientific compute intensive problems can be isolated from event dispatching. On the other hand, high performance transaction servers generally do mandate a truly multithreaded approach. π₯
Another consideration is that threads are not quite as widely available as event loops. While a few forward-thinking operating systems have offered threads since the beginning, their addition to many popular operating systems is much more recent and some still offer no threads support. If portability is a requirement, one must check that threads support is available and also carefully test a particular threads implementation to see whether it supports the features you need. It is likely that all platforms will have a solid implementation soon but at this point in history it is best to double check. π¦
Many suggestions by Mark Mielke <Mark.Mielke.markm@nt.com> π
π§ WHAT ABOUT NON-PREEMPTIVE THREADS? π§
The Java language is oriented to use non-preemptive threads, yet even Java uses an event-loop for Swing (AFAIK). That is one of the reasons I don't use Java for network-centric applications. My belief is that the benefit of multi-threading is the gain in performance on SMP hardware. In my view, non-preemptive threads (java green-threads) are usually poor design. I find them harder to work with, harder to debug, and slower for a rather marginal gain in readability. I really like working with a state machine. I find it leads to more stable and better code. It also has the benefit of abstracting away how concurrency is achieved. β¨
Contributed by artur@vogon-solutions.com, 12 Jul 1999. π¦
π BUGS π
- No support for
epoll, or better,libevent. π - The scope of events is pretty strange compared to most other perl objects. I'm not sure if this is a bug or a feature (OK, probably it was a mistake). We'll probably want to re-work things for Perl6. π
- The meaning of
$io->timeout(0)might change. Useundefto unset the timeout. β οΈ - There seems to be some sort of bug in the global destruction phase:
Attempt to free unreferenced scalar during global destruction.
Use of uninitialized value during global destruction.
Explicit blessing to '' (assuming package main) during global
destruction.
π THE FUTURE π
Even if this module does not end up being the One and True Event Loop, the author will insure that it is source compatible with its successor, or arrange for gradual migration. Back in the early days, the Event programming API was changing at every release. Care was taken to allow the old API to continue to work, and the transition was eased by printing out lots of warnings about the new usage. So you shouldn't sit on your hands in anticipation of the One and True Event Loop. Just start coding! π
π ALSO SEE π
- Useful and Fun β
Time::HiRes,NetServer::Portal,Time::WarpπΈ - Message Passing β
COPE,IPC::LDT,Event-tcpβ¨ - GUI β While
Tkdoes not yet support Event,PerlQtdoes. π - C API β
Inlineπ οΈ
π«Ά SUPPORT π«Ά
If you have insights or complaints then please subscribe to the mailing list! Send email to: perl-loop-subscribe@perl.org π
π AUTHOR π
Joshua N. Pritikin <jpritikin@pobox.com> π
π ACKNOWLEDGMENT π
Initial 0.01 implementation by Graham Barr <gbarr@pobox.com>. Other contributors include at least those lists below and folks mentioned in the ChangeLog. π«Ά
Gisle Aas <gisle@aas.no>Uri Guttman <uri@sysarch.com>Nick Ing-Simmons <nick@ni-s.u-net.com>(Tk)Sarathy <gsar@engin.umich.edu>Jochen Stenzel <perl@jochen-stenzel.de>
Β©οΈ COPYRIGHT Β©οΈ
Copyright Β© 1997 Joshua Nathaniel Pritikin & Graham Barr
Copyright Β© 1998, 1999, 2000, 2001, 2002, 2003, 2004 Joshua Nathaniel Pritikin
All rights reserved. This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself. π
That's the whole page, bestie! This one is perl v5.42.0, dated 2021-04-30, and it's the Event(3) manual. Go forth and event-loop like the icon you are! πβ¨
π¦ where this page comes from
- opensuse-leap 16.0: perl-Event 1.28-bp160.1.14 Β· Artistic-1.0 OR GPL-1.0-or-later
- opensuse-leap 16.0: perl-Event 1.28-bp160.1.14 Β· Artistic-1.0 OR GPL-1.0-or-later
- opensuse-leap 16.0: perl-Event 1.28-bp160.1.14 Β· Artistic-1.0 OR GPL-1.0-or-later
- opensuse-leap 16.0: perl-Event 1.28-bp160.1.14 Β· Artistic-1.0 OR GPL-1.0-or-later
- π girlified by deepseek-ai/DeepSeek-V4.1-Flash-1017 on : a language model's rewrite of the original, under its terms