1629946740
This short tutorial will walk through the various examples to lock the screen orientation with CSS and Javascript.
-- CHAPTERS --
0:00 Introduction
0:25 Orientation Lock API
3:17 CSS Force Rotate
4:03 Rotate Message
5:25 Closing
1653475560
msgpack.php
A pure PHP implementation of the MessagePack serialization format.
The recommended way to install the library is through Composer:
composer require rybakit/msgpack
To pack values you can either use an instance of a Packer
:
$packer = new Packer();
$packed = $packer->pack($value);
or call a static method on the MessagePack
class:
$packed = MessagePack::pack($value);
In the examples above, the method pack
automatically packs a value depending on its type. However, not all PHP types can be uniquely translated to MessagePack types. For example, the MessagePack format defines map
and array
types, which are represented by a single array
type in PHP. By default, the packer will pack a PHP array as a MessagePack array if it has sequential numeric keys, starting from 0
and as a MessagePack map otherwise:
$mpArr1 = $packer->pack([1, 2]); // MP array [1, 2]
$mpArr2 = $packer->pack([0 => 1, 1 => 2]); // MP array [1, 2]
$mpMap1 = $packer->pack([0 => 1, 2 => 3]); // MP map {0: 1, 2: 3}
$mpMap2 = $packer->pack([1 => 2, 2 => 3]); // MP map {1: 2, 2: 3}
$mpMap3 = $packer->pack(['a' => 1, 'b' => 2]); // MP map {a: 1, b: 2}
However, sometimes you need to pack a sequential array as a MessagePack map. To do this, use the packMap
method:
$mpMap = $packer->packMap([1, 2]); // {0: 1, 1: 2}
Here is a list of type-specific packing methods:
$packer->packNil(); // MP nil
$packer->packBool(true); // MP bool
$packer->packInt(42); // MP int
$packer->packFloat(M_PI); // MP float (32 or 64)
$packer->packFloat32(M_PI); // MP float 32
$packer->packFloat64(M_PI); // MP float 64
$packer->packStr('foo'); // MP str
$packer->packBin("\x80"); // MP bin
$packer->packArray([1, 2]); // MP array
$packer->packMap(['a' => 1]); // MP map
$packer->packExt(1, "\xaa"); // MP ext
Check the "Custom types" section below on how to pack custom types.
The Packer
object supports a number of bitmask-based options for fine-tuning the packing process (defaults are in bold):
Name | Description |
---|---|
FORCE_STR | Forces PHP strings to be packed as MessagePack UTF-8 strings |
FORCE_BIN | Forces PHP strings to be packed as MessagePack binary data |
DETECT_STR_BIN | Detects MessagePack str/bin type automatically |
FORCE_ARR | Forces PHP arrays to be packed as MessagePack arrays |
FORCE_MAP | Forces PHP arrays to be packed as MessagePack maps |
DETECT_ARR_MAP | Detects MessagePack array/map type automatically |
FORCE_FLOAT32 | Forces PHP floats to be packed as 32-bits MessagePack floats |
FORCE_FLOAT64 | Forces PHP floats to be packed as 64-bits MessagePack floats |
The type detection mode (
DETECT_STR_BIN
/DETECT_ARR_MAP
) adds some overhead which can be noticed when you pack large (16- and 32-bit) arrays or strings. However, if you know the value type in advance (for example, you only work with UTF-8 strings or/and associative arrays), you can eliminate this overhead by forcing the packer to use the appropriate type, which will save it from running the auto-detection routine. Another option is to explicitly specify the value type. The library provides 2 auxiliary classes for this,Map
andBin
. Check the "Custom types" section below for details.
Examples:
// detect str/bin type and pack PHP 64-bit floats (doubles) to MP 32-bit floats
$packer = new Packer(PackOptions::DETECT_STR_BIN | PackOptions::FORCE_FLOAT32);
// these will throw MessagePack\Exception\InvalidOptionException
$packer = new Packer(PackOptions::FORCE_STR | PackOptions::FORCE_BIN);
$packer = new Packer(PackOptions::FORCE_FLOAT32 | PackOptions::FORCE_FLOAT64);
To unpack data you can either use an instance of a BufferUnpacker
:
$unpacker = new BufferUnpacker();
$unpacker->reset($packed);
$value = $unpacker->unpack();
or call a static method on the MessagePack
class:
$value = MessagePack::unpack($packed);
If the packed data is received in chunks (e.g. when reading from a stream), use the tryUnpack
method, which attempts to unpack data and returns an array of unpacked messages (if any) instead of throwing an InsufficientDataException
:
while ($chunk = ...) {
$unpacker->append($chunk);
if ($messages = $unpacker->tryUnpack()) {
return $messages;
}
}
If you want to unpack from a specific position in a buffer, use seek
:
$unpacker->seek(42); // set position equal to 42 bytes
$unpacker->seek(-8); // set position to 8 bytes before the end of the buffer
To skip bytes from the current position, use skip
:
$unpacker->skip(10); // set position to 10 bytes ahead of the current position
To get the number of remaining (unread) bytes in the buffer:
$unreadBytesCount = $unpacker->getRemainingCount();
To check whether the buffer has unread data:
$hasUnreadBytes = $unpacker->hasRemaining();
If needed, you can remove already read data from the buffer by calling:
$releasedBytesCount = $unpacker->release();
With the read
method you can read raw (packed) data:
$packedData = $unpacker->read(2); // read 2 bytes
Besides the above methods BufferUnpacker
provides type-specific unpacking methods, namely:
$unpacker->unpackNil(); // PHP null
$unpacker->unpackBool(); // PHP bool
$unpacker->unpackInt(); // PHP int
$unpacker->unpackFloat(); // PHP float
$unpacker->unpackStr(); // PHP UTF-8 string
$unpacker->unpackBin(); // PHP binary string
$unpacker->unpackArray(); // PHP sequential array
$unpacker->unpackMap(); // PHP associative array
$unpacker->unpackExt(); // PHP MessagePack\Type\Ext object
The BufferUnpacker
object supports a number of bitmask-based options for fine-tuning the unpacking process (defaults are in bold):
Name | Description |
---|---|
BIGINT_AS_STR | Converts overflowed integers to strings [1] |
BIGINT_AS_GMP | Converts overflowed integers to GMP objects [2] |
BIGINT_AS_DEC | Converts overflowed integers to Decimal\Decimal objects [3] |
1. The binary MessagePack format has unsigned 64-bit as its largest integer data type, but PHP does not support such integers, which means that an overflow can occur during unpacking.
2. Make sure the GMP extension is enabled.
3. Make sure the Decimal extension is enabled.
Examples:
$packedUint64 = "\xcf"."\xff\xff\xff\xff"."\xff\xff\xff\xff";
$unpacker = new BufferUnpacker($packedUint64);
var_dump($unpacker->unpack()); // string(20) "18446744073709551615"
$unpacker = new BufferUnpacker($packedUint64, UnpackOptions::BIGINT_AS_GMP);
var_dump($unpacker->unpack()); // object(GMP) {...}
$unpacker = new BufferUnpacker($packedUint64, UnpackOptions::BIGINT_AS_DEC);
var_dump($unpacker->unpack()); // object(Decimal\Decimal) {...}
In addition to the basic types, the library provides functionality to serialize and deserialize arbitrary types. This can be done in several ways, depending on your use case. Let's take a look at them.
If you need to serialize an instance of one of your classes into one of the basic MessagePack types, the best way to do this is to implement the CanBePacked interface in the class. A good example of such a class is the Map
type class that comes with the library. This type is useful when you want to explicitly specify that a given PHP array should be packed as a MessagePack map without triggering an automatic type detection routine:
$packer = new Packer();
$packedMap = $packer->pack(new Map([1, 2, 3]));
$packedArray = $packer->pack([1, 2, 3]);
More type examples can be found in the src/Type directory.
As with type objects, type transformers are only responsible for serializing values. They should be used when you need to serialize a value that does not implement the CanBePacked interface. Examples of such values could be instances of built-in or third-party classes that you don't own, or non-objects such as resources.
A transformer class must implement the CanPack interface. To use a transformer, it must first be registered in the packer. Here is an example of how to serialize PHP streams into the MessagePack bin
format type using one of the supplied transformers, StreamTransformer
:
$packer = new Packer(null, [new StreamTransformer()]);
$packedBin = $packer->pack(fopen('/path/to/file', 'r+'));
More type transformer examples can be found in the src/TypeTransformer directory.
In contrast to the cases described above, extensions are intended to handle extension types and are responsible for both serialization and deserialization of values (types).
An extension class must implement the Extension interface. To use an extension, it must first be registered in the packer and the unpacker.
The MessagePack specification divides extension types into two groups: predefined and application-specific. Currently, there is only one predefined type in the specification, Timestamp.
Timestamp
The Timestamp extension type is a predefined type. Support for this type in the library is done through the TimestampExtension
class. This class is responsible for handling Timestamp
objects, which represent the number of seconds and optional adjustment in nanoseconds:
$timestampExtension = new TimestampExtension();
$packer = new Packer();
$packer = $packer->extendWith($timestampExtension);
$unpacker = new BufferUnpacker();
$unpacker = $unpacker->extendWith($timestampExtension);
$packedTimestamp = $packer->pack(Timestamp::now());
$timestamp = $unpacker->reset($packedTimestamp)->unpack();
$seconds = $timestamp->getSeconds();
$nanoseconds = $timestamp->getNanoseconds();
When using the MessagePack
class, the Timestamp extension is already registered:
$packedTimestamp = MessagePack::pack(Timestamp::now());
$timestamp = MessagePack::unpack($packedTimestamp);
Application-specific extensions
In addition, the format can be extended with your own types. For example, to make the built-in PHP DateTime
objects first-class citizens in your code, you can create a corresponding extension, as shown in the example. Please note, that custom extensions have to be registered with a unique extension ID (an integer from 0
to 127
).
More extension examples can be found in the examples/MessagePack directory.
To learn more about how extension types can be useful, check out this article.
If an error occurs during packing/unpacking, a PackingFailedException
or an UnpackingFailedException
will be thrown, respectively. In addition, an InsufficientDataException
can be thrown during unpacking.
An InvalidOptionException
will be thrown in case an invalid option (or a combination of mutually exclusive options) is used.
Run tests as follows:
vendor/bin/phpunit
Also, if you already have Docker installed, you can run the tests in a docker container. First, create a container:
./dockerfile.sh | docker build -t msgpack -
The command above will create a container named msgpack
with PHP 8.1 runtime. You may change the default runtime by defining the PHP_IMAGE
environment variable:
PHP_IMAGE='php:8.0-cli' ./dockerfile.sh | docker build -t msgpack -
See a list of various images here.
Then run the unit tests:
docker run --rm -v $PWD:/msgpack -w /msgpack msgpack
To ensure that the unpacking works correctly with malformed/semi-malformed data, you can use a testing technique called Fuzzing. The library ships with a help file (target) for PHP-Fuzzer and can be used as follows:
php-fuzzer fuzz tests/fuzz_buffer_unpacker.php
To check performance, run:
php -n -dzend_extension=opcache.so \
-dpcre.jit=1 -dopcache.enable=1 -dopcache.enable_cli=1 \
tests/bench.php
Example output
Filter: MessagePack\Tests\Perf\Filter\ListFilter
Rounds: 3
Iterations: 100000
=============================================
Test/Target Packer BufferUnpacker
---------------------------------------------
nil .................. 0.0030 ........ 0.0139
false ................ 0.0037 ........ 0.0144
true ................. 0.0040 ........ 0.0137
7-bit uint #1 ........ 0.0052 ........ 0.0120
7-bit uint #2 ........ 0.0059 ........ 0.0114
7-bit uint #3 ........ 0.0061 ........ 0.0119
5-bit sint #1 ........ 0.0067 ........ 0.0126
5-bit sint #2 ........ 0.0064 ........ 0.0132
5-bit sint #3 ........ 0.0066 ........ 0.0135
8-bit uint #1 ........ 0.0078 ........ 0.0200
8-bit uint #2 ........ 0.0077 ........ 0.0212
8-bit uint #3 ........ 0.0086 ........ 0.0203
16-bit uint #1 ....... 0.0111 ........ 0.0271
16-bit uint #2 ....... 0.0115 ........ 0.0260
16-bit uint #3 ....... 0.0103 ........ 0.0273
32-bit uint #1 ....... 0.0116 ........ 0.0326
32-bit uint #2 ....... 0.0118 ........ 0.0332
32-bit uint #3 ....... 0.0127 ........ 0.0325
64-bit uint #1 ....... 0.0140 ........ 0.0277
64-bit uint #2 ....... 0.0134 ........ 0.0294
64-bit uint #3 ....... 0.0134 ........ 0.0281
8-bit int #1 ......... 0.0086 ........ 0.0241
8-bit int #2 ......... 0.0089 ........ 0.0225
8-bit int #3 ......... 0.0085 ........ 0.0229
16-bit int #1 ........ 0.0118 ........ 0.0280
16-bit int #2 ........ 0.0121 ........ 0.0270
16-bit int #3 ........ 0.0109 ........ 0.0274
32-bit int #1 ........ 0.0128 ........ 0.0346
32-bit int #2 ........ 0.0118 ........ 0.0339
32-bit int #3 ........ 0.0135 ........ 0.0368
64-bit int #1 ........ 0.0138 ........ 0.0276
64-bit int #2 ........ 0.0132 ........ 0.0286
64-bit int #3 ........ 0.0137 ........ 0.0274
64-bit int #4 ........ 0.0180 ........ 0.0285
64-bit float #1 ...... 0.0134 ........ 0.0284
64-bit float #2 ...... 0.0125 ........ 0.0275
64-bit float #3 ...... 0.0126 ........ 0.0283
fix string #1 ........ 0.0035 ........ 0.0133
fix string #2 ........ 0.0094 ........ 0.0216
fix string #3 ........ 0.0094 ........ 0.0222
fix string #4 ........ 0.0091 ........ 0.0241
8-bit string #1 ...... 0.0122 ........ 0.0301
8-bit string #2 ...... 0.0118 ........ 0.0304
8-bit string #3 ...... 0.0119 ........ 0.0315
16-bit string #1 ..... 0.0150 ........ 0.0388
16-bit string #2 ..... 0.1545 ........ 0.1665
32-bit string ........ 0.1570 ........ 0.1756
wide char string #1 .. 0.0091 ........ 0.0236
wide char string #2 .. 0.0122 ........ 0.0313
8-bit binary #1 ...... 0.0100 ........ 0.0302
8-bit binary #2 ...... 0.0123 ........ 0.0324
8-bit binary #3 ...... 0.0126 ........ 0.0327
16-bit binary ........ 0.0168 ........ 0.0372
32-bit binary ........ 0.1588 ........ 0.1754
fix array #1 ......... 0.0042 ........ 0.0131
fix array #2 ......... 0.0294 ........ 0.0367
fix array #3 ......... 0.0412 ........ 0.0472
16-bit array #1 ...... 0.1378 ........ 0.1596
16-bit array #2 ........... S ............. S
32-bit array .............. S ............. S
complex array ........ 0.1865 ........ 0.2283
fix map #1 ........... 0.0725 ........ 0.1048
fix map #2 ........... 0.0319 ........ 0.0405
fix map #3 ........... 0.0356 ........ 0.0665
fix map #4 ........... 0.0465 ........ 0.0497
16-bit map #1 ........ 0.2540 ........ 0.3028
16-bit map #2 ............. S ............. S
32-bit map ................ S ............. S
complex map .......... 0.2372 ........ 0.2710
fixext 1 ............. 0.0283 ........ 0.0358
fixext 2 ............. 0.0291 ........ 0.0371
fixext 4 ............. 0.0302 ........ 0.0355
fixext 8 ............. 0.0288 ........ 0.0384
fixext 16 ............ 0.0293 ........ 0.0359
8-bit ext ............ 0.0302 ........ 0.0439
16-bit ext ........... 0.0334 ........ 0.0499
32-bit ext ........... 0.1845 ........ 0.1888
32-bit timestamp #1 .. 0.0337 ........ 0.0547
32-bit timestamp #2 .. 0.0335 ........ 0.0560
64-bit timestamp #1 .. 0.0371 ........ 0.0575
64-bit timestamp #2 .. 0.0374 ........ 0.0542
64-bit timestamp #3 .. 0.0356 ........ 0.0533
96-bit timestamp #1 .. 0.0362 ........ 0.0699
96-bit timestamp #2 .. 0.0381 ........ 0.0701
96-bit timestamp #3 .. 0.0367 ........ 0.0687
=============================================
Total 2.7618 4.0820
Skipped 4 4
Failed 0 0
Ignored 0 0
With JIT:
php -n -dzend_extension=opcache.so \
-dpcre.jit=1 -dopcache.jit_buffer_size=64M -dopcache.jit=tracing -dopcache.enable=1 -dopcache.enable_cli=1 \
tests/bench.php
Example output
Filter: MessagePack\Tests\Perf\Filter\ListFilter
Rounds: 3
Iterations: 100000
=============================================
Test/Target Packer BufferUnpacker
---------------------------------------------
nil .................. 0.0005 ........ 0.0054
false ................ 0.0004 ........ 0.0059
true ................. 0.0004 ........ 0.0059
7-bit uint #1 ........ 0.0010 ........ 0.0047
7-bit uint #2 ........ 0.0010 ........ 0.0046
7-bit uint #3 ........ 0.0010 ........ 0.0046
5-bit sint #1 ........ 0.0025 ........ 0.0046
5-bit sint #2 ........ 0.0023 ........ 0.0046
5-bit sint #3 ........ 0.0024 ........ 0.0045
8-bit uint #1 ........ 0.0043 ........ 0.0081
8-bit uint #2 ........ 0.0043 ........ 0.0079
8-bit uint #3 ........ 0.0041 ........ 0.0080
16-bit uint #1 ....... 0.0064 ........ 0.0095
16-bit uint #2 ....... 0.0064 ........ 0.0091
16-bit uint #3 ....... 0.0064 ........ 0.0094
32-bit uint #1 ....... 0.0085 ........ 0.0114
32-bit uint #2 ....... 0.0077 ........ 0.0122
32-bit uint #3 ....... 0.0077 ........ 0.0120
64-bit uint #1 ....... 0.0085 ........ 0.0159
64-bit uint #2 ....... 0.0086 ........ 0.0157
64-bit uint #3 ....... 0.0086 ........ 0.0158
8-bit int #1 ......... 0.0042 ........ 0.0080
8-bit int #2 ......... 0.0042 ........ 0.0080
8-bit int #3 ......... 0.0042 ........ 0.0081
16-bit int #1 ........ 0.0065 ........ 0.0095
16-bit int #2 ........ 0.0065 ........ 0.0090
16-bit int #3 ........ 0.0056 ........ 0.0085
32-bit int #1 ........ 0.0067 ........ 0.0107
32-bit int #2 ........ 0.0066 ........ 0.0106
32-bit int #3 ........ 0.0063 ........ 0.0104
64-bit int #1 ........ 0.0072 ........ 0.0162
64-bit int #2 ........ 0.0073 ........ 0.0174
64-bit int #3 ........ 0.0072 ........ 0.0164
64-bit int #4 ........ 0.0077 ........ 0.0161
64-bit float #1 ...... 0.0053 ........ 0.0135
64-bit float #2 ...... 0.0053 ........ 0.0135
64-bit float #3 ...... 0.0052 ........ 0.0135
fix string #1 ....... -0.0002 ........ 0.0044
fix string #2 ........ 0.0035 ........ 0.0067
fix string #3 ........ 0.0035 ........ 0.0077
fix string #4 ........ 0.0033 ........ 0.0078
8-bit string #1 ...... 0.0059 ........ 0.0110
8-bit string #2 ...... 0.0063 ........ 0.0121
8-bit string #3 ...... 0.0064 ........ 0.0124
16-bit string #1 ..... 0.0099 ........ 0.0146
16-bit string #2 ..... 0.1522 ........ 0.1474
32-bit string ........ 0.1511 ........ 0.1483
wide char string #1 .. 0.0039 ........ 0.0084
wide char string #2 .. 0.0073 ........ 0.0123
8-bit binary #1 ...... 0.0040 ........ 0.0112
8-bit binary #2 ...... 0.0075 ........ 0.0123
8-bit binary #3 ...... 0.0077 ........ 0.0129
16-bit binary ........ 0.0096 ........ 0.0145
32-bit binary ........ 0.1535 ........ 0.1479
fix array #1 ......... 0.0008 ........ 0.0061
fix array #2 ......... 0.0121 ........ 0.0165
fix array #3 ......... 0.0193 ........ 0.0222
16-bit array #1 ...... 0.0607 ........ 0.0479
16-bit array #2 ........... S ............. S
32-bit array .............. S ............. S
complex array ........ 0.0749 ........ 0.0824
fix map #1 ........... 0.0329 ........ 0.0431
fix map #2 ........... 0.0161 ........ 0.0189
fix map #3 ........... 0.0205 ........ 0.0262
fix map #4 ........... 0.0252 ........ 0.0205
16-bit map #1 ........ 0.1016 ........ 0.0927
16-bit map #2 ............. S ............. S
32-bit map ................ S ............. S
complex map .......... 0.1096 ........ 0.1030
fixext 1 ............. 0.0157 ........ 0.0161
fixext 2 ............. 0.0175 ........ 0.0183
fixext 4 ............. 0.0156 ........ 0.0185
fixext 8 ............. 0.0163 ........ 0.0184
fixext 16 ............ 0.0164 ........ 0.0182
8-bit ext ............ 0.0158 ........ 0.0207
16-bit ext ........... 0.0203 ........ 0.0219
32-bit ext ........... 0.1614 ........ 0.1539
32-bit timestamp #1 .. 0.0195 ........ 0.0249
32-bit timestamp #2 .. 0.0188 ........ 0.0260
64-bit timestamp #1 .. 0.0207 ........ 0.0281
64-bit timestamp #2 .. 0.0212 ........ 0.0291
64-bit timestamp #3 .. 0.0207 ........ 0.0295
96-bit timestamp #1 .. 0.0222 ........ 0.0358
96-bit timestamp #2 .. 0.0228 ........ 0.0353
96-bit timestamp #3 .. 0.0210 ........ 0.0319
=============================================
Total 1.6432 1.9674
Skipped 4 4
Failed 0 0
Ignored 0 0
You may change default benchmark settings by defining the following environment variables:
Name | Default |
---|---|
MP_BENCH_TARGETS | pure_p,pure_u , see a list of available targets |
MP_BENCH_ITERATIONS | 100_000 |
MP_BENCH_DURATION | not set |
MP_BENCH_ROUNDS | 3 |
MP_BENCH_TESTS | -@slow , see a list of available tests |
For example:
export MP_BENCH_TARGETS=pure_p
export MP_BENCH_ITERATIONS=1000000
export MP_BENCH_ROUNDS=5
# a comma separated list of test names
export MP_BENCH_TESTS='complex array, complex map'
# or a group name
# export MP_BENCH_TESTS='-@slow' // @pecl_comp
# or a regexp
# export MP_BENCH_TESTS='/complex (array|map)/'
Another example, benchmarking both the library and the PECL extension:
MP_BENCH_TARGETS=pure_p,pure_u,pecl_p,pecl_u \
php -n -dextension=msgpack.so -dzend_extension=opcache.so \
-dpcre.jit=1 -dopcache.enable=1 -dopcache.enable_cli=1 \
tests/bench.php
Example output
Filter: MessagePack\Tests\Perf\Filter\ListFilter
Rounds: 3
Iterations: 100000
===========================================================================
Test/Target Packer BufferUnpacker msgpack_pack msgpack_unpack
---------------------------------------------------------------------------
nil .................. 0.0031 ........ 0.0141 ...... 0.0055 ........ 0.0064
false ................ 0.0039 ........ 0.0154 ...... 0.0056 ........ 0.0053
true ................. 0.0038 ........ 0.0139 ...... 0.0056 ........ 0.0044
7-bit uint #1 ........ 0.0061 ........ 0.0110 ...... 0.0059 ........ 0.0046
7-bit uint #2 ........ 0.0065 ........ 0.0119 ...... 0.0042 ........ 0.0029
7-bit uint #3 ........ 0.0054 ........ 0.0117 ...... 0.0045 ........ 0.0025
5-bit sint #1 ........ 0.0047 ........ 0.0103 ...... 0.0038 ........ 0.0022
5-bit sint #2 ........ 0.0048 ........ 0.0117 ...... 0.0038 ........ 0.0022
5-bit sint #3 ........ 0.0046 ........ 0.0102 ...... 0.0038 ........ 0.0023
8-bit uint #1 ........ 0.0063 ........ 0.0174 ...... 0.0039 ........ 0.0031
8-bit uint #2 ........ 0.0063 ........ 0.0167 ...... 0.0040 ........ 0.0029
8-bit uint #3 ........ 0.0063 ........ 0.0168 ...... 0.0039 ........ 0.0030
16-bit uint #1 ....... 0.0092 ........ 0.0222 ...... 0.0049 ........ 0.0030
16-bit uint #2 ....... 0.0096 ........ 0.0227 ...... 0.0042 ........ 0.0046
16-bit uint #3 ....... 0.0123 ........ 0.0274 ...... 0.0059 ........ 0.0051
32-bit uint #1 ....... 0.0136 ........ 0.0331 ...... 0.0060 ........ 0.0048
32-bit uint #2 ....... 0.0130 ........ 0.0336 ...... 0.0070 ........ 0.0048
32-bit uint #3 ....... 0.0127 ........ 0.0329 ...... 0.0051 ........ 0.0048
64-bit uint #1 ....... 0.0126 ........ 0.0268 ...... 0.0055 ........ 0.0049
64-bit uint #2 ....... 0.0135 ........ 0.0281 ...... 0.0052 ........ 0.0046
64-bit uint #3 ....... 0.0131 ........ 0.0274 ...... 0.0069 ........ 0.0044
8-bit int #1 ......... 0.0077 ........ 0.0236 ...... 0.0058 ........ 0.0044
8-bit int #2 ......... 0.0087 ........ 0.0244 ...... 0.0058 ........ 0.0048
8-bit int #3 ......... 0.0084 ........ 0.0241 ...... 0.0055 ........ 0.0049
16-bit int #1 ........ 0.0112 ........ 0.0271 ...... 0.0048 ........ 0.0045
16-bit int #2 ........ 0.0124 ........ 0.0292 ...... 0.0057 ........ 0.0049
16-bit int #3 ........ 0.0118 ........ 0.0270 ...... 0.0058 ........ 0.0050
32-bit int #1 ........ 0.0137 ........ 0.0366 ...... 0.0058 ........ 0.0051
32-bit int #2 ........ 0.0133 ........ 0.0366 ...... 0.0056 ........ 0.0049
32-bit int #3 ........ 0.0129 ........ 0.0350 ...... 0.0052 ........ 0.0048
64-bit int #1 ........ 0.0145 ........ 0.0254 ...... 0.0034 ........ 0.0025
64-bit int #2 ........ 0.0097 ........ 0.0214 ...... 0.0034 ........ 0.0025
64-bit int #3 ........ 0.0096 ........ 0.0287 ...... 0.0059 ........ 0.0050
64-bit int #4 ........ 0.0143 ........ 0.0277 ...... 0.0059 ........ 0.0046
64-bit float #1 ...... 0.0134 ........ 0.0281 ...... 0.0057 ........ 0.0052
64-bit float #2 ...... 0.0141 ........ 0.0281 ...... 0.0057 ........ 0.0050
64-bit float #3 ...... 0.0144 ........ 0.0282 ...... 0.0057 ........ 0.0050
fix string #1 ........ 0.0036 ........ 0.0143 ...... 0.0066 ........ 0.0053
fix string #2 ........ 0.0107 ........ 0.0222 ...... 0.0065 ........ 0.0068
fix string #3 ........ 0.0116 ........ 0.0245 ...... 0.0063 ........ 0.0069
fix string #4 ........ 0.0105 ........ 0.0253 ...... 0.0083 ........ 0.0077
8-bit string #1 ...... 0.0126 ........ 0.0318 ...... 0.0075 ........ 0.0088
8-bit string #2 ...... 0.0121 ........ 0.0295 ...... 0.0076 ........ 0.0086
8-bit string #3 ...... 0.0125 ........ 0.0293 ...... 0.0130 ........ 0.0093
16-bit string #1 ..... 0.0159 ........ 0.0368 ...... 0.0117 ........ 0.0086
16-bit string #2 ..... 0.1547 ........ 0.1686 ...... 0.1516 ........ 0.1373
32-bit string ........ 0.1558 ........ 0.1729 ...... 0.1511 ........ 0.1396
wide char string #1 .. 0.0098 ........ 0.0237 ...... 0.0066 ........ 0.0065
wide char string #2 .. 0.0128 ........ 0.0291 ...... 0.0061 ........ 0.0082
8-bit binary #1 ........... I ............. I ........... F ............. I
8-bit binary #2 ........... I ............. I ........... F ............. I
8-bit binary #3 ........... I ............. I ........... F ............. I
16-bit binary ............. I ............. I ........... F ............. I
32-bit binary ............. I ............. I ........... F ............. I
fix array #1 ......... 0.0040 ........ 0.0129 ...... 0.0120 ........ 0.0058
fix array #2 ......... 0.0279 ........ 0.0390 ...... 0.0143 ........ 0.0165
fix array #3 ......... 0.0415 ........ 0.0463 ...... 0.0162 ........ 0.0187
16-bit array #1 ...... 0.1349 ........ 0.1628 ...... 0.0334 ........ 0.0341
16-bit array #2 ........... S ............. S ........... S ............. S
32-bit array .............. S ............. S ........... S ............. S
complex array ............. I ............. I ........... F ............. F
fix map #1 ................ I ............. I ........... F ............. I
fix map #2 ........... 0.0345 ........ 0.0391 ...... 0.0143 ........ 0.0168
fix map #3 ................ I ............. I ........... F ............. I
fix map #4 ........... 0.0459 ........ 0.0473 ...... 0.0151 ........ 0.0163
16-bit map #1 ........ 0.2518 ........ 0.2962 ...... 0.0400 ........ 0.0490
16-bit map #2 ............. S ............. S ........... S ............. S
32-bit map ................ S ............. S ........... S ............. S
complex map .......... 0.2380 ........ 0.2682 ...... 0.0545 ........ 0.0579
fixext 1 .................. I ............. I ........... F ............. F
fixext 2 .................. I ............. I ........... F ............. F
fixext 4 .................. I ............. I ........... F ............. F
fixext 8 .................. I ............. I ........... F ............. F
fixext 16 ................. I ............. I ........... F ............. F
8-bit ext ................. I ............. I ........... F ............. F
16-bit ext ................ I ............. I ........... F ............. F
32-bit ext ................ I ............. I ........... F ............. F
32-bit timestamp #1 ....... I ............. I ........... F ............. F
32-bit timestamp #2 ....... I ............. I ........... F ............. F
64-bit timestamp #1 ....... I ............. I ........... F ............. F
64-bit timestamp #2 ....... I ............. I ........... F ............. F
64-bit timestamp #3 ....... I ............. I ........... F ............. F
96-bit timestamp #1 ....... I ............. I ........... F ............. F
96-bit timestamp #2 ....... I ............. I ........... F ............. F
96-bit timestamp #3 ....... I ............. I ........... F ............. F
===========================================================================
Total 1.5625 2.3866 0.7735 0.7243
Skipped 4 4 4 4
Failed 0 0 24 17
Ignored 24 24 0 7
With JIT:
MP_BENCH_TARGETS=pure_p,pure_u,pecl_p,pecl_u \
php -n -dextension=msgpack.so -dzend_extension=opcache.so \
-dpcre.jit=1 -dopcache.jit_buffer_size=64M -dopcache.jit=tracing -dopcache.enable=1 -dopcache.enable_cli=1 \
tests/bench.php
Example output
Filter: MessagePack\Tests\Perf\Filter\ListFilter
Rounds: 3
Iterations: 100000
===========================================================================
Test/Target Packer BufferUnpacker msgpack_pack msgpack_unpack
---------------------------------------------------------------------------
nil .................. 0.0001 ........ 0.0052 ...... 0.0053 ........ 0.0042
false ................ 0.0007 ........ 0.0060 ...... 0.0057 ........ 0.0043
true ................. 0.0008 ........ 0.0060 ...... 0.0056 ........ 0.0041
7-bit uint #1 ........ 0.0031 ........ 0.0046 ...... 0.0062 ........ 0.0041
7-bit uint #2 ........ 0.0021 ........ 0.0043 ...... 0.0062 ........ 0.0041
7-bit uint #3 ........ 0.0022 ........ 0.0044 ...... 0.0061 ........ 0.0040
5-bit sint #1 ........ 0.0030 ........ 0.0048 ...... 0.0062 ........ 0.0040
5-bit sint #2 ........ 0.0032 ........ 0.0046 ...... 0.0062 ........ 0.0040
5-bit sint #3 ........ 0.0031 ........ 0.0046 ...... 0.0062 ........ 0.0040
8-bit uint #1 ........ 0.0054 ........ 0.0079 ...... 0.0062 ........ 0.0050
8-bit uint #2 ........ 0.0051 ........ 0.0079 ...... 0.0064 ........ 0.0044
8-bit uint #3 ........ 0.0051 ........ 0.0082 ...... 0.0062 ........ 0.0044
16-bit uint #1 ....... 0.0077 ........ 0.0094 ...... 0.0065 ........ 0.0045
16-bit uint #2 ....... 0.0077 ........ 0.0094 ...... 0.0063 ........ 0.0045
16-bit uint #3 ....... 0.0077 ........ 0.0095 ...... 0.0064 ........ 0.0047
32-bit uint #1 ....... 0.0088 ........ 0.0119 ...... 0.0063 ........ 0.0043
32-bit uint #2 ....... 0.0089 ........ 0.0117 ...... 0.0062 ........ 0.0039
32-bit uint #3 ....... 0.0089 ........ 0.0118 ...... 0.0063 ........ 0.0044
64-bit uint #1 ....... 0.0097 ........ 0.0155 ...... 0.0063 ........ 0.0045
64-bit uint #2 ....... 0.0095 ........ 0.0153 ...... 0.0061 ........ 0.0045
64-bit uint #3 ....... 0.0096 ........ 0.0156 ...... 0.0063 ........ 0.0047
8-bit int #1 ......... 0.0053 ........ 0.0083 ...... 0.0062 ........ 0.0044
8-bit int #2 ......... 0.0052 ........ 0.0080 ...... 0.0062 ........ 0.0044
8-bit int #3 ......... 0.0052 ........ 0.0080 ...... 0.0062 ........ 0.0043
16-bit int #1 ........ 0.0089 ........ 0.0097 ...... 0.0069 ........ 0.0046
16-bit int #2 ........ 0.0075 ........ 0.0093 ...... 0.0063 ........ 0.0043
16-bit int #3 ........ 0.0075 ........ 0.0094 ...... 0.0062 ........ 0.0046
32-bit int #1 ........ 0.0086 ........ 0.0122 ...... 0.0063 ........ 0.0044
32-bit int #2 ........ 0.0087 ........ 0.0120 ...... 0.0066 ........ 0.0046
32-bit int #3 ........ 0.0086 ........ 0.0121 ...... 0.0060 ........ 0.0044
64-bit int #1 ........ 0.0096 ........ 0.0149 ...... 0.0060 ........ 0.0045
64-bit int #2 ........ 0.0096 ........ 0.0157 ...... 0.0062 ........ 0.0044
64-bit int #3 ........ 0.0096 ........ 0.0160 ...... 0.0063 ........ 0.0046
64-bit int #4 ........ 0.0097 ........ 0.0157 ...... 0.0061 ........ 0.0044
64-bit float #1 ...... 0.0079 ........ 0.0153 ...... 0.0056 ........ 0.0044
64-bit float #2 ...... 0.0079 ........ 0.0152 ...... 0.0057 ........ 0.0045
64-bit float #3 ...... 0.0079 ........ 0.0155 ...... 0.0057 ........ 0.0044
fix string #1 ........ 0.0010 ........ 0.0045 ...... 0.0071 ........ 0.0044
fix string #2 ........ 0.0048 ........ 0.0075 ...... 0.0070 ........ 0.0060
fix string #3 ........ 0.0048 ........ 0.0086 ...... 0.0068 ........ 0.0060
fix string #4 ........ 0.0050 ........ 0.0088 ...... 0.0070 ........ 0.0059
8-bit string #1 ...... 0.0081 ........ 0.0129 ...... 0.0069 ........ 0.0062
8-bit string #2 ...... 0.0086 ........ 0.0128 ...... 0.0069 ........ 0.0065
8-bit string #3 ...... 0.0086 ........ 0.0126 ...... 0.0115 ........ 0.0065
16-bit string #1 ..... 0.0105 ........ 0.0137 ...... 0.0128 ........ 0.0068
16-bit string #2 ..... 0.1510 ........ 0.1486 ...... 0.1526 ........ 0.1391
32-bit string ........ 0.1517 ........ 0.1475 ...... 0.1504 ........ 0.1370
wide char string #1 .. 0.0044 ........ 0.0085 ...... 0.0067 ........ 0.0057
wide char string #2 .. 0.0081 ........ 0.0125 ...... 0.0069 ........ 0.0063
8-bit binary #1 ........... I ............. I ........... F ............. I
8-bit binary #2 ........... I ............. I ........... F ............. I
8-bit binary #3 ........... I ............. I ........... F ............. I
16-bit binary ............. I ............. I ........... F ............. I
32-bit binary ............. I ............. I ........... F ............. I
fix array #1 ......... 0.0014 ........ 0.0059 ...... 0.0132 ........ 0.0055
fix array #2 ......... 0.0146 ........ 0.0156 ...... 0.0155 ........ 0.0148
fix array #3 ......... 0.0211 ........ 0.0229 ...... 0.0179 ........ 0.0180
16-bit array #1 ...... 0.0673 ........ 0.0498 ...... 0.0343 ........ 0.0388
16-bit array #2 ........... S ............. S ........... S ............. S
32-bit array .............. S ............. S ........... S ............. S
complex array ............. I ............. I ........... F ............. F
fix map #1 ................ I ............. I ........... F ............. I
fix map #2 ........... 0.0148 ........ 0.0180 ...... 0.0156 ........ 0.0179
fix map #3 ................ I ............. I ........... F ............. I
fix map #4 ........... 0.0252 ........ 0.0201 ...... 0.0214 ........ 0.0167
16-bit map #1 ........ 0.1027 ........ 0.0836 ...... 0.0388 ........ 0.0510
16-bit map #2 ............. S ............. S ........... S ............. S
32-bit map ................ S ............. S ........... S ............. S
complex map .......... 0.1104 ........ 0.1010 ...... 0.0556 ........ 0.0602
fixext 1 .................. I ............. I ........... F ............. F
fixext 2 .................. I ............. I ........... F ............. F
fixext 4 .................. I ............. I ........... F ............. F
fixext 8 .................. I ............. I ........... F ............. F
fixext 16 ................. I ............. I ........... F ............. F
8-bit ext ................. I ............. I ........... F ............. F
16-bit ext ................ I ............. I ........... F ............. F
32-bit ext ................ I ............. I ........... F ............. F
32-bit timestamp #1 ....... I ............. I ........... F ............. F
32-bit timestamp #2 ....... I ............. I ........... F ............. F
64-bit timestamp #1 ....... I ............. I ........... F ............. F
64-bit timestamp #2 ....... I ............. I ........... F ............. F
64-bit timestamp #3 ....... I ............. I ........... F ............. F
96-bit timestamp #1 ....... I ............. I ........... F ............. F
96-bit timestamp #2 ....... I ............. I ........... F ............. F
96-bit timestamp #3 ....... I ............. I ........... F ............. F
===========================================================================
Total 0.9642 1.0909 0.8224 0.7213
Skipped 4 4 4 4
Failed 0 0 24 17
Ignored 24 24 0 7
Note that the msgpack extension (v2.1.2) doesn't support ext, bin and UTF-8 str types.
The library is released under the MIT License. See the bundled LICENSE file for details.
Author: rybakit
Source Code: https://github.com/rybakit/msgpack.php
License: MIT License
1659640560
Job scheduler for Ruby (at, cron, in and every jobs).
It uses threads.
Note: maybe are you looking for the README of rufus-scheduler 2.x? (especially if you're using Dashing which is stuck on rufus-scheduler 2.0.24)
Quickstart:
# quickstart.rb
require 'rufus-scheduler'
scheduler = Rufus::Scheduler.new
scheduler.in '3s' do
puts 'Hello... Rufus'
end
scheduler.join
#
# let the current thread join the scheduler thread
#
# (please note that this join should be removed when scheduling
# in a web application (Rails and friends) initializer)
(run with ruby quickstart.rb
)
Various forms of scheduling are supported:
require 'rufus-scheduler'
scheduler = Rufus::Scheduler.new
# ...
scheduler.in '10d' do
# do something in 10 days
end
scheduler.at '2030/12/12 23:30:00' do
# do something at a given point in time
end
scheduler.every '3h' do
# do something every 3 hours
end
scheduler.every '3h10m' do
# do something every 3 hours and 10 minutes
end
scheduler.cron '5 0 * * *' do
# do something every day, five minutes after midnight
# (see "man 5 crontab" in your terminal)
end
# ...
Rufus-scheduler uses fugit for parsing time strings, et-orbi for pairing time and tzinfo timezones.
Rufus-scheduler (out of the box) is an in-process, in-memory scheduler. It uses threads.
It does not persist your schedules. When the process is gone and the scheduler instance with it, the schedules are gone.
A rufus-scheduler instance will go on scheduling while it is present among the objects in a Ruby process. To make it stop scheduling you have to call its #shutdown
method.
(please note: rufus-scheduler is not a cron replacement)
It's a complete rewrite of rufus-scheduler.
There is no EventMachine-based scheduler anymore.
I'll drive you right to the tracks.
scheduler.every('100') {
will schedule every 100 seconds (previously, it would have been 0.1s). This aligns rufus-scheduler with Ruby's sleep(100)
every '10m'
job is on, it will trigger once at wakeup, not 6 times (discard_past was false by default in rufus-scheduler 2.x). No intention to re-introduce discard_past: false
in 3.0 for now.So you need help. People can help you, but first help them help you, and don't waste their time. Provide a complete description of the issue. If it works on A but not on B and others have to ask you: "so what is different between A and B" you are wasting everyone's time.
"hello", "please" and "thanks" are not swear words.
Go read how to report bugs effectively, twice.
Update: help_help.md might help help you.
You can find help via chat over at https://gitter.im/floraison/fugit. It's fugit, et-orbi, and rufus-scheduler combined chat room.
Please be courteous.
Yes, issues can be reported in rufus-scheduler issues, I'd actually prefer bugs in there. If there is nothing wrong with rufus-scheduler, a Stack Overflow question is better.
Rufus-scheduler supports five kinds of jobs. in, at, every, interval and cron jobs.
Most of the rufus-scheduler examples show block scheduling, but it's also OK to schedule handler instances or handler classes.
In and at jobs trigger once.
require 'rufus-scheduler'
scheduler = Rufus::Scheduler.new
scheduler.in '10d' do
puts "10 days reminder for review X!"
end
scheduler.at '2014/12/24 2000' do
puts "merry xmas!"
end
In jobs are scheduled with a time interval, they trigger after that time elapsed. At jobs are scheduled with a point in time, they trigger when that point in time is reached (better to choose a point in the future).
Every, interval and cron jobs trigger repeatedly.
require 'rufus-scheduler'
scheduler = Rufus::Scheduler.new
scheduler.every '3h' do
puts "change the oil filter!"
end
scheduler.interval '2h' do
puts "thinking..."
puts sleep(rand * 1000)
puts "thought."
end
scheduler.cron '00 09 * * *' do
puts "it's 9am! good morning!"
end
Every jobs try hard to trigger following the frequency they were scheduled with.
Interval jobs trigger, execute and then trigger again after the interval elapsed. (every jobs time between trigger times, interval jobs time between trigger termination and the next trigger start).
Cron jobs are based on the venerable cron utility (man 5 crontab
). They trigger following a pattern given in (almost) the same language cron uses.
schedule_in, schedule_at, schedule_cron, etc will return the new Job instance.
in, at, cron will return the new Job instance's id (a String).
job_id =
scheduler.in '10d' do
# ...
end
job = scheduler.job(job_id)
# versus
job =
scheduler.schedule_in '10d' do
# ...
end
# also
job =
scheduler.in '10d', job: true do
# ...
end
Sometimes it pays to be less verbose.
The #schedule
methods schedules an at, in or cron job. It just decides based on its input. It returns the Job instance.
scheduler.schedule '10d' do; end.class
# => Rufus::Scheduler::InJob
scheduler.schedule '2013/12/12 12:30' do; end.class
# => Rufus::Scheduler::AtJob
scheduler.schedule '* * * * *' do; end.class
# => Rufus::Scheduler::CronJob
The #repeat
method schedules and returns an EveryJob or a CronJob.
scheduler.repeat '10d' do; end.class
# => Rufus::Scheduler::EveryJob
scheduler.repeat '* * * * *' do; end.class
# => Rufus::Scheduler::CronJob
(Yes, no combination here gives back an IntervalJob).
A schedule block may be given 0, 1 or 2 arguments.
The first argument is "job", it's simply the Job instance involved. It might be useful if the job is to be unscheduled for some reason.
scheduler.every '10m' do |job|
status = determine_pie_status
if status == 'burnt' || status == 'cooked'
stop_oven
takeout_pie
job.unschedule
end
end
The second argument is "time", it's the time when the job got cleared for triggering (not Time.now).
Note that time is the time when the job got cleared for triggering. If there are mutexes involved, now = mutex_wait_time + time...
It's OK to change the next_time of an every job in-flight:
scheduler.every '10m' do |job|
# ...
status = determine_pie_status
job.next_time = Time.now + 30 * 60 if status == 'burnt'
#
# if burnt, wait 30 minutes for the oven to cool a bit
end
It should work as well with cron jobs, not so with interval jobs whose next_time is computed after their block ends its current run.
It's OK to pass any object, as long as it responds to #call(), when scheduling:
class Handler
def self.call(job, time)
p "- Handler called for #{job.id} at #{time}"
end
end
scheduler.in '10d', Handler
# or
class OtherHandler
def initialize(name)
@name = name
end
def call(job, time)
p "* #{time} - Handler #{name.inspect} called for #{job.id}"
end
end
oh = OtherHandler.new('Doe')
scheduler.every '10m', oh
scheduler.in '3d5m', oh
The call method must accept 2 (job, time), 1 (job) or 0 arguments.
Note that time is the time when the job got cleared for triggering. If there are mutexes involved, now = mutex_wait_time + time...
One can pass a handler class to rufus-scheduler when scheduling. Rufus will instantiate it and that instance will be available via job#handler.
class MyHandler
attr_reader :count
def initialize
@count = 0
end
def call(job)
@count += 1
puts ". #{self.class} called at #{Time.now} (#{@count})"
end
end
job = scheduler.schedule_every '35m', MyHandler
job.handler
# => #<MyHandler:0x000000021034f0>
job.handler.count
# => 0
If you want to keep that "block feeling":
job_id =
scheduler.every '10m', Class.new do
def call(job)
puts ". hello #{self.inspect} at #{Time.now}"
end
end
The scheduler can be paused via the #pause and #resume methods. One can determine if the scheduler is currently paused by calling #paused?.
While paused, the scheduler still accepts schedules, but no schedule will get triggered as long as #resume isn't called.
Sets the name of the job.
scheduler.cron '*/15 8 * * *', name: 'Robert' do |job|
puts "A, it's #{Time.now} and my name is #{job.name}"
end
job1 =
scheduler.schedule_cron '*/30 9 * * *', n: 'temporary' do |job|
puts "B, it's #{Time.now} and my name is #{job.name}"
end
# ...
job1.name = 'Beowulf'
By default, jobs are triggered in their own, new threads. When blocking: true
, the job is triggered in the scheduler thread (a new thread is not created). Yes, while a blocking job is running, the scheduler is not scheduling.
Since, by default, jobs are triggered in their own new threads, job instances might overlap. For example, a job that takes 10 minutes and is scheduled every 7 minutes will have overlaps.
To prevent overlap, one can set overlap: false
. Such a job will not trigger if one of its instances is already running.
The :overlap
option is considered before the :mutex
option when the scheduler is reviewing jobs for triggering.
When a job with a mutex triggers, the job's block is executed with the mutex around it, preventing other jobs with the same mutex from entering (it makes the other jobs wait until it exits the mutex).
This is different from overlap: false
, which is, first, limited to instances of the same job, and, second, doesn't make the incoming job instance block/wait but give up.
:mutex
accepts a mutex instance or a mutex name (String). It also accept an array of mutex names / mutex instances. It allows for complex relations between jobs.
Array of mutexes: original idea and implementation by Rainux Luo
Note: creating lots of different mutexes is OK. Rufus-scheduler will place them in its Scheduler#mutexes hash... And they won't get garbage collected.
The :overlap
option is considered before the :mutex
option when the scheduler is reviewing jobs for triggering.
It's OK to specify a timeout when scheduling some work. After the time specified, it gets interrupted via a Rufus::Scheduler::TimeoutError.
scheduler.in '10d', timeout: '1d' do
begin
# ... do something
rescue Rufus::Scheduler::TimeoutError
# ... that something got interrupted after 1 day
end
end
The :timeout option accepts either a duration (like "1d" or "2w3d") or a point in time (like "2013/12/12 12:00").
This option is for repeat jobs (cron / every) only.
It's used to specify the first time after which the repeat job should trigger for the first time.
In the case of an "every" job, this will be the first time (modulo the scheduler frequency) the job triggers. For a "cron" job as well, the :first will point to the first time the job has to trigger, the following trigger times are then determined by the cron string.
scheduler.every '2d', first_at: Time.now + 10 * 3600 do
# ... every two days, but start in 10 hours
end
scheduler.every '2d', first_in: '10h' do
# ... every two days, but start in 10 hours
end
scheduler.cron '00 14 * * *', first_in: '3d' do
# ... every day at 14h00, but start after 3 * 24 hours
end
:first, :first_at and :first_in all accept a point in time or a duration (number or time string). Use the symbol you think makes your schedule more readable.
Note: it's OK to change the first_at (a Time instance) directly:
job.first_at = Time.now + 10
job.first_at = Rufus::Scheduler.parse('2029-12-12')
The first argument (in all its flavours) accepts a :now or :immediately value. That schedules the first occurrence for immediate triggering. Consider:
require 'rufus-scheduler'
s = Rufus::Scheduler.new
n = Time.now; p [ :scheduled_at, n, n.to_f ]
s.every '3s', first: :now do
n = Time.now; p [ :in, n, n.to_f ]
end
s.join
that'll output something like:
[:scheduled_at, 2014-01-22 22:21:21 +0900, 1390396881.344438]
[:in, 2014-01-22 22:21:21 +0900, 1390396881.6453865]
[:in, 2014-01-22 22:21:24 +0900, 1390396884.648807]
[:in, 2014-01-22 22:21:27 +0900, 1390396887.651686]
[:in, 2014-01-22 22:21:30 +0900, 1390396890.6571937]
...
This option is for repeat jobs (cron / every) only.
It indicates the point in time after which the job should unschedule itself.
scheduler.cron '5 23 * * *', last_in: '10d' do
# ... do something every evening at 23:05 for 10 days
end
scheduler.every '10m', last_at: Time.now + 10 * 3600 do
# ... do something every 10 minutes for 10 hours
end
scheduler.every '10m', last_in: 10 * 3600 do
# ... do something every 10 minutes for 10 hours
end
:last, :last_at and :last_in all accept a point in time or a duration (number or time string). Use the symbol you think makes your schedule more readable.
Note: it's OK to change the last_at (nil or a Time instance) directly:
job.last_at = nil
# remove the "last" bound
job.last_at = Rufus::Scheduler.parse('2029-12-12')
# set the last bound
One can tell how many times a repeat job (CronJob or EveryJob) is to execute before unscheduling by itself.
scheduler.every '2d', times: 10 do
# ... do something every two days, but not more than 10 times
end
scheduler.cron '0 23 * * *', times: 31 do
# ... do something every day at 23:00 but do it no more than 31 times
end
It's OK to assign nil to :times to make sure the repeat job is not limited. It's useful when the :times is determined at scheduling time.
scheduler.cron '0 23 * * *', times: (nolimit ? nil : 10) do
# ...
end
The value set by :times is accessible in the job. It can be modified anytime.
job =
scheduler.cron '0 23 * * *' do
# ...
end
# later on...
job.times = 10
# 10 days and it will be over
When calling a schedule method, the id (String) of the job is returned. Longer schedule methods return Job instances directly. Calling the shorter schedule methods with the job: true
also returns Job instances instead of Job ids (Strings).
require 'rufus-scheduler'
scheduler = Rufus::Scheduler.new
job_id =
scheduler.in '10d' do
# ...
end
job =
scheduler.schedule_in '1w' do
# ...
end
job =
scheduler.in '1w', job: true do
# ...
end
Those Job instances have a few interesting methods / properties:
Returns the job id.
job = scheduler.schedule_in('10d') do; end
job.id
# => "in_1374072446.8923042_0.0_0"
Returns the scheduler instance itself.
Returns the options passed at the Job creation.
job = scheduler.schedule_in('10d', tag: 'hello') do; end
job.opts
# => { :tag => 'hello' }
Returns the original schedule.
job = scheduler.schedule_in('10d', tag: 'hello') do; end
job.original
# => '10d'
callable() returns the scheduled block (or the call method of the callable object passed in lieu of a block)
handler() returns nil if a block was scheduled and the instance scheduled otherwise.
# when passing a block
job =
scheduler.schedule_in('10d') do
# ...
end
job.handler
# => nil
job.callable
# => #<Proc:0x00000001dc6f58@/home/jmettraux/whatever.rb:115>
and
# when passing something else than a block
class MyHandler
attr_reader :counter
def initialize
@counter = 0
end
def call(job, time)
@counter = @counter + 1
end
end
job = scheduler.schedule_in('10d', MyHandler.new)
job.handler
# => #<Method: MyHandler#call>
job.callable
# => #<MyHandler:0x0000000163ae88 @counter=0>
Added to rufus-scheduler 3.8.0.
Returns the array [ 'path/to/file.rb', 123 ]
like Proc#source_location
does.
require 'rufus-scheduler'
scheduler = Rufus::Scheduler.new
job = scheduler.schedule_every('2h') { p Time.now }
p job.source_location
# ==> [ '/home/jmettraux/rufus-scheduler/test.rb', 6 ]
Returns the Time instance when the job got created.
job = scheduler.schedule_in('10d', tag: 'hello') do; end
job.scheduled_at
# => 2013-07-17 23:48:54 +0900
Returns the last time the job triggered (is usually nil for AtJob and InJob).
job = scheduler.schedule_every('10s') do; end
job.scheduled_at
# => 2013-07-17 23:48:54 +0900
job.last_time
# => nil (since we've just scheduled it)
# after 10 seconds
job.scheduled_at
# => 2013-07-17 23:48:54 +0900 (same as above)
job.last_time
# => 2013-07-17 23:49:04 +0900
Returns the previous #next_time
scheduler.every('10s') do |job|
puts "job scheduled for #{job.previous_time} triggered at #{Time.now}"
puts "next time will be around #{job.next_time}"
puts "."
end
The job keeps track of how long its work was in the last_work_time
attribute. For a one time job (in, at) it's probably not very useful.
The attribute mean_work_time
contains a computed mean work time. It's recomputed after every run (if it's a repeat job).
Returns an array of EtOrbi::EoTime
instances (Time instances with a designated time zone), listing the n
next occurrences for this job.
Please note that for "interval" jobs, a mean work time is computed each time and it's used by this #next_times(n)
method to approximate the next times beyond the immediate next time.
Unschedule the job, preventing it from firing again and removing it from the schedule. This doesn't prevent a running thread for this job to run until its end.
Returns the list of threads currently "hosting" runs of this Job instance.
Interrupts all the work threads currently running for this job instance. They discard their work and are free for their next run (of whatever job).
Note: this doesn't unschedule the Job instance.
Note: if the job is pooled for another run, a free work thread will probably pick up that next run and the job will appear as running again. You'd have to unschedule and kill to make sure the job doesn't run again.
Returns true if there is at least one running Thread hosting a run of this Job instance.
Returns true if the job is scheduled (is due to trigger). For repeat jobs it should return true until the job gets unscheduled. "at" and "in" jobs will respond with false as soon as they start running (execution triggered).
These four methods are only available to CronJob, EveryJob and IntervalJob instances. One can pause or resume such jobs thanks to these methods.
job =
scheduler.schedule_every('10s') do
# ...
end
job.pause
# => 2013-07-20 01:22:22 +0900
job.paused?
# => true
job.paused_at
# => 2013-07-20 01:22:22 +0900
job.resume
# => nil
Returns the list of tags attached to this Job instance.
By default, returns an empty array.
job = scheduler.schedule_in('10d') do; end
job.tags
# => []
job = scheduler.schedule_in('10d', tag: 'hello') do; end
job.tags
# => [ 'hello' ]
Threads have thread-local variables, similarly Rufus-scheduler jobs have job-local variables. Those are more like a dict with thread-safe access.
job =
@scheduler.schedule_every '1s' do |job|
job[:timestamp] = Time.now.to_f
job[:counter] ||= 0
job[:counter] += 1
end
sleep 3.6
job[:counter]
# => 3
job.key?(:timestamp) # => true
job.has_key?(:timestamp) # => true
job.keys # => [ :timestamp, :counter ]
Locals can be set at schedule time:
job0 =
@scheduler.schedule_cron '*/15 12 * * *', locals: { a: 0 } do
# ...
end
job1 =
@scheduler.schedule_cron '*/15 13 * * *', l: { a: 1 } do
# ...
end
One can fetch the Hash directly with Job#locals
. Of course, direct manipulation is not thread-safe.
job.locals.entries do |k, v|
p "#{k}: #{v}"
end
Job instances have a #call method. It simply calls the scheduled block or callable immediately.
job =
@scheduler.schedule_every '10m' do |job|
# ...
end
job.call
Warning: the Scheduler#on_error handler is not involved. Error handling is the responsibility of the caller.
If the call has to be rescued by the error handler of the scheduler, call(true)
might help:
require 'rufus-scheduler'
s = Rufus::Scheduler.new
def s.on_error(job, err)
if job
p [ 'error in scheduled job', job.class, job.original, err.message ]
else
p [ 'error while scheduling', err.message ]
end
rescue
p $!
end
job =
s.schedule_in('1d') do
fail 'again'
end
job.call(true)
#
# true lets the error_handler deal with error in the job call
Returns when the job will trigger (hopefully).
An alias for time.
Returns the next time the job will trigger (hopefully).
Returns how many times the job fired.
It returns the scheduling frequency. For a job scheduled "every 20s", it's 20.
It's used to determine if the job frequency is higher than the scheduler frequency (it raises an ArgumentError if that is the case).
Returns the interval scheduled between each execution of the job.
Every jobs use a time duration between each start of their execution, while interval jobs use a time duration between the end of an execution and the start of the next.
An expensive method to run, it's brute. It caches its results. By default it runs for 2017 (a non leap-year).
require 'rufus-scheduler'
Rufus::Scheduler.parse('* * * * *').brute_frequency
#
# => #<Fugit::Cron::Frequency:0x00007fdf4520c5e8
# @span=31536000.0, @delta_min=60, @delta_max=60,
# @occurrences=525600, @span_years=1.0, @yearly_occurrences=525600.0>
#
# Occurs 525600 times in a span of 1 year (2017) and 1 day.
# There are least 60 seconds between "triggers" and at most 60 seconds.
Rufus::Scheduler.parse('0 12 * * *').brute_frequency
# => #<Fugit::Cron::Frequency:0x00007fdf451ec6d0
# @span=31536000.0, @delta_min=86400, @delta_max=86400,
# @occurrences=365, @span_years=1.0, @yearly_occurrences=365.0>
Rufus::Scheduler.parse('0 12 * * *').brute_frequency.to_debug_s
# => "dmin: 1D, dmax: 1D, ocs: 365, spn: 52W1D, spnys: 1, yocs: 365"
#
# 365 occurrences, at most 1 day between each, at least 1 day.
The CronJob#frequency
method found in rufus-scheduler < 3.5 has been retired.
The scheduler #job(job_id)
method can be used to look up Job instances.
require 'rufus-scheduler'
scheduler = Rufus::Scheduler.new
job_id =
scheduler.in '10d' do
# ...
end
# later on...
job = scheduler.job(job_id)
Are methods for looking up lists of scheduled Job instances.
Here is an example:
#
# let's unschedule all the at jobs
scheduler.at_jobs.each(&:unschedule)
When scheduling a job, one can specify one or more tags attached to the job. These can be used to look up the job later on.
scheduler.in '10d', tag: 'main_process' do
# ...
end
scheduler.in '10d', tags: [ 'main_process', 'side_dish' ] do
# ...
end
# ...
jobs = scheduler.jobs(tag: 'main_process')
# find all the jobs with the 'main_process' tag
jobs = scheduler.jobs(tags: [ 'main_process', 'side_dish' ]
# find all the jobs with the 'main_process' AND 'side_dish' tags
Returns the list of Job instance that have currently running instances.
Whereas other "_jobs" method scan the scheduled job list, this method scans the thread list to find the job. It thus comprises jobs that are running but are not scheduled anymore (that happens for at and in jobs).
Unschedule a job given directly or by its id.
Shuts down the scheduler, ceases any scheduler/triggering activity.
Shuts down the scheduler, waits (blocks) until all the jobs cease running.
Shuts down the scheduler, waits (blocks) at most n seconds until all the jobs cease running. (Jobs are killed after n seconds have elapsed).
Kills all the job (threads) and then shuts the scheduler down. Radical.
Returns true if the scheduler has been shut down.
Returns the Time instance at which the scheduler got started.
Returns since the count of seconds for which the scheduler has been running.
#uptime_s
returns this count in a String easier to grasp for humans, like "3d12m45s123"
.
Lets the current thread join the scheduling thread in rufus-scheduler. The thread comes back when the scheduler gets shut down.
#join
is mostly used in standalone scheduling script (or tiny one file examples). Calling #join
from a web application initializer will probably hijack the main thread and prevent the web application from being served. Do not put a #join
in such a web application initializer file.
Returns all the threads associated with the scheduler, including the scheduler thread itself.
Lists the work threads associated with the scheduler. The query option defaults to :all.
Note that the main schedule thread will be returned if it is currently running a Job (ie one of those blocking: true
jobs).
Returns true if the arg is a currently scheduled job (see Job#scheduled?).
Returns a hash { job => [ t0, t1, ... ] }
mapping jobs to their potential trigger time within the [ time0, time1 ]
span.
Please note that, for interval jobs, the #mean_work_time
is used, so the result is only a prediction.
Like #occurrences
but returns a list [ [ t0, job0 ], [ t1, job1 ], ... ]
of time + job pairs.
The easy, job-granular way of dealing with errors is to rescue and deal with them immediately. The two next sections show examples. Skip them for explanations on how to deal with errors at the scheduler level.
As said, jobs could take care of their errors themselves.
scheduler.every '10m' do
begin
# do something that might fail...
rescue => e
$stderr.puts '-' * 80
$stderr.puts e.message
$stderr.puts e.stacktrace
$stderr.puts '-' * 80
end
end
Jobs are not only shrunk to blocks, here is how the above would look like with a dedicated class.
scheduler.every '10m', Class.new do
def call(job)
# do something that might fail...
rescue => e
$stderr.puts '-' * 80
$stderr.puts e.message
$stderr.puts e.stacktrace
$stderr.puts '-' * 80
end
end
TODO: talk about callable#on_error (if implemented)
(see scheduling handler instances and scheduling handler classes for more about those "callable jobs")
By default, rufus-scheduler intercepts all errors (that inherit from StandardError) and dumps abundant details to $stderr.
If, for example, you'd like to divert that flow to another file (descriptor), you can reassign $stderr for the current Ruby process
$stderr = File.open('/var/log/myapplication.log', 'ab')
or, you can limit that reassignement to the scheduler itself
scheduler.stderr = File.open('/var/log/myapplication.log', 'ab')
We've just seen that, by default, rufus-scheduler dumps error information to $stderr. If one needs to completely change what happens in case of error, it's OK to overwrite #on_error
def scheduler.on_error(job, error)
Logger.warn("intercepted error in #{job.id}: #{error.message}")
end
On Rails, the on_error
method redefinition might look like:
def scheduler.on_error(job, error)
Rails.logger.error(
"err#{error.object_id} rufus-scheduler intercepted #{error.inspect}" +
" in job #{job.inspect}")
error.backtrace.each_with_index do |line, i|
Rails.logger.error(
"err#{error.object_id} #{i}: #{line}")
end
end
One can bind callbacks before and after jobs trigger:
s = Rufus::Scheduler.new
def s.on_pre_trigger(job, trigger_time)
puts "triggering job #{job.id}..."
end
def s.on_post_trigger(job, trigger_time)
puts "triggered job #{job.id}."
end
s.every '1s' do
# ...
end
The trigger_time
is the time at which the job triggers. It might be a bit before Time.now
.
Warning: these two callbacks are executed in the scheduler thread, not in the work threads (the threads where the job execution really happens).
One can create an around callback which will wrap a job:
def s.around_trigger(job)
t = Time.now
puts "Starting job #{job.id}..."
yield
puts "job #{job.id} finished in #{Time.now-t} seconds."
end
The around callback is executed in the thread.
Returning false
in on_pre_trigger will prevent the job from triggering. Returning anything else (nil, -1, true, ...) will let the job trigger.
Note: your business logic should go in the scheduled block itself (or the scheduled instance). Don't put business logic in on_pre_trigger. Return false for admin reasons (backend down, etc), not for business reasons that are tied to the job itself.
def s.on_pre_trigger(job, trigger_time)
return false if Backend.down?
puts "triggering job #{job.id}..."
end
By default, rufus-scheduler sleeps 0.300 second between every step. At each step it checks for jobs to trigger and so on.
The :frequency option lets you change that 0.300 second to something else.
scheduler = Rufus::Scheduler.new(frequency: 5)
It's OK to use a time string to specify the frequency.
scheduler = Rufus::Scheduler.new(frequency: '2h10m')
# this scheduler will sleep 2 hours and 10 minutes between every "step"
Use with care.
This feature only works on OSes that support the flock (man 2 flock) call.
Starting the scheduler with lockfile: '.rufus-scheduler.lock'
will make the scheduler attempt to create and lock the file .rufus-scheduler.lock
in the current working directory. If that fails, the scheduler will not start.
The idea is to guarantee only one scheduler (in a group of schedulers sharing the same lockfile) is running.
This is useful in environments where the Ruby process holding the scheduler gets started multiple times.
If the lockfile mechanism here is not sufficient, you can plug your custom mechanism. It's explained in advanced lock schemes below.
(since rufus-scheduler 3.0.9)
The scheduler lock is an object that responds to #lock
and #unlock
. The scheduler calls #lock
when starting up. If the answer is false
, the scheduler stops its initialization work and won't schedule anything.
Here is a sample of a scheduler lock that only lets the scheduler on host "coffee.example.com" start:
class HostLock
def initialize(lock_name)
@lock_name = lock_name
end
def lock
@lock_name == `hostname -f`.strip
end
def unlock
true
end
end
scheduler =
Rufus::Scheduler.new(scheduler_lock: HostLock.new('coffee.example.com'))
By default, the scheduler_lock is an instance of Rufus::Scheduler::NullLock
, with a #lock
that returns true.
(since rufus-scheduler 3.0.9)
The trigger lock in an object that responds to #lock
. The scheduler calls that method on the job lock right before triggering any job. If the answer is false, the trigger doesn't happen, the job is not done (at least not in this scheduler).
Here is a (stupid) PingLock example, it'll only trigger if an "other host" is not responding to ping. Do not use that in production, you don't want to fork a ping process for each trigger attempt...
class PingLock
def initialize(other_host)
@other_host = other_host
end
def lock
! system("ping -c 1 #{@other_host}")
end
end
scheduler =
Rufus::Scheduler.new(trigger_lock: PingLock.new('main.example.com'))
By default, the trigger_lock is an instance of Rufus::Scheduler::NullLock
, with a #lock
that always returns true.
As explained in advanced lock schemes, another way to tune that behaviour is by overriding the scheduler's #confirm_lock
method. (You could also do that with an #on_pre_trigger
callback).
In rufus-scheduler 2.x, by default, each job triggering received its own, brand new, thread of execution. In rufus-scheduler 3.x, execution happens in a pooled work thread. The max work thread count (the pool size) defaults to 28.
One can set this maximum value when starting the scheduler.
scheduler = Rufus::Scheduler.new(max_work_threads: 77)
It's OK to increase the :max_work_threads of a running scheduler.
scheduler.max_work_threads += 10
Do not want to store a reference to your rufus-scheduler instance? Then Rufus::Scheduler.singleton
can help, it returns a singleton instance of the scheduler, initialized the first time this class method is called.
Rufus::Scheduler.singleton.every '10s' { puts "hello, world!" }
It's OK to pass initialization arguments (like :frequency or :max_work_threads) but they will only be taken into account the first time .singleton
is called.
Rufus::Scheduler.singleton(max_work_threads: 77)
Rufus::Scheduler.singleton(max_work_threads: 277) # no effect
The .s
is a shortcut for .singleton
.
Rufus::Scheduler.s.every '10s' { puts "hello, world!" }
As seen above, rufus-scheduler proposes the :lockfile system out of the box. If in a group of schedulers only one is supposed to run, the lockfile mechanism prevents schedulers that have not set/created the lockfile from running.
There are situations where this is not sufficient.
By overriding #lock and #unlock, one can customize how schedulers lock.
This example was provided by Eric Lindvall:
class ZookeptScheduler < Rufus::Scheduler
def initialize(zookeeper, opts={})
@zk = zookeeper
super(opts)
end
def lock
@zk_locker = @zk.exclusive_locker('scheduler')
@zk_locker.lock # returns true if the lock was acquired, false else
end
def unlock
@zk_locker.unlock
end
def confirm_lock
return false if down?
@zk_locker.assert!
rescue ZK::Exceptions::LockAssertionFailedError => e
# we've lost the lock, shutdown (and return false to at least prevent
# this job from triggering
shutdown
false
end
end
This uses a zookeeper to make sure only one scheduler in a group of distributed schedulers runs.
The methods #lock and #unlock are overridden and #confirm_lock is provided, to make sure that the lock is still valid.
The #confirm_lock method is called right before a job triggers (if it is provided). The more generic callback #on_pre_trigger is called right after #confirm_lock.
(introduced in rufus-scheduler 3.0.9).
Another way of prodiving #lock
, #unlock
and #confirm_lock
to a rufus-scheduler is by using the :scheduler_lock
and :trigger_lock
options.
See :trigger_lock and :scheduler_lock.
The scheduler lock may be used to prevent a scheduler from starting, while a trigger lock prevents individual jobs from triggering (the scheduler goes on scheduling).
One has to be careful with what goes in #confirm_lock
or in a trigger lock, as it gets called before each trigger.
Warning: you may think you're heading towards "high availability" by using a trigger lock and having lots of schedulers at hand. It may be so if you limit yourself to scheduling the same set of jobs at scheduler startup. But if you add schedules at runtime, they stay local to their scheduler. There is no magic that propagates the jobs to all the schedulers in your pack.
(Please note that fugit does the heavy-lifting parsing work for rufus-scheduler).
Rufus::Scheduler provides a class method .parse
to parse time durations and cron strings. It's what it's using when receiving schedules. One can use it directly (no need to instantiate a Scheduler).
require 'rufus-scheduler'
Rufus::Scheduler.parse('1w2d')
# => 777600.0
Rufus::Scheduler.parse('1.0w1.0d')
# => 777600.0
Rufus::Scheduler.parse('Sun Nov 18 16:01:00 2012').strftime('%c')
# => 'Sun Nov 18 16:01:00 2012'
Rufus::Scheduler.parse('Sun Nov 18 16:01:00 2012 Europe/Berlin').strftime('%c %z')
# => 'Sun Nov 18 15:01:00 2012 +0000'
Rufus::Scheduler.parse(0.1)
# => 0.1
Rufus::Scheduler.parse('* * * * *')
# => #<Fugit::Cron:0x00007fb7a3045508
# @original="* * * * *", @cron_s=nil,
# @seconds=[0], @minutes=nil, @hours=nil, @monthdays=nil, @months=nil,
# @weekdays=nil, @zone=nil, @timezone=nil>
It returns a number when the input is a duration and a Fugit::Cron instance when the input is a cron string.
It will raise an ArgumentError if it can't parse the input.
Beyond .parse
, there are also .parse_cron
and .parse_duration
, for finer granularity.
There is an interesting helper method named .to_duration_hash
:
require 'rufus-scheduler'
Rufus::Scheduler.to_duration_hash(60)
# => { :m => 1 }
Rufus::Scheduler.to_duration_hash(62.127)
# => { :m => 1, :s => 2, :ms => 127 }
Rufus::Scheduler.to_duration_hash(62.127, drop_seconds: true)
# => { :m => 1 }
To schedule something at noon every first Monday of the month:
scheduler.cron('00 12 * * mon#1') do
# ...
end
To schedule something at noon the last Sunday of every month:
scheduler.cron('00 12 * * sun#-1') do
# ...
end
#
# OR
#
scheduler.cron('00 12 * * sun#L') do
# ...
end
Such cronlines can be tested with scripts like:
require 'rufus-scheduler'
Time.now
# => 2013-10-26 07:07:08 +0900
Rufus::Scheduler.parse('* * * * mon#1').next_time.to_s
# => 2013-11-04 00:00:00 +0900
L can be used in the "day" slot:
In this example, the cronline is supposed to trigger every last day of the month at noon:
require 'rufus-scheduler'
Time.now
# => 2013-10-26 07:22:09 +0900
Rufus::Scheduler.parse('00 12 L * *').next_time.to_s
# => 2013-10-31 12:00:00 +0900
It's OK to pass negative values in the "day" slot:
scheduler.cron '0 0 -5 * *' do
# do it at 00h00 5 days before the end of the month...
end
Negative ranges (-10--5-
: 10 days before the end of the month to 5 days before the end of the month) are OK, but mixed positive / negative ranges will raise an ArgumentError
.
Negative ranges with increments (-10---2/2
) are accepted as well.
Descending day ranges are not accepted (10-8
or -8--10
for example).
Cron schedules and at schedules support the specification of a timezone.
scheduler.cron '0 22 * * 1-5 America/Chicago' do
# the job...
end
scheduler.at '2013-12-12 14:00 Pacific/Samoa' do
puts "it's tea time!"
end
# or even
Rufus::Scheduler.parse("2013-12-12 14:00 Pacific/Saipan")
# => #<Rufus::Scheduler::ZoTime:0x007fb424abf4e8 @seconds=1386820800.0, @zone=#<TZInfo::DataTimezone: Pacific/Saipan>, @time=nil>
For when you see an error like:
rufus-scheduler/lib/rufus/scheduler/zotime.rb:41:
in `initialize':
cannot determine timezone from nil (etz:nil,tnz:"中国标准时间",tzid:nil)
(ArgumentError)
from rufus-scheduler/lib/rufus/scheduler/zotime.rb:198:in `new'
from rufus-scheduler/lib/rufus/scheduler/zotime.rb:198:in `now'
from rufus-scheduler/lib/rufus/scheduler.rb:561:in `start'
...
It may happen on Windows or on systems that poorly hint to Ruby which timezone to use. It should be solved by setting explicitly the ENV['TZ']
before the scheduler instantiation:
ENV['TZ'] = 'Asia/Shanghai'
scheduler = Rufus::Scheduler.new
scheduler.every '2s' do
puts "#{Time.now} Hello #{ENV['TZ']}!"
end
On Rails you might want to try with:
ENV['TZ'] = Time.zone.name # Rails only
scheduler = Rufus::Scheduler.new
scheduler.every '2s' do
puts "#{Time.now} Hello #{ENV['TZ']}!"
end
(Hat tip to Alexander in gh-230)
Rails sets its timezone under config/application.rb
.
Rufus-Scheduler 3.3.3 detects the presence of Rails and uses its timezone setting (tested with Rails 4), so setting ENV['TZ']
should not be necessary.
The value can be determined thanks to https://en.wikipedia.org/wiki/List_of_tz_database_time_zones.
Use a "continent/city" identifier (for example "Asia/Shanghai"). Do not use an abbreviation (not "CST") and do not use a local time zone name (not "中国标准时间" nor "Eastern Standard Time" which, for instance, points to a time zone in America and to another one in Australia...).
If the error persists (and especially on Windows), try to add the tzinfo-data
to your Gemfile, as in:
gem 'tzinfo-data'
or by manually requiring it before requiring rufus-scheduler (if you don't use Bundler):
require 'tzinfo/data'
require 'rufus-scheduler'
Yes, I know, all of the above is boring and you're only looking for a snippet to paste in your Ruby-on-Rails application to schedule...
Here is an example initializer:
#
# config/initializers/scheduler.rb
require 'rufus-scheduler'
# Let's use the rufus-scheduler singleton
#
s = Rufus::Scheduler.singleton
# Stupid recurrent task...
#
s.every '1m' do
Rails.logger.info "hello, it's #{Time.now}"
Rails.logger.flush
end
And now you tell me that this is good, but you want to schedule stuff from your controller.
Maybe:
class ScheController < ApplicationController
# GET /sche/
#
def index
job_id =
Rufus::Scheduler.singleton.in '5s' do
Rails.logger.info "time flies, it's now #{Time.now}"
end
render text: "scheduled job #{job_id}"
end
end
The rufus-scheduler singleton is instantiated in the config/initializers/scheduler.rb
file, it's then available throughout the webapp via Rufus::Scheduler.singleton
.
Warning: this works well with single-process Ruby servers like Webrick and Thin. Using rufus-scheduler with Passenger or Unicorn requires a bit more knowledge and tuning, gently provided by a bit of googling and reading, see Faq above.
(Written in reply to gh-186)
If you don't want rufus-scheduler to trigger anything while running the Ruby on Rails console, running for tests/specs, or running from a Rake task, you can insert a conditional return statement before jobs are added to the scheduler instance:
#
# config/initializers/scheduler.rb
require 'rufus-scheduler'
return if defined?(Rails::Console) || Rails.env.test? || File.split($PROGRAM_NAME).last == 'rake'
#
# do not schedule when Rails is run from its console, for a test/spec, or
# from a Rake task
# return if $PROGRAM_NAME.include?('spring')
#
# see https://github.com/jmettraux/rufus-scheduler/issues/186
s = Rufus::Scheduler.singleton
s.every '1m' do
Rails.logger.info "hello, it's #{Time.now}"
Rails.logger.flush
end
(Beware later version of Rails where Spring takes care pre-running the initializers. Running spring stop
or disabling Spring might be necessary in some cases to see changes to initializers being taken into account.)
(Written in reply to https://github.com/jmettraux/rufus-scheduler/issues/165 )
There is the handy rails server -d
that starts a development Rails as a daemon. The annoying thing is that the scheduler as seen above is started in the main process that then gets forked and daemonized. The rufus-scheduler thread (and any other thread) gets lost, no scheduling happens.
I avoid running -d
in development mode and bother about daemonizing only for production deployment.
These are two well crafted articles on process daemonization, please read them:
If, anyway, you need something like rails server -d
, why not try bundle exec unicorn -D
instead? In my (limited) experience, it worked out of the box (well, had to add gem 'unicorn'
to Gemfile
first).
You might benefit from wraping your scheduled code in the executor or reloader. Read more here: https://guides.rubyonrails.org/threading_and_code_execution.html
see getting help above.
Author: jmettraux
Source code: https://github.com/jmettraux/rufus-scheduler
License: MIT license
1657785244
In today’s tutorial, we will learn how to create a Custom Video Player. To build this project, we need HTML, CSS and Javascript.
00:00 Intro
00:05 Preview
02:58 HTML & CSS
35:26 Step 1: Create Initial References
45:46 Step 2: Implement slider() For Volume
51:33 Step 3: Detect Device Type
57:27 Step 4: Implement Functionality For Play & Pause Button
01:03:04 Step 5: Hide/ Show Playback Speed Options
01:08:47 Step 6: Function To Set Playback Speed.
01:12:59 Step 7: Function To Mute Video
01:18:24 Step 8: Function To Set Volume
01:24:55 Step 9: Function To Set Fullscreen
01:31:47 Step 10: Function To Exit Fullscreen
01:40:08 Step 11: Function To Format Current Time & Total Time
01:44:46 Step 12: Function To Update Progress & Timer
01:50:13 Step 13: Implement Click Event On Progress Bar
01:57:26 Step 14: Function On Window Load
Before we start coding let us take a look at the project folder structure. We create a project folder called – ‘Custom Video Player’. Inside this folder, we have three files. The first file is index.html which is the HTML document. Next, we have style.css which is the stylesheet. Finally, we have script.js which is the script file.
We start with the HTML code. First, copy the code below and paste it into your HTML document.
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Custom Video Player</title>
<!-- Font Awesome Icons -->
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.1.1/css/all.min.css"
/>
<!-- Google Fonts -->
<link
href="https://fonts.googleapis.com/css2?family=Roboto+Mono&display=swap"
rel="stylesheet"
/>
<!-- Stylesheet -->
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="container">
<div class="rotate-container hide">
<div id="rotate-icon">
<i class="fa-solid fa-rotate-left"></i>
<p>Rotate for a better experience</p>
</div>
</div>
<div class="video-container" id="video-container">
<video id="my-video" preload="metadata">
<source
src="https://dl.dropbox.com/s/l90y72zm97ayzhx/my%20video.mp4?raw=1"
type="video/mp4"
/>
Your browser does not support the video tag
</video>
<div class="controls" id="controls">
<div class="progress-container flex-space">
<div id="progress-bar">
<div id="current-progress"></div>
</div>
<div class="song-timer">
<span id="current-time">00:00</span>
<span>/</span>
<span id="max-duration">00:00</span>
</div>
</div>
<div id="video-controls" class="video-controls flex-space">
<div class="container-1 flex">
<div>
<!-- Play video -->
<button id="play-btn" class="control-btn">
<i class="fa-solid fa-play"></i>
</button>
<!-- Pause video-->
<button id="pauseButton" class="control-btn hide">
<i class="fa-solid fa-pause"></i>
</button>
</div>
<!-- volume of video-->
<div id="volume" class="volume flex">
<span id="high">
<i class="fa-solid fa-volume-high"></i>
</span>
<span class="hide" id="low">
<i class="fa-solid fa-volume-low"></i>
</span>
<span class="hide" id="mute">
<i class="fa-solid fa-volume-xmark"></i>
</span>
<input
type="range"
min="0"
max="100"
value="50"
id="volume-range"
oninput="slider()"
/>
<span id="volume-num">50</span>
</div>
</div>
<div class="container-2 flex-space">
<div class="playback">
<button id="playback-speed-btn">1x</button>
<div class="playback-options hide">
<button onclick="setPlayback(0.5)">0.5</button>
<button onclick="setPlayback(1.0)">1</button>
<button onclick="setPlayback(2.0)">2</button>
</div>
</div>
<!-- screen size -->
<div id="size-screen">
<button id="screen-expand">
<i class="fa-solid fa-expand"></i>
</button>
<button id="screen-compress" class="hide">
<i class="fa-solid fa-compress"></i>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Script -->
<script src="script.js"></script>
</body>
</html>
Next, we style our video player using CSS. For this copy, the code provided to you below and paste it into your stylesheet.
* {
padding: 0;
margin: 0;
box-sizing: border-box;
outline: none;
color: #ffffff;
font-family: "Roboto Mono", monospace;
}
body {
background-color: #2887e3;
}
.flex {
display: flex;
}
.flex-space {
display: flex;
justify-content: space-between;
}
.container {
padding: 1em 0;
}
#my-video {
width: 100%;
}
.rotate-container {
top: 0;
position: absolute;
height: 100%;
width: 100%;
background-color: rgba(0, 0, 0, 0.3);
display: flex;
justify-content: center;
align-items: center;
}
#rotate-icon {
display: flex;
flex-direction: column;
color: #dddddd;
text-align: center;
}
.hide {
display: none;
}
.video-container {
width: 60%;
position: absolute;
transform: translate(-50%, -50%);
left: 50%;
top: 50%;
box-shadow: 20px 30px 50px rgba(0, 0, 0, 0.2);
}
.controls {
position: absolute;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(35, 34, 39, 0.8);
}
.progress-container {
align-items: center;
padding: 0 0.5em;
}
.video-controls {
flex-direction: row;
align-items: center;
}
#progress-bar {
position: relative;
width: 75%;
height: 5px;
background-color: #000000;
margin: 1em 0;
vertical-align: 2px;
border-radius: 5px;
cursor: pointer;
}
.song-timer {
font-size: 0.8em;
width: 25%;
text-align: right;
}
#current-progress {
position: absolute;
left: 0;
display: inline-block;
height: 5px;
width: 0;
background: #2887e3;
border-radius: 5px;
}
#current-progress:after {
content: "";
position: absolute;
left: calc(100% - 1.5px);
top: -2.5px;
width: 10px;
height: 10px;
border-radius: 50%;
background-color: #ffffff;
}
.playback {
position: relative;
}
.control-btn,
#screen-expand,
#screen-compress {
width: 3em;
height: 3em;
outline: none;
border: none;
background-color: transparent;
}
#size-screen {
margin-left: auto;
}
.volume {
align-items: center;
margin-left: 0.6em;
}
#volume-range {
position: relative;
margin: 0 0.5em;
cursor: pointer;
height: 5px;
-webkit-appearance: none;
background-color: #000000;
border-radius: 5px;
outline: none;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
height: 10px;
width: 10px;
background-color: #2887e3;
border-radius: 50%;
cursor: pointer;
border: none;
}
.fa-solid {
font-size: 1.1rem;
}
.container-2 {
width: 10%;
min-width: 70px;
align-items: center;
}
#playback-speed-btn {
position: relative;
background-color: transparent;
border: 1px solid #ffffff;
color: #ffffff;
font-size: 0.9rem;
border-radius: 5px;
padding: 0.3em 0.25em;
cursor: pointer;
}
.playback-options {
position: absolute;
bottom: 0;
background-color: #000000;
min-width: 5em;
box-shadow: 0 8px 16px 0 rgba(0, 0, 0, 0.2);
z-index: 1;
}
.playback-options button {
color: #ffffff;
border-left: 0;
border-right: 0;
border-top: 0;
width: 100%;
background-color: transparent;
padding: 1em;
text-decoration: none;
display: block;
}
@media all and (display-mode: fullscreen) {
.container {
padding: 0;
}
.video-container {
width: 100%;
margin: 0;
}
.controls {
position: absolute;
display: block;
bottom: 0;
left: 0;
width: 100%;
z-index: 2;
}
#progress-bar {
width: 80%;
}
.song-timer {
width: 20%;
font-size: 1.2em;
}
.fa-solid {
color: #dddddd;
}
}
@media only screen and (max-width: 768px) {
.video-container,
.controls {
width: 100%;
}
span {
display: inline;
}
#progress-bar {
width: 60%;
}
.song-timer {
width: 40%;
font-size: 0.9em;
}
.fa-solid {
font-size: 1rem;
}
.control-btn,
#screen-expand,
#screen-compress {
width: 2em;
height: 1.5em;
}
}
@media only screen and (max-width: 768px) and (display-mode: fullscreen) {
.video-container {
margin-top: 50%;
}
}
Lastly, we add functionality to our custom video player using Javascript. Once again copy the code below and paste it into your script file.
We do this in fourteen steps:
Create initial references.
Implement slider()
Detect device type.
Implement functionality for the play and pause button.
Hide/Show playback speed options
Function to set playback speed.
Logic to mute video.
Function to set Fullscreen.
Function to exit Fullscreen.
Create a function to format the current time & maximum time.
Create a function to update progress & timer.
Implement a click event on the progress bar.
Function on window load.
let videoContainer = document.querySelector(".video-container");
let container = document.querySelector(".container");
let myVideo = document.getElementById("my-video");
let rotateContainer = document.querySelector(".rotate-container");
let videoControls = document.querySelector(".controls");
let playButton = document.getElementById("play-btn");
let pauseButton = document.getElementById("pauseButton");
let volume = document.getElementById("volume");
let volumeRange = document.getElementById("volume-range");
let volumeNum = document.getElementById("volume-num");
let high = document.getElementById("high");
let low = document.getElementById("low");
let mute = document.getElementById("mute");
let sizeScreen = document.getElementById("size-screen");
let screenCompress = document.getElementById("screen-compress");
let screenExpand = document.getElementById("screen-expand");
const currentProgress = document.getElementById("current-progress");
const currentTimeRef = document.getElementById("current-time");
const maxDuration = document.getElementById("max-duration");
const progressBar = document.getElementById("progress-bar");
const playbackSpeedButton = document.getElementById("playback-speed-btn");
const playbackContainer = document.querySelector(".playback");
const playbackSpeedOptions = document.querySelector(".playback-options");
function slider() {
valPercent = (volumeRange.value / volumeRange.max) * 100;
volumeRange.style.background = `linear-gradient(to right, #2887e3 ${valPercent}%, #000000 ${valPercent}%)`;
}
//events object
let events = {
mouse: {
click: "click",
},
touch: {
click: "touchstart",
},
};
let deviceType = "";
//Detech touch device
const isTouchDevice = () => {
try {
//We try to create TouchEvent (it would fail for desktops and throw error)
document.createEvent("TouchEvent");
deviceType = "touch";
return true;
} catch (e) {
deviceType = "mouse";
return false;
}
};
//play and pause button
playButton.addEventListener("click", () => {
myVideo.play();
pauseButton.classList.remove("hide");
playButton.classList.add("hide");
});
pauseButton.addEventListener(
"click",
(pauseVideo = () => {
myVideo.pause();
pauseButton.classList.add("hide");
playButton.classList.remove("hide");
})
);
//playback
playbackContainer.addEventListener("click", () => {
playbackSpeedOptions.classList.remove("hide");
});
//if user clicks outside or on the option
window.addEventListener("click", (e) => {
if (!playbackContainer.contains(e.target)) {
playbackSpeedOptions.classList.add("hide");
} else if (playbackSpeedOptions.contains(e.target)) {
playbackSpeedOptions.classList.add("hide");
}
});
//playback speed
const setPlayback = (value) => {
playbackSpeedButton.innerText = value + "x";
myVideo.playbackRate = value;
};
//mute video
const muter = () => {
mute.classList.remove("hide");
high.classList.add("hide");
low.classList.add("hide");
myVideo.volume = 0;
volumeNum.innerHTML = 0;
volumeRange.value = 0;
slider();
};
//when user click on high and low volume then mute the audio
high.addEventListener("click", muter);
low.addEventListener("click", muter);
//for volume
volumeRange.addEventListener("input", () => {
//for converting % to decimal values since video.volume would accept decimals only
let volumeValue = volumeRange.value / 100;
myVideo.volume = volumeValue;
volumeNum.innerHTML = volumeRange.value;
//mute icon, low volume, high volume icons
if (volumeRange.value < 50) {
low.classList.remove("hide");
high.classList.add("hide");
mute.classList.add("hide");
} else if (volumeRange.value > 50) {
low.classList.add("hide");
high.classList.remove("hide");
mute.classList.add("hide");
}
});
//Screen size
screenExpand.addEventListener("click", () => {
screenCompress.classList.remove("hide");
screenExpand.classList.add("hide");
videoContainer
.requestFullscreen()
.catch((err) => alert("Your device doesn't support full screen API"));
if (isTouchDevice) {
let screenOrientation =
screen.orientation || screen.mozOrientation || screen.msOrientation;
if (screenOrientation.type == "portrait-primary") {
//update styling for fullscreen
pauseVideo();
rotateContainer.classList.remove("hide");
const myTimeout = setTimeout(() => {
rotateContainer.classList.add("hide");
}, 3000);
}
}
});
//if user presses escape the browser fire 'fullscreenchange' event
document.addEventListener("fullscreenchange", exitHandler);
document.addEventListener("webkitfullscreenchange", exitHandler);
document.addEventListener("mozfullscreenchange", exitHandler);
document.addEventListener("MSFullscreenchange", exitHandler);
function exitHandler() {
//if fullscreen is closed
if (
!document.fullscreenElement &&
!document.webkitIsFullScreen &&
!document.mozFullScreen &&
!document.msFullscreenElement
) {
normalScreen();
}
}
//back to normal screen
screenCompress.addEventListener(
"click",
(normalScreen = () => {
screenCompress.classList.add("hide");
screenExpand.classList.remove("hide");
if (document.fullscreenElement) {
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.mozCancelFullScreen) {
document.mozCancelFullScreen();
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen();
}
}
})
);
//Format time
const timeFormatter = (timeInput) => {
let minute = Math.floor(timeInput / 60);
minute = minute < 10 ? "0" + minute : minute;
let second = Math.floor(timeInput % 60);
second = second < 10 ? "0" + second : second;
return `${minute}:${second}`;
};
//Update progress every second
setInterval(() => {
currentTimeRef.innerHTML = timeFormatter(myVideo.currentTime);
currentProgress.style.width =
(myVideo.currentTime / myVideo.duration.toFixed(3)) * 100 + "%";
}, 1000);
//update timer
myVideo.addEventListener("timeupdate", () => {
currentTimeRef.innerText = timeFormatter(myVideo.currentTime);
});
//If user click on progress bar
isTouchDevice();
progressBar.addEventListener(events[deviceType].click, (event) => {
//start of progressbar
let coordStart = progressBar.getBoundingClientRect().left;
//mouse click position
let coordEnd = !isTouchDevice() ? event.clientX : event.touches[0].clientX;
let progress = (coordEnd - coordStart) / progressBar.offsetWidth;
//set width to progress
currentProgress.style.width = progress * 100 + "%";
//set time
myVideo.currentTime = progress * myVideo.duration;
//play
myVideo.play();
pauseButton.classList.remove("hide");
playButton.classList.add("hide");
});
window.onload = () => {
//display duration
myVideo.onloadedmetadata = () => {
maxDuration.innerText = timeFormatter(myVideo.duration);
};
slider();
};
That’s it for this tutorial. If you face any issues while creating this code, you can download the source code by clicking the ‘Download Code’
📁 Download Source Code : https://www.codingartistweb.com
#html #css #javascript #webdev
1669952228
In this tutorial, you'll learn: What is Dijkstra's Algorithm and how Dijkstra's algorithm works with the help of visual guides.
You can use algorithms in programming to solve specific problems through a set of precise instructions or procedures.
Dijkstra's algorithm is one of many graph algorithms you'll come across. It is used to find the shortest path from a fixed node to all other nodes in a graph.
There are different representations of Dijkstra's algorithm. You can either find the shortest path between two nodes, or the shortest path from a fixed node to the rest of the nodes in a graph.
In this article, you'll learn how Dijkstra's algorithm works with the help of visual guides.
Before we dive into more detailed visual examples, you need to understand how Dijkstra's algorithm works.
Although the theoretical explanation may seem a bit abstract, it'll help you understand the practical aspect better.
In a given graph containing different nodes, we are required to get the shortest path from a given node to the rest of the nodes.
These nodes can represent any object like the names of cities, letters, and so on.
Between each node is a number denoting the distance between two nodes, as you can see in the image below:
We usually work with two arrays – one for visited nodes, and another for unvisited nodes. You'll learn more about the arrays in the next section.
When a node is visited, the algorithm calculates how long it took to get to the node and stores the distance. If a shorter path to a node is found, the initial value assigned for the distance is updated.
Note that a node cannot be visited twice.
The algorithm runs recursively until all the nodes have been visited.
In this section, we'll take a look at a practical example that shows how Dijkstra's algorithm works.
Here's the graph we'll be working with:
We'll use the table below to put down the visited nodes and their distance from the fixed node:
NODE | SHORTEST DISTANCE FROM FIXED NODE |
---|---|
A | ∞ |
B | ∞ |
C | ∞ |
D | ∞ |
E | ∞ |
Visited nodes = []
Unvisited nodes = [A,B,C,D,E]
Above, we have a table showing each node and the shortest distance from the that node to the fixed node. We are yet to choose the fixed node.
Note that the distance for each node in the table is currently denoted as infinity (∞). This is because we don't know the shortest distance yet.
We also have two arrays – visited and unvisited. Whenever a node is visited, it is added to the visited nodes array.
Let's get started!
To simplify things, I'll break the process down into iterations. You'll see what happens in each step with the aid of diagrams.
The first iteration might seem confusing, but that's totally fine. Once we start repeating the process in each iteration, you'll have a clearer picture of how the algorithm works.
Step #1 - Pick an unvisited node
We'll choose A as the fixed node. So we'll find the shortest distance from A to every other node in the graph.
We're going to give A a distance of 0 because it is the initial node. So the table would look like this:
NODE | SHORTEST DISTANCE FROM FIXED NODE |
---|---|
A | 0 |
B | ∞ |
C | ∞ |
D | ∞ |
E | ∞ |
Step #2 - Find the distance from current node
The next thing to do after choosing a node is to find the distance from it to the unvisited nodes around it.
The two unvisited nodes directly linked to A are B and C.
To get the distance from A to B:
0 + 4 = 4
0 being the value of the current node (A), and 4 being the distance between A and B in the graph.
To get the distance from A to C:
0 + 2 = 2
Step #3 - Update table with known distances
In the last step, we got 4 and 2 as the values of B and C respectively. So we'll update the table with those values:
NODE | SHORTEST DISTANCE FROM FIXED NODE |
---|---|
A | 0 |
B | 4 |
C | 2 |
D | ∞ |
E | ∞ |
Step #4 - Update arrays
At this point, the first iteration is complete. We'll move node A to the visited nodes array:
Visited nodes = [A]
Unvisited nodes = [B,C,D,E]
Before we proceed to the next iteration, you should know the following:
Step #1 - Pick an unvisited node
We have four unvisited nodes — [B,C,D,E]. So how do you know which node to pick for the next iteration?
Well, we pick the node with the smallest known distance recorded in the table. Here's the table:
NODE | SHORTEST DISTANCE FROM FIXED NODE |
---|---|
A | 0 |
B | 4 |
C | 2 |
D | ∞ |
E | ∞ |
So we're going with node C.
Step #2 - Find the distance from current node
To find the distance from the current node to the fixed node, we have to consider the nodes linked to the current node.
The nodes linked to the current node are A and B.
But A has been visited in the previous iteration so it will not be linked to the current node. That is:
From the diagram above,
To find the distance from C to B:
2 + 1 = 3
2 above is recorded distance for node C while 1 is the distance between C and B in the graph.
Step #3 - Update table with known distances
In the last step, we got the value of B to be 3. In the first iteration, it was 4.
We're going to update the distance in the table to 3.
NODE | SHORTEST DISTANCE FROM FIXED NODE |
---|---|
A | 0 |
B | 3 |
C | 2 |
D | ∞ |
E | ∞ |
So, A --> B = 4 (First iteration).
A --> C --> B = 3 (Second iteration).
The algorithm has helped us find the shortest path to B from A.
Step #4 - Update arrays
We're done with the last visited node. Let's add it to the visited nodes array:
Visited nodes = [A,C]
Unvisited nodes = [B,D,E]
Step #1 - Pick an unvisited node
We're down to three unvisited nodes — [B,D,E]. From the array, B has the shortest known distance.
To restate what is going on in the diagram above:
Step #2 - Find the distance from current node
The nodes linked to the current node are D and E.
B (the current node) has a value of 3. Therefore,
For node D, 3 + 3 = 6.
For node E, 3 + 2 = 5.
Step #3 - Update table with known distances
NODE | SHORTEST DISTANCE FROM FIXED NODE |
---|---|
A | 0 |
B | 3 |
C | 2 |
D | 6 |
E | 5 |
Step #4 - Update arrays
Visited nodes = [A,C,B]
Unvisited nodes = [D,E]
Step #1 - Pick an unvisited node
Like other iterations, we'll go with the unvisited node with the shortest known distance. That is E.
Step #2 - Find the distance from current node
According to our table, E has a value of 5.
For D in the current iteration,
5 + 5 = 10.
The value gotten for D here is 10, which is greater than the recorded value of 6 in the previous iteration. For this reason, we'll not update the table.
Step #3 - Update table with known distances
Our table remains the same:
NODE | SHORTEST DISTANCE FROM FIXED NODE |
---|---|
A | 0 |
B | 3 |
C | 2 |
D | 6 |
E | 5 |
Step #4 - Update arrays
Visited nodes = [A,C,B,E]
Unvisited nodes = [D]
Step #1 - Pick an unvisited node
We're currently left with one node in the unvisited array — D.
Step #2 - Find the distance from current node
The algorithm has gotten to the last iteration. This is because all nodes linked to the current node have been visited already so we can't link to them.
Step #3 - Update table with known distances
Our table remains the same:
NODE | SHORTEST DISTANCE FROM FIXED NODE |
---|---|
A | 0 |
B | 3 |
C | 2 |
D | 6 |
E | 5 |
At this point, we have updated the table with the shortest distance from the fixed node to every other node in the graph.
Step #4 - Update arrays
Visited nodes = [A,C,B,E,D]
Unvisited nodes = []
As can be seen above, we have no nodes left to visit. Using Dijkstra's algorithm, we've found the shortest distance from the fixed node to others nodes in the graph.
The pseudocode example in this section was gotten from Wikipedia. Here it is:
1 function Dijkstra(Graph, source):
2
3 for each vertex v in Graph.Vertices:
4 dist[v] ← INFINITY
5 prev[v] ← UNDEFINED
6 add v to Q
7 dist[source] ← 0
8
9 while Q is not empty:
10 u ← vertex in Q with min dist[u]
11 remove u from Q
12
13 for each neighbor v of u still in Q:
14 alt ← dist[u] + Graph.Edges(u, v)
15 if alt < dist[v]:
16 dist[v] ← alt
17 prev[v] ← u
18
19 return dist[], prev[]
Here are some of the common applications of Dijkstra's algorithm:
In this article, we talked about Dijkstra's algorithm. It is used to find the shortest distance from a fixed node to all other nodes in a graph.
We started by giving a brief summary of how the algorithm works.
We then had a look at an example that further explained Dijkstra's algorithm in steps using visual guides.
We concluded with a pseudocode example and some of the applications of Dijkstra's algorithm.
Happy coding!
Original article source at https://www.freecodecamp.org
#algorithm #datastructures
1677907260
Node.js client for the official ChatGPT API.
This package is a Node.js wrapper around ChatGPT by OpenAI. TS batteries included. ✨
March 1, 2023
The official OpenAI chat completions API has been released, and it is now the default for this package! 🔥
Method | Free? | Robust? | Quality? |
---|---|---|---|
ChatGPTAPI | ❌ No | ✅ Yes | ✅️ Real ChatGPT models |
ChatGPTUnofficialProxyAPI | ✅ Yes | ☑️ Maybe | ✅ Real ChatGPT |
Note: We strongly recommend using ChatGPTAPI
since it uses the officially supported API from OpenAI. We may remove support for ChatGPTUnofficialProxyAPI
in a future release.
ChatGPTAPI
- Uses the gpt-3.5-turbo-0301
model with the official OpenAI chat completions API (official, robust approach, but it's not free)ChatGPTUnofficialProxyAPI
- Uses an unofficial proxy server to access ChatGPT's backend API in a way that circumvents Cloudflare (uses the real ChatGPT and is pretty lightweight, but relies on a third-party server and is rate-limited)To run the CLI, you'll need an OpenAI API key:
export OPENAI_API_KEY="sk-TODO"
npx chatgpt "your prompt here"
By default, the response is streamed to stdout, the results are stored in a local config file, and every invocation starts a new conversation. You can use -c
to continue the previous conversation and --no-stream
to disable streaming.
Under the hood, the CLI uses ChatGPTAPI
with text-davinci-003
to mimic ChatGPT.
Usage:
$ chatgpt <prompt>
Commands:
<prompt> Ask ChatGPT a question
rm-cache Clears the local message cache
ls-cache Prints the local message cache path
For more info, run any command with the `--help` flag:
$ chatgpt --help
$ chatgpt rm-cache --help
$ chatgpt ls-cache --help
Options:
-c, --continue Continue last conversation (default: false)
-d, --debug Enables debug logging (default: false)
-s, --stream Streams the response (default: true)
-s, --store Enables the local message cache (default: true)
-t, --timeout Timeout in milliseconds
-k, --apiKey OpenAI API key
-n, --conversationName Unique name for the conversation
-h, --help Display this message
-v, --version Display version number
npm install chatgpt
Make sure you're using node >= 18
so fetch
is available (or node >= 14
if you install a fetch polyfill).
To use this module from Node.js, you need to pick between two methods:
Method | Free? | Robust? | Quality? |
---|---|---|---|
ChatGPTAPI | ❌ No | ✅ Yes | ✅️ Real ChatGPT models |
ChatGPTUnofficialProxyAPI | ✅ Yes | ☑️ Maybe | ✅ Real ChatGPT |
ChatGPTAPI
- Uses the gpt-3.5-turbo-0301
model with the official OpenAI chat completions API (official, robust approach, but it's not free). You can override the model, completion params, and system message to fully customize your assistant.
ChatGPTUnofficialProxyAPI
- Uses an unofficial proxy server to access ChatGPT's backend API in a way that circumvents Cloudflare (uses the real ChatGPT and is pretty lightweight, but relies on a third-party server and is rate-limited)
Both approaches have very similar APIs, so it should be simple to swap between them.
Note: We strongly recommend using ChatGPTAPI
since it uses the officially supported API from OpenAI. We may remove support for ChatGPTUnofficialProxyAPI
in a future release.
Sign up for an OpenAI API key and store it in your environment.
import { ChatGPTAPI } from 'chatgpt'
async function example() {
const api = new ChatGPTAPI({
apiKey: process.env.OPENAI_API_KEY
})
const res = await api.sendMessage('Hello World!')
console.log(res.text)
}
You can override the default model
(gpt-3.5-turbo-0301
) and any OpenAI chat completion params using completionParams
:
const api = new ChatGPTAPI({
apiKey: process.env.OPENAI_API_KEY,
completionParams: {
temperature: 0.5,
top_p: 0.8
}
})
If you want to track the conversation, you'll need to pass the parentMessageId
like this:
const api = new ChatGPTAPI({ apiKey: process.env.OPENAI_API_KEY })
// send a message and wait for the response
let res = await api.sendMessage('What is OpenAI?')
console.log(res.text)
// send a follow-up
res = await api.sendMessage('Can you expand on that?', {
parentMessageId: res.id
})
console.log(res.text)
// send another follow-up
res = await api.sendMessage('What were we talking about?', {
parentMessageId: res.id
})
console.log(res.text)
You can add streaming via the onProgress
handler:
const res = await api.sendMessage('Write a 500 word essay on frogs.', {
// print the partial response as the AI is "typing"
onProgress: (partialResponse) => console.log(partialResponse.text)
})
// print the full text at the end
console.log(res.text)
You can add a timeout using the timeoutMs
option:
// timeout after 2 minutes (which will also abort the underlying HTTP request)
const response = await api.sendMessage(
'write me a really really long essay on frogs',
{
timeoutMs: 2 * 60 * 1000
}
)
If you want to see more info about what's actually being sent to OpenAI's chat completions API, set the debug: true
option in the ChatGPTAPI
constructor:
const api = new ChatGPTAPI({
apiKey: process.env.OPENAI_API_KEY,
debug: true
})
We default to a basic systemMessage
. You can override this in either the ChatGPTAPI
constructor or sendMessage
:
const res = await api.sendMessage('what is the answer to the universe?', {
systemMessage: `You are ChatGPT, a large language model trained by OpenAI. You answer as concisely as possible for each responseIf you are generating a list, do not have too many items.
Current date: ${new Date().toISOString()}\n\n`
})
Note that we automatically handle appending the previous messages to the prompt and attempt to optimize for the available tokens (which defaults to 4096
).
Usage in CommonJS (Dynamic import)
async function example() {
// To use ESM in CommonJS, you can use a dynamic import
const { ChatGPTAPI } = await import('chatgpt')
const api = new ChatGPTAPI({ apiKey: process.env.OPENAI_API_KEY })
const res = await api.sendMessage('Hello World!')
console.log(res.text)
}
The API for ChatGPTUnofficialProxyAPI
is almost exactly the same. You just need to provide a ChatGPT accessToken
instead of an OpenAI API key.
import { ChatGPTUnofficialProxyAPI } from 'chatgpt'
async function example() {
const api = new ChatGPTUnofficialProxyAPI({
accessToken: process.env.OPENAI_ACCESS_TOKEN
})
const res = await api.sendMessage('Hello World!')
console.log(res.text)
}
See demos/demo-reverse-proxy for a full example:
npx tsx demos/demo-reverse-proxy.ts
ChatGPTUnofficialProxyAPI
messages also contain a conversationid
in addition to parentMessageId
, since the ChatGPT webapp can't reference messages across
You can override the reverse proxy by passing apiReverseProxyUrl
:
const api = new ChatGPTUnofficialProxyAPI({
accessToken: process.env.OPENAI_ACCESS_TOKEN,
apiReverseProxyUrl: 'https://your-example-server.com/api/conversation'
})
Known reverse proxies run by community members include:
Reverse Proxy URL | Author | Rate Limits | Last Checked |
---|---|---|---|
https://chat.duti.tech/api/conversation | @acheong08 | 120 req/min by IP | 2/19/2023 |
https://gpt.pawan.krd/backend-api/conversation | @PawanOsman | ? | 2/19/2023 |
Note: info on how the reverse proxies work is not being published at this time in order to prevent OpenAI from disabling access.
To use ChatGPTUnofficialProxyAPI
, you'll need an OpenAI access token from the ChatGPT webapp. To do this, you can use any of the following methods which take an email
and password
and return an access token:
These libraries work with email + password accounts (e.g., they do not support accounts where you auth via Microsoft / Google).
Alternatively, you can manually get an accessToken
by logging in to the ChatGPT webapp and then opening https://chat.openai.com/api/auth/session
, which will return a JSON object containing your accessToken
string.
Access tokens last for days.
Note: using a reverse proxy will expose your access token to a third-party. There shouldn't be any adverse effects possible from this, but please consider the risks before using this method.
See the auto-generated docs for more info on methods and parameters.
Most of the demos use ChatGPTAPI
. It should be pretty easy to convert them to use ChatGPTUnofficialProxyAPI
if you'd rather use that approach. The only thing that needs to change is how you initialize the api with an accessToken
instead of an apiKey
.
To run the included demos:
OPENAI_API_KEY
in .envA basic demo is included for testing purposes:
npx tsx demos/demo.ts
A demo showing on progress handler:
npx tsx demos/demo-on-progress.ts
The on progress demo uses the optional onProgress
parameter to sendMessage
to receive intermediary results as ChatGPT is "typing".
npx tsx demos/demo-conversation.ts
A persistence demo shows how to store messages in Redis for persistence:
npx tsx demos/demo-persistence.ts
Any keyv adaptor is supported for persistence, and there are overrides if you'd like to use a different way of storing / retrieving messages.
Note that persisting message is required for remembering the context of previous conversations beyond the scope of the current Node.js process, since by default, we only store messages in memory. Here's an external demo of using a completely custom database solution to persist messages.
Note: Persistence is handled automatically when using ChatGPTUnofficialProxyAPI
because it is connecting indirectly to ChatGPT.
All of these awesome projects are built using the chatgpt
package. 🤯
If you create a cool integration, feel free to open a PR and add it to the list.
node >= 14
.fetch
is installed.chatgpt
, we recommend using it only from your backend APIPrevious Updates
Feb 19, 2023
We now provide three ways of accessing the unofficial ChatGPT API, all of which have tradeoffs:
Method | Free? | Robust? | Quality? |
---|---|---|---|
ChatGPTAPI | ❌ No | ✅ Yes | ☑️ Mimics ChatGPT |
ChatGPTUnofficialProxyAPI | ✅ Yes | ☑️ Maybe | ✅ Real ChatGPT |
ChatGPTAPIBrowser (v3) | ✅ Yes | ❌ No | ✅ Real ChatGPT |
Note: I recommend that you use either ChatGPTAPI
or ChatGPTUnofficialProxyAPI
.
ChatGPTAPI
- Uses text-davinci-003
to mimic ChatGPT via the official OpenAI completions API (most robust approach, but it's not free and doesn't use a model fine-tuned for chat)ChatGPTUnofficialProxyAPI
- Uses an unofficial proxy server to access ChatGPT's backend API in a way that circumvents Cloudflare (uses the real ChatGPT and is pretty lightweight, but relies on a third-party server and is rate-limited)ChatGPTAPIBrowser
- (deprecated; v3.5.1 of this package) Uses Puppeteer to access the official ChatGPT webapp (uses the real ChatGPT, but very flaky, heavyweight, and error prone)Feb 5, 2023
OpenAI has disabled the leaked chat model we were previously using, so we're now defaulting to text-davinci-003
, which is not free.
We've found several other hidden, fine-tuned chat models, but OpenAI keeps disabling them, so we're searching for alternative workarounds.
Feb 1, 2023
This package no longer requires any browser hacks – it is now using the official OpenAI completions API with a leaked model that ChatGPT uses under the hood. 🔥
import { ChatGPTAPI } from 'chatgpt'
const api = new ChatGPTAPI({
apiKey: process.env.OPENAI_API_KEY
})
const res = await api.sendMessage('Hello World!')
console.log(res.text)
Please upgrade to chatgpt@latest
(at least v4.0.0). The updated version is significantly more lightweight and robust compared with previous versions. You also don't have to worry about IP issues or rate limiting.
Huge shoutout to @waylaidwanderer for discovering the leaked chat model!
If you run into any issues, we do have a pretty active Discord with a bunch of ChatGPT hackers from the Node.js & Python communities.
Lastly, please consider starring this repo and following me on twitter to help support the project.
Thanks && cheers, Travis
Author: Transitive-bullshit
Source Code: https://github.com/transitive-bullshit/chatgpt-api
License: MIT license