-
Notifications
You must be signed in to change notification settings - Fork 3k
RFC: Add a persistent term storage #1989
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
RFC: Add a persistent term storage #1989
Conversation
|
Sounds like it would be perfect for Cowboy's routing data, which rarely changes, can be fairly big, is passed on to every request process when they spawn, and from which only a small term is looked up to process said request. Passing on the |
|
Will it support functions as terms or it is limited to tuples, maps, lists and others? |
|
Yes, all term types (including funs) are supported. |
|
The global GC is a bit annoying but it seems unavoidable. Seems like a good way to kill patterns like mochiglobals and other "live-compiled modules as data stores" out there, which represented a legitimate need. I wouldn't imagine it displacing ETS or pdicts or anything like that in usage either ideally. |
|
@bjorng The functionality here looks very useful. I have two suggestions:
Happy to see the idea in Erlang/OTP. Having this will help to avoid the |
Looking up complex keys is more expensive than simple ones and having a "namespace" argument would mean all keys need to be complex. We don't want to force this cost on everyone, and we don't see this as a big problem as collisions are easily avoided by following the recommendation to use
This would not improve latency to any meaningful degree. If you have terms that are updated together we recommend storing them as a map or tuple instead. Persistent terms should not be your first choice for term storage. If you expect to change the values you should take a long hard look at ETS first. |
0289977 to
a0b336e
Compare
Ok, thanks for putting this into the documentation, this makes the usage clear.
My only other suggestion, is that the name The name If the name changes to use the word |
In this respect, |
|
@okeuday We did consider using constant but rejected it because we felt it was too confusing. Naming is one of the hardest problems in computer science (I won't name the other hard problems out of fear of making an off-by-one error). We finally settled on persistent because it seemed to be the least confusing of the names we considered. |
a0b336e to
07ef616
Compare
|
Isn't it more accurate to call them |
|
I jumped but then realized this has no relation to state machine replication / persistanceactors (like in scala). BTW is distributed Erlang in the picture? |
garazdawi
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Naming the feature has been the most problematic part. For a long time I was of the opinion that since we could not find a good name to describe what it is that we are doing, we should not do it. Or possibly put it into an already existing API that does the same thing.
Some crazy ideas that I came up with along the way are:
- to view it as a global process dictionary and put the API in erlang:put_term/2.
- to view it as a specialized ets table and put an option at table creation about the properties
- erts:put/2, Erlang Restricted Term Storage
- pets:put/2, Persistent Erlang Term Storage
- lets:put/2, Literal Erlang Term Storage
persistent_term is not a great name for this, because of it's associations with persisting to disk and to immutability. It is however in my oppionion better than the other suggestions, and this functionality is needed as shown by the usage of mochiglobal and similar libraries.
I don't think we have considered interned_term before, that may be a better name.
@vans163 This is a local optimization and has nothing to do with distributed Erlang.
erts/doc/src/persistent_term.xml
Outdated
| allocator also used for literals (constant terms) in BEAM code. | ||
| By default, 1 GB of virtual address space is reserved for literals | ||
| in BEAM code and persistent terms. The amount of virtual address | ||
| space reserved for literals can be changed by using the <c>MIscs</c> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+MIscs ? Link to docs?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will fix.
| Eterm res = NIL; | ||
|
|
||
| trap_data = (TrapData *) hp; | ||
| trap_data->header = make_pos_bignum_header(trap_data_size-1); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Creative way to create a trap context :)
|
Hmm, actually interning is generally implicit and automatic, the explicit version of interning is generally called a flyweight value (in C++-land anyway). Flyweight might be the most accurate, but I still prefer interned... |
|
I am not keen on the With Anyway, not trying to cause any interruption in the merge of this functionality. If |
|
|
Heh, oh wow, never associated that before (I tend to know the programming-related meanings of things)... >.> |
In the implementation of the zero-copying term storage, we want to preserve sharing, but not copy literals because the modules holding the literals could be unloaded under our feet.
07ef616 to
0161799
Compare
ERTS is already Erlang Runtime System How about cdb? Or ecdb? At least the term cdb already has some pre-existing use: Something that is quick to lookup but slow to modify because modification is basically rebuilding. |
| new_table = (HashTable *) data; | ||
| ASSERT(new_table->num_to_delete == 0); | ||
| erts_atomic_set_nob(&the_hash_table, (erts_aint_t)new_table); | ||
| erts_schedule_thr_prgr_later_op(table_deleter, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems safe to call release_update_permission(1) here directly after the_hash_table is set. table_deleter is a pure cleanup that no one needs to wait for.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed.
| } | ||
|
|
||
| static HashTable* | ||
| copy_table(HashTable* old_table, Uint new_size, int rehash) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How large do we expect this hash table to get? Do we need to trap while copying it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The recommendation in the documentation is to limit the total number of persistent terms. Therefore, I have assumed that the table is always small enough so that we don't have to trap.
|
|
||
| ASSERT(is_tuple_arity(old_term, 2)); | ||
| old_table->term[entry_index] = NIL; | ||
| old_table->num_entries--; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here in erase/1 you write into the active old_table. I think this can lead to all kinds of buggy behavior with concurrent readers like get/0.
Instead treat old_table as read only, copy it and make changes in new_table.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed.
| * Increment the ref counter to prevent an update operation (by put/2 | ||
| * or erase/1) to delete this hash table. | ||
| */ | ||
| erts_atomic_inc_nob(&hash_table->refc); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this hash_table->refc bump in get/0 and info/0 is not enough to keep the referred literals alive. If two or more updating operations are done before the trapping is done, the hash tables may be deallocated out of order and literals referred by this hash_table may be purged when a newer hash table is deallocated.
One solution could be to keep the hash tables in a linked list and make sure they are deallocated in order, older before newer.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in the way that you suggested.
While at it, I also made sure that there will not be a memory leak if a process is killed while calling get/0 and info/0.
e92d877 to
d3f0571
Compare
Introudce erts_queue_release_literals() to queue a literal area to be released.
23ed43b to
27fd0b6
Compare
Persistent terms are useful for storing Erlang terms that are never
or infrequently updated. They have the following advantages:
* Constant time access. A persistent term is not copied when it is
looked up. The constant factor is lower than for ETS, and no locks
are taken when looking up a term.
* Persistent terms are not copied in garbage collections.
* There is only ever one copy of a persistent term (until it is
deleted). That makes them useful for storing configuration data
that needs to be easily accessible by all processes.
Persistent terms have the following drawbacks:
* Updates are expensive. The hash table holding the keys for the
persistent terms are updated whenever a persistent term is added,
updated or deleted.
* Updating or deleting a persistent term triggers a "global GC", which
will schedule a heap scan of all processes to search the heap of all
processes for the deleted term. If a process still holds a reference
to the deleted term, the process will be garbage collected and the
term copied to the heap of the process. This global GC can make the
system less responsive for some time.
Three BIFs (implemented in C in the emulator) is the entire
interface to the persistent term functionality:
* put(Key, Value) to store a persistent term.
* get(Key) to look up a persistent term.
* erase(Key) to delete a persistent term.
There are also two additional BIFs to obtain information about
persistent terms:
* info() to return a map with information about persistent terms.
* get() to return a list of a {Key,Value} tuples for all persistent
terms. (The values are not copied.)
Co-authored-by: Siri Hansen <[email protected]>
d1a462b to
7489e81
Compare
Persistent terms are useful for storing Erlang terms that are never
or infrequently updated. They have the following advantages:
Constant time access. A persistent term is not copied when it is
looked up. The constant factor is lower than for ETS, and no locks
are taken when looking up a term.
Persistent terms are not copied in garbage collections.
There is only ever one copy of a persistent term (until it is
deleted). That makes them useful for storing configuration data
that needs to be easily accessible by all processes.
Persistent terms have the following drawbacks:
Updates are expensive. The hash table holding the keys for the
persistent terms are updated whenever a persistent term is added,
updated or deleted.
Updating or deleting a persistent term triggers a "global GC", which
will schedule a heap scan of all processes to search the heap of all
processes for the deleted term. If a process still holds a reference
to the deleted term, the process will be garbage collected and the
term copied to the heap of the process. This global GC can make the
system less responsive for some time.
Three BIFs (implemented in C in the emulator) is the entire
interface to the persistent term functionality:
put(Key, Value)to store a persistent term.get(Key)to look up a persistent term.erase(Key)to delete a persistent term.There are also two additional BIFs to obtain information about
persistent terms:
info()to return a map with information about persistent terms.get()to return a list of a {Key,Value} tuples for all persistentterms. (The values are not copied, but the keys are.)