Move navigation from _menu.json files in each navigation-parent directory to a naviga...
[ndcode_site.git] / blog / 20220114 / index.html.jst
1 return async env => {
2   let blog_post = await _require('/_lib/blog_post.jst')
3
4   await blog_post(
5     env,
6     // head
7     async _out => {},
8     // body
9     async _out => {
10       p {
11         'Normally I would write a blog post coincident with the release of a new version of the public website you are reading; in this case there is some heavy refactoring going on and so I have decided to just document the process of the work and let it be viewed and interacted with later when ready for release.'
12       }
13
14       h4 {'NoSQL discussion'}
15
16       p {
17         'One of the early decisions I made when teaching myself web development was that I would not be using SQL for the backend. I simply believe the disadvantages outweigh the advantages for what I am trying to do. And I’m probably not the only person with this view, because the NoSQL movement has been gaining traction.'
18       }
19
20       p {
21         'SQL pros:'
22         ul {
23           li {'Complex queries are possible: range queries, joins, and the like.'}
24           li {'SQL server can serve multiple webservers, increasing scalability.'}
25         }
26       }
27
28       p {
29         'SQL cons:'
30         ul {
31           li {'Deployment is a hassle, need to make sure the SQL server process is configured and running before launching the webserver process.'}
32           li {'SQL queries are submitted as code and need to be tokenized and parsed at the server for every request, which is not efficient.'}
33           li {'Administration has a steep learning curve, for example you cannot just view or edit the database file—you need to craft queries for troubleshooting and activities like migration or structure upgrades.'}
34           li {'Relational database capabilities are overkill for many trivial applications, such as storing a username against a session key.'}
35         }
36       }
37
38       p {
39         'I want my application to be as self-contained and self-configuring as possible, so basically I would just check out the website’s repository on the intended Virtual Private Server (VPS) and launch it. In a traditional configuration you have a long list of other services to configure first, such as PHP, PHP-FPM, MySQL, etc, etc, and setting up or migrating to a new server could be days of work with tedious troubleshooting. There are of course cases where these activities have a payoff (such as if you require a highly scalable website), but my case is not one of them.'
40       }
41
42       h4 {'First try—all JSON approach'}
43
44       p {
45         'Therefore, for a first cut I simply stored my website’s data as JSON files in a hidden folder of the site’s directory. The hidden folder and JSON files were created as required if not already existent. And a few sophisticated features were built around this to make things workable:'
46         ul {
47           li {'After first access, each JSON file was cached in RAM for the lifetime of the webserver process.'}
48           li {'After modification, the JSON files were not written back immediately, but a timer was set to guarantee modified data was written in 5 seconds or less.'}
49           li {'Writeback was done to a temporary file followed by a renaming step, and if the site crashed or lost power during renaming it could recover.'}
50         }
51       }
52
53       p {
54         'Admittedly, I could have constructed a similar system using '
55         tt {'SQLite'}
56         ', and I would then have access to complex queries which I do not with JSON, but I felt the added complexity was not worthwhile. If I need to do complex queries, I will simply write them in Javascript, including the maintenance of any indices needed to carry out the queries efficiently. This will give me complete control, rather than simply hoping the SQL server optimizes the indices and queries well.'
57       }
58
59       p {
60         'My JSON database was sufficient for building the prototype blog and online store that you can interact with now, but it did suffer from a serious problem which is that upon successful launch, there could be thousands of customers and/or orders and the database could potentially grow to a huge size. Then it would not be feasible to hold the entire file in memory or write it every 5 seconds.'
61       }
62
63       h4 {'Second try—log-structured JSON approach'}
64
65       p {
66         'To solve the scalability issue, my plan has always been to allow the JSON tree to be read into memory lazily as required (and cached, with some kind of cache policy implemented), and for modifications to be written to the end of the file. Thus the file would be log-structured and simply keep growing forever, except for the intervention of a daily cleaning routine which would copy all the live (still reachable) parts of the log into a new log to reduce the size.'
67       }
68
69       p {
70         'The database I created with this approach is called '
71         tt {'LogJSON'}
72         '. It means a database that appears to the calling code like a huge JSON tree, but one which can be accessed efficiently, and which has transaction ability to avoid concurrent updates corrupting the tree. The log-structuring is more or less hidden from the calling code, which provides the illusion of a mutable tree, even though data once written is immutable in the backend.'
73       }
74
75       p {
76         'Why log-structured? Mainly for simplicity and efficiency. If you allow deletion in a data store, then you now have to track empty space and you have fragmentation to worry about. In a filesystem such as '
77         tt {'ext4'}
78         ', a good deal of the complexity arises from this. Another example of an allocator with free-space management is '
79         tt {'malloc()'}
80         ' in C. But '
81         tt {'malloc()'}
82         ' is slow, as it needs to scan the free list at each allocation (at least in the traditional implementation), and memory-inefficient, as it does not support fragmented allocations in the way that '
83         tt {'ext4'}
84         ' does.'
85       }
86
87       p {
88         'A more efficient approach to dealing with fragmentation and dead objects, that was popularized with object-oriented languages like '
89         tt {'Java'}
90         ' and '
91         tt {'Smalltalk'}
92         ', is to allocate new objects contiguously from a region of spare memory instead of scanning for a free spot every time, and perform a periodic garbage collection to reclaim free space. It was controversial at the time because of garbage collection pauses, but if you can perform garbage collection concurrently with normal processing, there should be no significant impact.'
93       }
94
95       p {
96         'For my application, it seemed simpler to implement a daily copy from old to new log than to deal with free-space management. And there are certain other benefits, such as having a series of snapshots of the database to aid in trouble-shoooting. I also liked the idea of the log being human-readable (traditional for web developers, who like ASCII formats such as JSON rather than binary formats), and this would have complicated the free-space management considerably.'
97       }
98
99       h4 {
100         tt {'LogJSON'}
101         ' implementation'
102       }
103
104       h5 {'Root object'}
105
106       p {
107         'The structure of the log is extraordinarily simple, it basically just writes the new root JSON object to the end of the log each time, encoded by '
108         tt {'Buffer.from(JSON.stringify(...), \'utf-8\')'}
109         ', and then surrounded by angle brackets ‘'
110         tt {'<'}
111         '’ and ‘'
112         tt {'>'}
113         '’. The purpose of the angle brackets is to let us resynchronize to the latest written root object after the system is rebooted (including for power loss) or the server is restarted. Here is a very simple example of a '
114         tt {'LogJSON'}
115         ' file:'
116       }
117
118       pre {
119         `<"Hello, world">
120 <"Go away">
121 `
122       }
123
124       p {
125         'In the above example, two transactions were undertaken on an empty file. The first one initialized the root to '
126         tt {'"Hello, world"'}
127         ' (a Javascript string, which is valid JSON), the second one overwrite the root with '
128         tt {'"Go away"'}
129         ' (also valid JSON). Now suppose the server had crashed during the writing of the second transaction, leaving the file looking something like:'
130       }
131
132       pre {
133         `<"Hello, world">
134 <"Go aw`
135       }
136
137       p {
138         'Then, upon restarting the server, it would resynchronize to the last occurrence of ‘'
139         tt {'>'}
140         '’ that is followed by a newline, and the root object would appear to be '
141         tt {'"Hello, world"'}
142         '. The requirement that ‘'
143         tt {'>'}
144         '’ be followed by a newline, prevents it being tricked by occurrences of ‘'
145         tt {'>'}
146         '’ inside strings, where newline has to be encoded as ‘'
147         tt {'\\n'}
148         '’ rather than a newline character itself.'
149       }
150
151       h5 {'References to objects'}
152
153       p {'Whilst the above method of writing the root object allows for a log-structured JSON file and transaction-based atomic updates to that file, we still need a method to allow lazy access to the required parts of the tree and to allow updates without rewriting the entire tree. This is done by having a special way of writing the JSON '
154         tt {'Array'}
155         ' and '
156         tt {'Object'}
157         ' types (basically those JSON elements that can contain further JSON).'
158       }
159
160       p {
161         'We will use Array as an example. Suppose a root element of '
162         tt {'["Hello", "Goodbye"]'}
163         ' needs to be written in an empty file. It will be done in two parts. Firstly, the element itself will be written, without angle brackets. And then a new root element will be written, with angle brackets, that contains a '
164         i {'reference'}
165         ' to the just-written element, rather than the full text of the element itself:'
166       }
167
168       pre {
169         `[
170   "Hello",
171   "Goodbye"
172 ]
173 <[
174   0,
175   26
176 ]>
177 `
178       }
179
180       p {
181         'A reference is always constructed as a 2-element list, here '
182         tt {'[0, 26]'}
183         ', containing the file pointer and length of the previously written JSON.'
184       }
185
186       p {
187         'So in the above example, it’s possible to open the file, synchronize to the root element using the angle brackets, and then read the root element as a reference to an unread portion of the file, without actually having to read the contents of the root element until they are required.'
188       }
189
190       h5 {'Nested objects'}
191
192       p {
193         'To present a more realistic example, we will consider a deeper tree. The root of the tree will be a JSON '
194         tt {'Object'}
195         ' type which is keyed by a person\'s nickname. This in turn leads to a JSON '
196         tt {'Object'}
197         ' type containing some details about the person. The plain JSON file to be encoded in LogJSON format is:'
198       }
199
200       pre {
201         `{
202   "jane": {
203     "name": "Jane Doe",
204     "address": "123 Main St, Smallville",
205     "age": 30
206   },
207   "mike": {
208     "name": "Mike Rho",
209     "address": "125 Main St, Smallville",
210     "age": 32
211   }
212 }
213 `
214       }
215
216       p {
217         'And the encoded version in LogJSON is:'
218       }
219
220       pre {
221         `{
222   "name": "Jane Doe",
223   "address": "123 Main St, Smallville",
224   "age": 30
225 }
226 {
227   "name": "Mike Rho",
228   "address": "125 Main St, Smallville",
229   "age": 32
230 }
231 {
232   "jane": [
233     0,
234     77
235   ],
236   "mike": [
237     78,
238     77
239   ]
240 }
241 <[
242   156,
243   65
244 ]>
245 `
246       }
247
248       p {
249         'We can see that the process of looking up a user can now be more efficient than plain JSON. First we read the root object '
250         tt {'[156, 65]'}
251         ' and the referred-to object containing the keys '
252         tt {'"jane"'}
253         ' and '
254         tt {'"mike"'}
255         '. Then, we find the person we are looking for, let’s say Mike, and the reference '
256         tt {'[78, 77]'}
257         ' to Mike’s record. And we only need to read Mike’s record to proceed, we do not need Jane’s record at this stage. We do know there is a user '
258         tt {"jane"}
259         ', but her details are irrelevant to the current query.'
260       }
261
262       h5 {'Modifying the tree'}
263
264       p {
265         'Now suppose Mike’s address is changing. Recalling that the '
266         tt {'LogJSON'}
267         ' file, once written, is immutable, we need to write a '
268         i {'new'}
269         ' record for Mike containing the changed address, as follows:'
270       }
271
272       pre {
273         `{
274   "name": "Mike Rho",
275   "address": "20 Another St, Smallville",
276   "age": 32
277 }
278 `
279       }
280
281       p {
282         'And then we need to rewrite the parent object so that the nickname '
283         tt {'"jane"'}
284         ' will still point to her original record, but the nickname '
285         tt {'"mike"'}
286         ' will redirect to his newly written record:'
287       }
288
289       pre {
290         `{
291   "jane": [
292     0,
293     77
294   ],
295   "mike": [
296     240,
297     79
298   ]
299 }
300 `
301       }
302
303       p {
304         'Finally, we need to rewrite the root object to point to the newly written subtree:'
305       }
306
307       pre {
308         `<[
309   320,
310   66
311 ]>
312 `
313       }
314
315       p {
316         'And of course the file will not be considered modified (the old root will be active) until the new root has been fully written.'
317       }
318
319       h5 {'Modified tree‒full example'}
320
321       p {
322         'After the '
323         tt {'LogJSON'}
324         ' file creation and then modification described in the previous section, the entire file looks like this:'
325       }
326
327       pre {
328         `{
329   "name": "Jane Doe",
330   "address": "123 Main St, Smallville",
331   "age": 30
332 }
333 {
334   "name": "Mike Rho",
335   "address": "125 Main St, Smallville",
336   "age": 32
337 }
338 {
339   "jane": [
340     0,
341     77
342   ],
343   "mike": [
344     78,
345     77
346   ]
347 }
348 <[
349   156,
350   65
351 ]>
352 {
353   "name": "Mike Rho",
354   "address": "20 Another St, Smallville",
355   "age": 32
356 }
357 {
358   "jane": [
359     0,
360     77
361   ],
362   "mike": [
363     240,
364     79
365   ]
366 }
367 <[
368   320,
369   66
370 ]>
371 `
372       }
373
374       p {
375         'We can see that it contains a snapshot of how the file looked after each modification, since if you read the previous root element '
376         tt {'[156, 65]'}
377         ' it would lead to Mike’s old address, but if you read the latest root element '
378         tt {'[320, 66]'}
379         ' it would lead to Mike’s new address. In either case Jane’s address will be the same. A disadvantage of this scheme is that the supporting elements such as the '
380         tt {'Object'}
381         ' containing the list of nicknames, and the root, must be rewritten each time, which wastes space. But the garbage collector tidies this periodically.'
382       }
383
384       h4 {'Database maintenance'}
385
386       p {
387         'Although the database is human-readable, it’s still somewhat difficult to follow, especially given that file viewers generally do not display the file positions in decimal alongside the text. Therefore, for database maintenance, you can use the tools '
388         tt {'logjson_to_json'}
389         ' and '
390         tt {'json_to_logjson'}
391         ' provided within the proposed '
392         tt {'node.js'}
393         ' '
394         tt {'LogJSON'}
395         ' package. These tools give you a manual way to view and edit the tree, and perform garbage collection if you need to‒but note that they cannot be run on a live database, you need to stop your webserver first.'
396       }
397
398       p {
399         'For example, running '
400         tt {'logjson_to_json'}
401         ' on the complete file of the previous example gives this JSON:'
402       }
403
404       pre {
405         `{
406   "jane": {
407     "name": "Jane Doe",
408     "address": "123 Main St, Smallville",
409     "age": 30
410   },
411   "mike": {
412     "name": "Mike Rho",
413     "address": "20 Another St, Smallville",
414     "age": 32
415   }
416 }
417 `
418       }
419
420       p {
421         'And running it back through '
422         tt {'json_to_logjson'}
423         ' into a clean log file gives a compacted version of the log:'
424       }
425
426       pre {
427         `{
428   "name": "Jane Doe",
429   "address": "123 Main St, Smallville",
430   "age": 30
431 }
432 {
433   "name": "Mike Rho",
434   "address": "20 Another St, Smallville",
435   "age": 32
436 }
437 {
438   "jane": [
439     0,
440     77
441   ],
442   "mike": [
443     78,
444     79
445   ]
446 }
447 <[
448   158,
449   65
450 ]>
451 `
452       }
453
454       h4 {'Garbage collection'}
455
456       p {
457         'I have implemented the garbage collection in a rather sophisticated way, taking advantage of the immutability of the log file once written in order that garbage collection can proceed in the background without disturbing the operation of the website.'
458       }
459
460       p {
461         'A garbage collection is triggered every midnight in Universal Time Coordinated (UTC), that is, when the date rolls over. A new log file is constructed by copying all the live data from the old log file into it. And then transaction processing is briefly paused whilst we atomically rename the old log file out of the way to a dated name (contains the year, month and day in UTC) and rename the new log file over the old one. If the system crashes during renaming it will recover at the next restart.'
462       }
463
464       p {
465         'The synchronization magic happens by atomically grabbing the current root object and copying everything reachable from it into the new log file, but allowing more transactions to be written into the old log file simultaneously. It then atomically grabs the updated root and copies that as well, and continues to do so until both files are up-to-date and it can perform the atomic replacement.'
466       }
467
468       h4 {'Concurrency'}
469
470       p {
471         'For the time being I have made transactions to the database mutually exclusive, by implementing a mutex in the running webserver. Much improvement would be possible here, for instance by allowing read transactions to be concurrent and only using the mutex for write transactions, or by an even more sophisticated concurrency management that can consider different elements or keys of a JSON '
472         tt {'Array'}
473         ' or '
474         tt {'Object'}
475         ' as not conflicting.'
476       }
477
478       p {
479         'Currently there is a bit of a risk that if I do not implement good try/catch handling in my code, then a webserver request will crash, leaving the webserver running but the mutex in ‘held’ state. This would hang the webserver, requiring a manual restart. I could implement some sort of disaster recovery, but there is probably no point at this time, I will simply use try/catch and make sure transactions are rolled back if not completed.'
480       }
481
482       h4 {'Conclusion'}
483
484       p {
485         'The log-structured format provides a useful way of maintaining huge JSON files. It remains to complete the documentation and release the source code and NPM package. It also remains to implement a stress test of my website, once up and running, to simulate thousands of user accounts and multiple logins to the same account all performing transactions simultaneously. Therefore, things are still experimental at this stage. I enjoy writing this blog to update on those experiments.'
486       }
487     },
488     // scripts
489     async _out => {}
490   )
491 }