Sunday, December 1, 2019

From Mlais MX28 to Greentel G9 4G, a.k.a. Android 4.2.2 to 7.0

Disclaimer: This post comes from way back - end of 2018 - but most of the points still hold.

This may be a small step for Android, but a great leap for myself.

After 4+ years of faithful service, I decided it's time to give my dear old Mlais MX28 a break.

Why the MX28 had to go

The Mlais MX28

The real reasons were, indeed, a bit more profound:

  • The battery degraded to a very poor and highly unreliable state: right now it could be at 80%, and then, when I open up a simple text editor, poof! It's dead.
  • Some apps (esp. Skype for Android kept on logging me out whenever the phone was restarted. Combined with the first point, this meant that I had to keep on logging in almost every day. Given that my employer has several customers who use Skype as a quick support channel, this was both inconvenient and risky—to say the least.
  • One of our teams was developing a SoLoMo Android app—and we inadvertently became their lab rats; and I was often mocked as the owner of a "stone-age smartphone" because many of their app's features didn't work on my phone.
  • There was a lot of features I was actually missing—lock screen notifications, ability to dismiss an alarm before it goes off, 4G, fast GPS location fixes, and so forth.

Farewell MX28, enter G9 4G

The Greentel G9 4G

So I got myself a G9 4G (locally owned brand, with Chinese make; because the only locally manufactured 4G brand had already been discontinued).

  • Android 7.0 (a leap from Jelly Bean to Nougat)
  • 1.3GHz quad-core CPU (slightly forward)
  • 1GB RAM (standstill)
  • 8GB internal storage with 4.2GB actually free (a 2.5x leap as the MX28 had only 1.7/4)
  • Mali-400MP2 GPU (standstill)
  • 4G (leap from 3G)
  • 3200mAh "mega" battery (a considerable improvement from the MX28's 2500mAh)

G9 4G + 7.0 Nougat: many things are looking up!

However, there was a few things that made me really happy:

  • GPS is working smoothly—with the MX28 I could never get a GPS fix on time (it would take 5-10 minutes, or perhaps forever if I'm inside a building).
  • Lock screen notifications!
  • Now I can dismiss my 6AM alarm whenever I get up at 5.55 (without manually turning off the alarm setting and forgetting to turn it back on the following day).
  • Skype never logged me out again (although it did once, when I updated the app—probably the expected behavior).
  • Built-in flashlight (finally!)
  • Built-in battery saver and data saver
  • Screen color inversion! It was a big, unexpected and pleasant surprise. Now I can do all sorts of stuff at night, without straining my eyes with bright backgrounds.

But... is it all unicorns and rainbows?

And now for the disappointments:

  • No root! (Unless I want to ruin my one-year warranty) So no more hacking around (moving apps around when out of space, getting rid of pesky system apps, tcpdump, editing /etc/hosts—and a lot more) :(
  • Darn those ads! Thanks to root, on the MX28 I had an ad-resistant /etc/hosts that helped me totally forget about them—on my PC as well, since I access the internet through a phone tether. Now suddenly, in every app I open, there's a pesky ad popping up every now and then!
  • Lack of granular data usage views: in 4.2.2's Data Usage graph I could use two movable guides to limit the usage view period to a few days or hours; the new interface is really dumb as it just shows the usage for the whole billing period, without any ability to drill down.
  • Lack of a volume controller in the top drawer! Really, Google? What were you thinking? Now every time I want to put my phone in silent or vibration mode, I have to long-press the hardware power button :(
  • Google Now Launcher, ever so intrusive: there's no way to get rid of the Google Search bar at the top of the home screen. It just sits there, eating up precious space—and I never ever google on my phone!
  • Automatic updates? I have marked every known Wi-Fi network as metered, yet every time I connect to one of them I see a few brief flashes of progress bars in the notification drawer. Even with app updates disabled on the Play Store side as well.
  • Crappy browser (or is it in the OS/device itself?) that crashes every now and then upon opening a saved page; in 4.2.2 I never ever had the browser crash, and now it's happening on a hourly basis! Although Chrome comes preinstalled, I was never a fan, and never will be!
  • Lags like a grandma: probably because 1GB is not sufficient for 7.0 (which seems to take up well over 500MB, right after startup)?

The worst part: how G9 4G really pisses me off

And now, the greatest (worst) of them all:

The phone restarts itself randomly!

Yep, especially when travelling (so I guess it could be something related to cellular signals); when I pull out the phone after a ride, the screen remains frozen for 10-15 seconds (not responding to anything, even to hardware buttons), and then the whole damn thing restarts.

But the worst part? Sometimes it decides to shut itself down (also when travelling), right inside my pocket without me even touching it. So I could be cut off from the rest of the world for hours, if I don't check whether my phone is "alive" every few minutes.

Yet, proud to be part of the ගන්න අපේ දේ movement...

It is comforting yet painful, to know there was a time Greentel actually manufactured and exported smartphones from Sri Lanka.

Greentel Mobile

Painful because, when I asked about it from one of their agents, they said the profit margins were too low so they had to abandon it.

Even today, probably only the middle-lower class of Sri Lankans would go for a Greentel smartphone - even them, I'm not so sure. Heck, even the support agents at our "always caring" mobile services provider do not know of a brand name called "Greentel" - let alone "Sri Lankan owned".

I don't see the සූර්ය සිංහ branding on Greentel (although our patriotic Dilantha does some good work in getting the word out); but I'm proud to be the owner of one, despite all the bumps along the road.

Way to go, Greentel!

වායෝ, වීණාවී, සහ මම: Wayo, Veenavee, and Me

වායෝ 2019

මං ඇවිල්ලා සංගීත විසාරදයෙක්වත් විචාරකයෙක්වත් නොවේ ය. හැබැයි ඊයේ විහාරමහාදේවියේ තිබ්බ වායෝ "වීණාවී" ප්‍රසංගය බැලූ වෙලේ ඉඳං කට (අත) කහන්ට පටන් ගත් හෙයින් වචන කිහිපයක් කොටා දාන්ට සිතුණේ ය.

"සංගීත"ය පුංචි සන්දියෙ ඉඳන් අහලා තිබුණට, ඒකට හල්කිරීමක් දාගත්ත ඩෑල් එකක් ඉන්නවයි කියල දැනගත්තේ ඉස්කෝලෙ පහ වසරත් පාස් වුණාට පස්සේ ය. ඒ දන්නෙත් "ආ අර රඟහල කාපු එකා" කියන කාඩ් ⁣එකෙන් ය. ඒ කාලේ ටීවී චැනල්වල විචාරකයන් සහ ප්‍රබුද්ධයන් සෙට්වෙලා සංගීත්ව පතුරු ගහපු හැටි තාම මතකයි ය. (පරණ කුණු ඇවිස්සුවා⁣ට හීනියට සමාවක් දෙන්ටැයි ඉල්ලනවා ය. 🙏)

අයියාත් තාත්තාත් හින්දි සින්දු ඔස්තාර්ලා උණා⁣ට, මට ගොඩක් කාලයක් යනකන් සංගීත "උණ" හැදු⁣ණේ නැත. (හය හත වසරවලදී ලාවට ඇඟ රත් වීගෙන ආවත්, ඉස්කෝලේ ගායන තරගයකදී කිච වෙලා සාටර් වෙලා ගිය කල්හි ඒක සුරුස් ගා බැස ගියේ ය.) එහෙම හි⁣⁣ටපු එකා ඉයර්පෝන් හොය හොයා පරණ සෙල්පෝන් පෙට්ටි අවුස්සන තත්ත්වයට පත් වුණේ සංගීත් හැදූ "වායෝ" නිසා ය. ඒකත් හරි අපූරු කතාවක් ය.

ඔන්න ඉතිං පෙරුම් පුරාගෙන කැම්පස් ගොහින්, (මොරටුව ඊ-ෆැක්) සෙමෙස්තරය ගොඩ දාන්න ඇක දාන (පාඩං කරන) දවස් ය. (ඔව් ඔව්. එක්සෑම් කට උඩ ය.) අපි ** ඉරාගෙන ඇක දාන අතරෙ, කලින් ම ගොඩ දාගෙන හිටපු වික්කා කියල ඩෑල් එකකුත් අප අතරේ හිටියා ය. දවසක් මූ, "ඇක දාල මහන්සි කට්ටිය අහල බලන්න මේ සින්දුව" කියලා (කින්ඩියට ද මන්දා 🤔) බුකියේ පෝස්ට් එකක් දැම්මා ය.

දාලා තිබුණේ "වායෝ"ගේ "අපි කවුරු ද" සින්දුව ය.

අතරේ අහනව බලනව මේ... අපි කවුරු ද...

ඉස්කෝලේ බුද්ධාගම පාඩමෙනුයි දහම් පාසලෙනුයි ඇහුවට පස්සේ "වායෝ" කියන වචනය ඇහුවේ එදා ය. වෙන විදිහකට කීවොත්, වායෝ කියා සංගීත කණ්ඩායමක් ඉන්නවා කියලා හාංකවිසියක් දැන ගත්තේ එදා ය. 😊

පළවෙනි පාර ඇහුවට ලොකු සීන් එකක් දැනු⁣ණේ නැත. "සෝක් සින්දුව" කියලා ආපහු ඇක දාන්න ගියා ය. (ඒ කාලෙත් මං හෙන ම ඇකයා ය.)

දෙවෙනි පාර අහද්දි නම් සිංදුවේ මොකක්හරි අමුතු ලල් එකක් තිබේ ය කියලා තේරුණා ය.

තුං වෙනි පාර අහල ඉවර වෙද්දි නම්, පිස්සුව ඔලුවට ම වැදී ඉවර ය.

එදා ඉඳං, උදේ ඉඳං හවස් වෙනකං ඉයර්පෝනය කනේ ගහන් "අපි කවුරු ද" අහන තත්ත්වෙට මං පත් වුණා ය. මේ වෙනකොට ලොවෙත් දහදාස් පාරකට වැඩිය සින්දුව අහලා තිබෙනවා ය. (දැනුත් සතියකට පාරක්වත් නොවරදවා ම අහන්න ඕනෑ ය. කුඩු සික් එකට වඩා පොඩ්ඩයි අඩු ය. — අපොයි අපොයි 🙏 සංගීත් අයියෙ, මා කුඩු ගසනවා කියා එයින් අදහස් කළේ නැත.)

සේනක බටගොඩ මහත්මාගේ පද මාලාව, ලහිරුගේ මියුසික් අවන් එකේ පදමට බේක් කර, සංගීත්, ෂේන් ප්‍රමුඛ වායෝ කල්ලිය රහට පදම⁣ට ගයනවා අහද්දි ලෝකෙ තියෙන ප්‍රස්න ඔක්කොම සිං ගා අමතක වෙනවා ය. (බටගොඩ මහත්මාගේ මුල් ගායනයත් මෙහි දී සිහි කිරීමට කැමැත්තෙමි. 🙏)

ඊට පස්සෙත් එහෙන් මෙහෙන් වායෝගේ පට්ට සින්දු ටිකක් අහන්න ලැබුණේ ය. මං එහෙම හොය හොයා සින්දු අහන කෙනෙක් නොවෙන නිසා මට ලැබුණේ "අපි සැනසිල්ලේ", "ගෙම්බෝ", "සිහින මවන්නැති" (බටගොඩයන්ගේ) වගේ කීපයක් විතර ය. හැබැයි ඇත්ත ම කිවුවොත් "අපි කවුරු ද" වගේ බොක්කට වදින සින්දුවක් නම් ආයෙ වායෝගෙන් තියා කාගෙන්වත් සෙට් වුණේ නැත.

කාලය ඔහොම ගත වී ගියේ ය. පත්තරවල "වායෝ පිපිරෙයි", "වායෝ දෙදරයි" වගේ සිරස්තල දැක්කත් ලොකුවට හොයන්නට කියවන්නට ගියේ නැත.

පසුකාලීන ව එක එක දේවල් සිද්ද වෙලා, දුක වාවනු බැරි හින්දා කට වහගෙන ඉකිබිඳිමින් ඉන්න දවසක, කටුබැද්ද හන්දියේ කලර් ලයිට් එක ළඟදී "වායෝ" බැනර් එකක් දැක්කා ය. මුලින් "අනාතයෝ" වගේ පෙනුණත් 🧐, ඇස් දෙක එහෙම පිහදාලා හොඳට බලද්දි, "වායෝ" නමට යටින් ලියලා තියෙන්නේ "අනාගතයේ" කියලා බව තේරුණා ය.

මේ අමුතු අබ්බූත බූත අකුරින් ලියා තියෙන "අනාගතයේ" සීන් එක මොකද්ද කියලා ගූගල් පාරක් දාල බලද්දි, අවුරුද්දකටත් කලින් රිලීස් උණ සින්දුවක් මතු වුණේ ය. සිරාවට ම ඊට කලින් එක පාරක්වත් එහෙම සින්දුවක නමක්වත් අහලා තිබුණේ නැත. (එහෙව් මට, වා⁣යෝ ෆෑන් කෙනෙක් කියලා කෝචොක් නොකරන මෙන් කාරුණික ව ඉල්ලා සිටිමි. 🙏)

අනාගතයේ

ඒ පාර නම් සින්දුව අහපු ගමන් ම ඔලුව මඤ්ඤං වී, උන්හිටි තැන් අමතක වෙන ගාන⁣ට ගියේ ය. සින්දුව බොක්කට වැදී බොක්ක හාරාගෙන ගැඹුරට ම ගියේ ය. 😳

ඒ මදිවාට, අන්තිම කෑල්ලේ pitch shift එක අහනකොට, සියොලඟ මවිල් කෙලින් වී, කකුල් දෙකෙන් අල්ලා පරසක්වල ගැහුවා වගේ උණේ ය. එහෙම සින්දුවක් ආයේ කවදා අහන්න ලැබේ ද කියා නොදන්නේ ය.

සනුක/සංගීත් පද මාලාවේත්, සනුකගේ මෙලඩියේත්, වායෝ ගැං එකේ ගායනයේත් සුසංයෝගය ඒ තරමට ආතල් ය. කික් ය. සුපිරි ය. 🤩

පොළෝ තලයේ අනාගතයේ මිනිසා මා බව සහසුද්දෙන් ම දැනගත්තේ එදා ය.

"අපි කවුරු ද" දෙවෙනි තැනට වැටී, "අනාග⁣තයේ" සෙනිකව ප්ලේ ලිස්ට් එකේ මුලට ම ආවේ ය.

එදා ඉඳං හැබැහින් වායෝ සෙට් එක දකින්නත්, අහන්නත්, "අනාගතයේ" ෂෝ එකක් බලන්න යන්න හිතං හි⁣ටියට, එක එක හේතු නිසා වැඩේ මගෑ⁣රුණේ ය. (අවංකව ම කිවුවොත්, වැඩි හරියක් මගෑරුණේ කම්මැලිකමට ය. කවදාවත් මියුසිකල්ලෙකක් බලන්න ගිහිං නැති, යන්න හිතුවෙත් නැති මා හට එහෙම වෙන එක, එක අතකින් සාදාරණ ය.)

ඔපිසියේ සොහොයුරියකගේ ඉල්ලීමකට, අයිටී ෆැක් "අත්වැල"ට සපෝට් එකට "වීණාවී 2019" ටිකට්ටුවක් ගත්තෙමි. ඒත් යනවා ද කියා සුවර් එකක් තිබ්බේ නැත. හවසට හවසට සුරු සුරු ගගා වහින දවසුත් නොවැ. අන්තිමට හිත හදාගෙන, කුඩයත් අතින් අරගෙන - නෑ, ඉහලාගෙන - බෝඩිමෙන් එලියට බැස්සෙමි.

මග දිගටත් බුකිය බල බලා ආවේ, "postponed due to bad weather" කියන (අ)සුබාරංචිය කොයි වෙලේ හරි වැටෙයි ද කියලා හිතමිනි. (ඔව්, මො⁣⁣රටු කොල්ලෙක් ලෙස මං මා ගැන ම ලැජ්ජා විය යුතු යැ.)

තුම්මුල්ලේ ඉඳං පයින් ම විහාරමහාදේවිය⁣ට එද්දිත් සර සර වැස්ස ය. එතකොටත් සීයක් විතර සෙට්වෙලා කුඩ ඉහලං බලා සිටිති. මේ වැස්සෙ කොහොම ෂෝ එකක් කරන්න ද?

(හා හා, කවදාවත් නැතුව මූ ගිය හින්ද තමයි ඊයෙ වැහැල වැහැල කුජීත වුණේ කියලා කුපිත නොවෙන ලෙස කාරුණික ව ඉල්ලා සිටිමි. 🙏)

කොහොම හරි වැඩේ පටන් ගත්තේ ය. එතකොටත් වැස්ස ය. කොල්ලො කෙල්ලෝ, පුටු වහල තිබුණ පොලිතින් කොළ බිම එලාගෙන, කුඩ යට තුරුලු වී සැදී පැහැදී සිටිති. ටිකක් වැඩිපුර සපෝට් එක දී තිබූ නිසා මාත් කුඩේ ඉහලං පුටුවකට හේත්තු වීමි.

හුරුපුරුදු ගිටාර් රිද්මයක් ඇහෙන්ට විය.

අම්මටසිරි, අනාගතයේ! 🙌

එහෙම සුපිරියට පටන් ගත් ගී මියැසිය, වරුසාව මැද නොසැලී ගලා ගියේ ය. කුඩේ යට සීතලට තුරුලු වෙන්න කෙනෙක් නොහිටියත් ලොකු පාලුවක් දැනු⁣ණේ නැත. වායෝ ෆැමිලි එකත් එක්ක අපි හැමෝ ම එකා වගේ වලා අතරේ පියාඹමින් සිටි නිසා විය යුතු ය.

සංගීත් අයියා උඩ පැන පැන ඇඹරි ඇඹරි දුව දුවා නට නටා ජෝක් දම දමා මයික් අල්ල අල්ලා ස්පීකර උඩ නැග⁣ගෙන රඟපු රැඟුමට, වායෝ සෙට් එක ම සංගීතයෙන් ගායනයෙන් පට්ට සහයක් දුන්නා ය. කැම්පස් අවුට් වෙලා සිවු වසරකට කිට්ටු උණත්, ඊයේ නං දැනුණේ ගිය මාසෙ කැම්පස් ආවා වාගේ ය.

නදීමාල්ගේ නුරා වසන්තේත්, චිත්‍රාල්ගේ නදී ගංගාත්, ධනිත්ගේ සඳගනාවත්, වායෝගේ අනාගතයත් එක් වූ තැන... තව මොන කතා ද? 🤘

⁣කස්ටිය හුරේ දැමුවෝය. වන්මෝ ගෑවෝය. නැටුවෝය. ගැයුවෝය. ෆ්ලෑෂර් වැනු⁣වෝය. ඒ සේරම, මහ වරුසා මැද්දේ ය.

වීණාවී 2019

සංගීත් අයියා කියපු විදිහට, මීට කලින් මෙහෙම වරුසාවක් මැද වායෝ ෂෝ එකක් කර නැත. ඇත්ත වුණත් බොරු වුණත්, ලයි⁣ටින්, සවුන්ඩ්ස්, පවර් ඔක්කොම මහ වැස්සේ තෙමෙද්දි, දුප්පත්කමට එරෙහිව සටන් කරන මොරටුව අයි⁣ටී ෆැක් එකට සවියක් වෙන්න වායෝ ගත් ඒ තීරණයට අපි හැමෝම හිස නමා ආචාර කළ යුතු ය. 🙇

ඒ එක්ක ම, "මට නං තව තෙමෙන්න දෙයක් නෑ බං" කියමින් ම, මහ වැස්සෙ කඩිගුලක් වගේ එහෙමෙහෙ දුව දුවා හැම දේම තිතට සංවිධානය කරපු අයි⁣ටී ෆැක් සෙට් එකටත් හිස නමා ආචාර කළ යුතු ය. 🙇

කනට විතරක් නොව හිතටත් වදින දෙයක් කළ ඒ හැමෝටමත්, වැස්සට කොකා පෙන්නා අවසන් මොහොත වෙනකං නොබිඳුණ ආතල් එකක් පවත්වාගත් (මා ඇතුළු) ප්‍රේක්ෂක සැමටත් නැවතත් ආචාර කරමි. 🙇

(ෂෝ එක ඉවර වෙලා, යන්න බස් නැතුව විහාරමහාදේවියෙ ඉඳන් තට්ට තනියමේ වැස්සේ පයිං ම ගල්කිස්සට ගිය එක වෙන ම ආතල් එකකි. සමහර විට මතු සම්බන්දයි වනු ඇත. ✍️)

බුකියේ Wayo Official Fan Club එකේ (අවාසනාවට public නැති) පෝස්ටුවක කොටසකින් මේ ඇඬියාව අවසන් කරන්ට සිතුවෙමි. (පූර්ණ අයිතිය, කර්තෘ Gagana Atapattu හට ය.)


ටිකක් හිතුවද තමන් කන ටොපි කොලේ, තමන් බොන සිගරැට් කොටේ ඉදන් දාන්නෙ එයාල මේ හදන්න හදන ලෝකේ උඩ කියලා?
නෑ.
නෝනා සතියෙ අන්තිමට කුණු බෑග් එක හංගලා දාලා එන්න කියල මහත්තයට කියද්දි හිතුනද ඒ කුණු දාලා නැති කලේ එයාගෙම ළමයට ඉන්න පොළොවක් කියලා.

...

"අපිම නේද බන් හිනා වෙන්නේ? අපිමයි නේද නිදහසේ ඉන්නේ ?
ඉතින් අපි නිදහසේ ඉදන් ඉස්සරහට ලෝකයක් මැව්වට වැඩක් නෑ.
සල්ලි පොදි ගැහුවට වැඩක් නෑ
ලොකු ලොකු ස්කෝල වලට දැම්මට වැඩක් නෑ.
ඉන්න පොළොවක් නැත්තන්"

මේ ඔහුගේ වදන් වලින්.

ටිකක්. ටිකම ටිකක් හිතමු.
කෝ නවලෝකයක් දකිනවුන්ට අනාගතයක් ?

අපමණ දුක් ඇති වින්දා
රැකගමු මරු ගෙල සින්දා
හෙට ඇස් අරින බිලින්දා
මනුසතුනේ

නොනිම් කතරේ පහන් තරුවෙන්
එතෙර වෙන්නේ
පොළෝ තලයේ ඔබයි මිනිසා
අනාගතයේ...


(ආතල් එක උදෙසා අක්ෂර වින්‍යාස හා ව්‍යාකරණ රීති අමු අමුවේ උල්ලංඝනය කිරීම වෙනුවෙන්, ම⁣ට සිංහල උගැන්වූ ගුරු මෑණිවරුන්ගෙන් පියවරුන්ගෙන් පටන් ගෙන සියලු දෙවි දේවතාවුන් දක්වා සැම දෙනාගෙන් ම සමාව භජනය කරනු කැමැත්තෙමි. 🙏)

Friday, November 29, 2019

Google Cloud has the fastest build now - a.k.a. හූ හූ, AWS!

SLAppForge Sigma cloud IDE - for an ultra-fast serverless ride! (courtesy of Pexels)

Building a deployable serverless code artifact is a key functionality of our SLAppForge Sigma cloud IDE - the first-ever, completely browser-based solution for serverless development. The faster the serverless build, the sooner you can get along with the deployment - and the sooner you get to see your serverless application up and running.

AWS was "slow" too - but then came Quick Build.

During early days, back when we supported only AWS, our mainstream build was driven by CodeBuild. This had several drawbacks; it usually took 10-20 seconds for the build to complete, and it was rather repetitive - cloning the repo and downloading dependencies each time. Plus, you only get 100 free build minutes per month, so it adds a bit of a cost - despite small - to ourselves, as well as to our users.

Then we noticed that we only need to modify the previous build artifact in order to get the new code rolling. So I wrote a "quick build"; basically a Lambda that downloads the last build artifact, updates the zipfile with the changed code files, and re-uploads it as the current artifact. This was accompanied by a "quick deploy" that directly updates the code of affected functions, thereby avoiding the overhead of a complete CloudFormation deployment.

Then our ex-Adroit wizard Chathura built a test environment, and things changed drastically. The test environment (basically a warm Lambda, replicating the content of the user's project) already had everything; all code files and dependencies, pre-loaded. Now "quick build" was just a matter of zipping everything up from within the test environment itself, and uploading it to S3; just one network call instead of two.

GCP build - still in stone age?

When we introduced GCP support, the build was again based on their Cloud Build, a.k.a. Container Builder service. Although GCP did offer 3600(!) free build minutes per month (120 each day; see what I'm talking, AWS?), theirs was generally slower than CodeBuild. So, for several months, Sigma's GCP support had the bad reputation of having the slowest build-deployment cycle.

But now, it is no longer the case.

Wait, what? It only needs code - no dependencies?

There's a very interesting characteristic of Cloud Functions:

When you deploy your function, Cloud Functions installs dependencies declared in the package.json file using the npm install command.

-Google Cloud Functions: Specifying dependencies in Node.js

This means, for deploying, you just have to upload a zipfile containing the sources and a dependencies file (package.json, requirements.txt and the like). No more npm install, or megabyte-sized bundle uploads.

But, the coolest part is...

... you can do it completely within the browser!

jszip FTW!

That awesome jszip package does it all for us, in just a couple lines:

let zip = new JSZip();

files.forEach(file => zip.file(file.name, file.code));

/*
a bit more complex, actually - e.g. for a nested file 'folder/file'
zip.folder(folder.name).file(file.name, file.code)
*/

let data = await zip.generateAsync({
 type: "string",
 streamFiles: true,
 compression: "DEFLATE"
});

We just zip up all code files in our project, plus the Node/npm package.json and/or Python/pip requirements.txt...

...and upload them to a Cloud Storage bucket:

let bucket = "your-bucket-name";
let key = "path/to/upload";

gapi.client.request({
 path: `/upload/storage/v1/b/${bucket}/o`,
 method: "POST",
 params: {
  uploadType: "media",
  name: key
 },
 headers: {
  "Content-Type": "application/zip",
  "Content-Encoding": "base64"
 },
 body: btoa(data)
})).then(res => {
 console.debug("GS upload successful", res);

 return {
  Bucket: res.result.bucket,
  Key: res.result.name
 };
});

Now we can add the Cloud Storage object path into our Deployment Manager template right away!

...
{
 "name": "goofunc",
 "type": "cloudfunctions.v1beta2.function",
 "properties": {
  "function": "goofunc",
  "sourceArchiveUrl": "gs://your-bucket-name/path/to/upload",
  "entryPoint": ...
 }
}

So, how fast is it - for real?

  1. jszip runs in-memory and takes just a few millis - as expected.
  2. If it's the first time after the IDE is loaded, the Google APIs JS client library takes a few seconds to load.
  3. After that, it's a single Cloud Storage API call - to upload our teeny tiny zipfile into our designated Cloud Storage bucket sigma-slappforge-{your Cloud Platform project name}-build-artifacts!
  4. If the bucket is not yet available, and the upload fails as a result, we have two more steps - create the bucket and then re-run the upload. This happens only once in a lifetime.

So for a routine serverless developer, skipping steps 2 and 4, the whole process takes around just one second - the faster your network, the faster it all is!

In comparison to AWS builds, where we want to first run a dependency sync and then a build (each of which is preceded by HTTP OPTIONS requests, thanks to CORS restrictions); this is lightning fast!

(And yeah, this is one of those places where the googleapis client library shines; high above aws-sdk.)

Enough reading - let's roll!

I am a Google Cloud fan by nature - perhaps because my "online" life started with Gmail, and my "cloud dev" life started with Google Apps Script and App Engine. So I'm certainly at bias here.

Still, when you really think about it, Google Cloud is way simpler far more organized than AWS. While this could be a disadvantage when it comes to advanced serverless apps - say, "how do I trigger my Cloud Function periodically?" - GCF is pretty simple, easy and fast. Very much so, when all you need is a serverless HTTP endpoint (webhook) or bucket/queue consumer up and running in a few minutes.

And, when you do that with Sigma IDE, that few minutes could even drop down to a matter of seconds - thanks to the brand new quick build!

So, why waste time reading this - when you can just go and do it right away?!

AS2 Gateway REST API - B2B EDI Exchange Streamlined!

AS2 Gateway REST API: seamless B2B trading integration (courtesy of Pexels)

All right. You have set up your local stations and trading partners on AS2 Gateway B2B Trading Platform - and successfully sent and received messages. Today, let's see how we can automate these AS2 communications - using the brand new AS2 Gateway REST API!

Of course, if you haven't set up AS2 communications yet, you can always get started right away, for free.

I'll be using curl, so I can show you the whole story - URL, headers, payload formats and all. But you can use any tool (say, Postman) to follow along; and use whatever language or library (HttpClient, requests, axios - to name a few) when it comes to programmatic automation!

Authorization

AS2 Gateway REST API is secured with token-based auth, so the first thing you need is an auth token.

curl -XPOST https://api.as2gateway.com/v1/authorize \
    -H 'Content-Type: application/json' \
    --data '{"username":"email@domain.com","password":"pazzword"}'

Replace email@domain.com and pazzword with your actual AS2G account credentials.

The response (formatted for clarity) would be like:

{
  "token": "hereGoesTheLongLongLongLongToken",
  "expiry": 1574299310779
}

Extract out the token field. This would be valid until the time indicated by the expiry timestamp - usually 24 hours. You can reuse the token until then, saving yourself the trouble of running auth before every API call.

Send a Message!

Good! We have the token, so let's send our message!

curl -v -XPOST https://api.as2gateway.com/v1/messages/AWSMSTTN/AWSMPTNR \
    -H 'Authorization: hereGoesTheLongLongLongLongToken' \
    -H 'Content-Type: multipart/form-data; boundary=----foobarbaz' \
    --data-binary \
'------foobarbaz
Content-Disposition: form-data; name="file"; filename="file1.txt"
Content-Type: text/plain

one
------foobarbaz
Content-Disposition: form-data; name="file"; filename="file2.txt"
Content-Type: text/plain

two
------foobarbaz--'

True, this is a bit too much for curl; obviously you would use a more convenient way - File option of Postman, MultipartEntityBuilder for Apache HttpClient, and so on. But the bottom line is:

  • you specify the AS2 IDs of your origin (local trading station) and destination (remote trading partner) as path parameters - on the API call URL
  • you can optionally include a message subject as a query parameter
  • as the request body (payload) you compose and send a multipart request - including the boundary as a Content-Type HTTP header on the API call request
  • you can include multiple files (attachments) in your AS2 message by including as many "file" parts needed, in the multipart body; each with a unique filename attribute.

Looking at the -v trace:

> POST /v1/messages/AWSMSTTN/AWSMPTNR HTTP/1.1
> Host: api.as2gateway.com
> User-Agent: curl/7.47.0
> Accept: */*
> Authorization: hereGoesTheLongLongLongLongToken
> Content-Type: multipart/form-data; boundary=----foobarbaz
> Content-Length: 243
>

* upload completely sent off: 243 out of 243 bytes

< HTTP/1.1 202 Accepted
< Date: Wed, 20 Nov 2019 02:53:25 GMT
...
< Link: https://api.as2gateway.com/v1/messages/sent/<0123456789.012.1574218406002@as2gateway.com>
< Content-Type: application/json
< Transfer-Encoding: chunked
<

* Connection #0 to host api.as2gateway.com left intact

{"message":"Successfully added a new outbound entry"}

The response would be an HTTP 202 (Accepted), with:

{"message":"Successfully added a new outbound entry"}

Remember to save up the response HTTP headers too: we're gonna need them later.

Cool! So our AS2 message was sent out?

No. Not yet.

"Queued", "Sent", and "Failed"

When AS2G sent you that 202 response, it meant that it has accepted the challenge of sending your files to AWSMPTNR. So the message is now enqueued for delivery.

Within about 5 seconds, AS2G will attempt to send your message out to AWSMPTNR, based on the URL, certificates, etc. that you have configured on their partner configuration.

(If the send fails, it will automatically retry in 10s; then 20s; then 40s; and so forth, up to 10 times.)

So, after you call the message send API, the message could end up in one of three places:

  • If AS2G has not yet tried to send the message - or has tried, failed and is planning to try again - it would appear under Queued.
  • If AS2G successfully sends it out, it would appear under Sent.
  • If the send fails, it will go under Failed. Also, as mentioned before, depending on the nature of the failure (e.g. if it is retryable - such as a temporary network issue) it may continue to appear under Queued as well.

So, in order to check what happened to the message, we better check under Sent first - we all love happy-day scenarios, don't we?

Checking Under Sent

curl 'https://api.as2gateway.com/v1/messages/sent/<0123456789.012.1574218406002@as2gateway.com>' \
    -H 'Authorization: hereGoesTheLongLongLongLongToken'

Looks fine: GET the message under sent, with that particular ID. But how do you get the ID?

Remember the response headers we saved up from the send API call? In there, there is a Link header, containing not just the message ID - but the entire sent-message URL!

Link: https://api.as2gateway.com/v1/messages/sent/<0123456789.012.1574218406002@as2gateway.com>

You can directly call that URL (adding the auth header) and get back an AS2 Message entity representing the sent-out message:

{
  "as2MessageId": "<0123456789.012.1574218406002@as2gateway.com>",
  "persistedTimestamp": 1574218406002,
  "compressed": true,
  "encrypted": true,
  "signed": true,
  "subject": "AWSMPTNR",
  "receiverId": "AWSMPTNR",
  "senderId": "AWSMSTTN",
  "transportStatusReceived": 200,
  "deliveryStatus": "Delivered",
  "mdnStatus": "Received",
  "partnerType": "Production",
  "mdnMessage": {
    "persistedTimestamp": 1574218406100,
    "mdnError": false,
    "content": "MDN for Message-ID: <0123456789.012.1574218406002@as2gateway.com>\r\n\
From: AWSMSTTN\r\nTo: AWSMPTNR\r\nReceived on: Wed Nov 20 02:53:26 UTC 2019\r\n\
Status: processed\r\n\
Comment: This is not a guarantee that the message has been completely processed or \
understood by the receiving translator\r\n\
Powered by the AdroitLogic UltraESB-X (https://www.adroitlogic.com)\r\n"
  },
  "attachments": [
    {
      "name": "file1.txt",
      "size": 3
    },
    {
      "name": "file2.txt",
      "size": 3
    }
  ]
}
  • If the message got an MDN back, it would also be included - under the mdnMessage field.
  • Also, the transportStatusReceived field represents the actual HTTP response code AS2G got back when sending out the message. It may become handy, say, when troubleshooting a missing MDN - knowing what we got back from the actual send-out HTTP call.

In this case, transportStatusReceived is HTTP 200 - and we have successfully received a non-error MDN ("mdnError": false); voilà!

Checking Under Failed

If your message was not so lucky - i.e. it failed to get sent - a copy would end up under failed:

curl 'https://api.as2gateway.com/v1/messages/failed/<0123456789.012.1574218406002@as2gateway.com>' \
    -H 'Authorization: hereGoesTheLongLongLongLongToken'

{
  "as2MessageId": "<0123456789.012.1574218406002@as2gateway.com>",
  "persistedTimestamp": 1574218406002,
  "compressed": true,
  "encrypted": true,
  "signed": true,
  "subject": "AWSMPTNR",
  "receiverId": "AWSMPTNR",
  "senderId": "AWSMSTTN",
  "transportStatusReceived": 404,
  "deliveryStatus": "Not Delivered",
  "mdnStatus": "Pending",
  "partnerType": "Production",
  "attachments": [
    {
      "name": "file1.txt",
      "size": 3
    },
    {
      "name": "file2.txt",
      "size": 3
    }
  ]
}

Almost same as Sent, except for the path fragment change. You can derive the URL easily from the Link header described above.

Note that some messages fail one-time, but others can keep on retrying and failing - in the latter case, there could be multiple entries under failed with the same message ID; but the API will always return the most recent one.

Checking Under Queued

If the message doesn't appear under either of the above - say, after 10 seconds from the send call - AS2G might not have gotten a chance to try to send it out. If so, it could be still in the queue:

curl 'https://api.as2gateway.com/v1/messages/queued/<0123456789.012.1574218406002@as2gateway.com>' \
    -H 'Authorization: hereGoesTheLongLongLongLongToken'

{
  "as2MessageId": "<0123456789.012.1574218406002@as2gateway.com>",
  "persistedTimestamp": 1574218406002,
  "compressed": true,
  "encrypted": true,
  "signed": true,
  "subject": "AWSMPTNR",
  "receiverId": "AWSMPTNR",
  "senderId": "AWSMSTTN",
  "partnerType": "Production"
}

Note that, while you checked sent and failed and decided to check queued, AS2G may have started to send the message. In that case it may not appear under queued as well - because we don't currently expose "retry-in-progress" messages via the queued endpoint.

Bottom Line: Checking Sent-out Message Status

  1. Call the send API.
  2. Give AS2G a few seconds - ideally 10-20 seconds - so it can try to send the message once.
  3. Check sent (Link header from send call). If the message is there, all went well.
  4. If message is not in sent, check failed. If it is there, the send-out has failed.
  5. If both endpoints don't have the message, AS2G is probably busy (it has not had a chance to process your message).
    1. Check queued, after a few seconds. If you see the message, it has probably failed once - and an entry should now be there under failed.
    2. If queued doesn't have the message, check sent and failed again - just to make sure.
    3. If it is still missing, repeat the queued-sent-failed check after a few seconds' delay.
    4. Unlikely but possible (if you are really unlucky): if the message remains missing after several such tries, something has gone wrong in AS2G itself; shout out for help.

There's More!

Cool! Now you can send out AS2 messages via the AS2G REST API - and ensure that it actually got sent out, check the received MDN, and so forth.

But the API goes way beyond that:

  • download actual MDN for a sent message
  • stop/resume retrying of a queued message
  • check received messages: GET /messages/received - with several filters
  • download attachments of a received message
  • mark/unmark a received message as "read" - so you won't see it again in the received messages listings
  • list sent, queued and failed messages - similar to /received

We are constantly working on improving the API - so, by the time you read this, there may be a lot of other cool features in the API as well; check our API docs for the complete and latest updates!

In Closing

AS2 Gateway makes it pretty easy to manage your B2B document exchange.

So far AS2G supported SFTP - and custom mechanisms for the On-Premise version - as integration points.

But now, with the REST API, it all becomes way much easier!

Right now, the REST API is available for all AS2G customers, at no extra cost.

But the best news is, it is fully available - along with all the other premium features - during the 30-day, extensible free trial!

So check it out, try it out, and tell us what you think!

Good luck with all your B2B communications!

Tuesday, October 15, 2019

JAR File Handles: Clean Up After Your Mess!

In Ultra ESB we use a special hot-swap classloader that allows us to reload Java classes on demand. This allows us to literally hot-swap our deployment units - load, unload, reload with updated classes, and phase-out gracefully - without restarting the JVM.

Hot swap, baby!

Windows: supporting the forbidden land

In Ultra ESB Legacy the loader was working fine on Windows, but on the newer X-version it seemed to be having some hiccups. We are not supporting Windows as a target platform, so it didn't matter much - until recently, when we decided to support non-production distros on Windows. (Our enterprise integration IDE UltraStudio runs fine on Windows, so Windows devs, you are all covered.)

AdroitLogic UltraStudio - Your Enterprise Integration IDE

TDD FTW

Fixing the classloader was a breeze, and all tests were passing; but I wanted to back my fixes up with some extra tests, so I wrote a few new ones. Most of these involved creating a new JAR file in a subditrectory under the system temp directory, and using the hot-swap classloader to load different artifacts that were placed inside the JAR. For extra credit on best practices, I also made sure to add some cleanup logic to delete the temp subdirectory via FileUtils.deleteDirectory().

And then, things went nuts.

And the tear-down was no more.

All tests were passing, in both Linux and Windows; but the final tear-down logic was failing in Windows, right at the point where I delete the temp subdirectory.

Being on Windows, I didn't have the luxury of lsof; fortunately, Sysinternals already had just the thing I needed: handle64.

Finding the culprit was pretty easy: hit a breakpoint in tearDown() just before the directory tree deletion call, and run a handle64 {my-jar-name}.jar.

Bummer.

My test Java process, was holding a handle to the test JAR file.

Hunting for the leak

No. Seriously. I didn't.

No. Seriously. I didn't.

Naturally, my first suspect was the classloader itself. I spent almost half an hour going over the classloader codebase again and again. No luck. Everything seemed rock solid.

The "leak dumper"; a.k.a my Grim Reaper for file handles

My best shot was to see what piece of code had opened the handler to the JAR file. So I wrote a quick-n-dirty patch to Java's FileInputStream and FilterInputStream that would dump acquire-time stacktrace snapshots; whenever a thread holds a stream open for too long.

This "leak dumper" was partly inspired by our JDBC connection pool that detects unreleased connections (subject to a grace period) and then dumps the stacktrace of the thread that borrowed it - back at the time it was borrowed. (Kudos to Sachini, my former colleague-intern at AdroitLogic.)

The leak, exposed!

Sure enough, the stacktrace revealed the culprit:

id: 174 created: 1570560438355
--filter--

  java.io.FilterInputStream.<init>(FilterInputStream.java:13)
  java.util.zip.InflaterInputStream.<init>(InflaterInputStream.java:81)
  java.util.zip.ZipFile$ZipFileInflaterInputStream.<init>(ZipFile.java:408)
  java.util.zip.ZipFile.getInputStream(ZipFile.java:389)
  java.util.jar.JarFile.getInputStream(JarFile.java:447)
  sun.net.www.protocol.jar.JarURLConnection.getInputStream(JarURLConnection.java:162)
  java.net.URL.openStream(URL.java:1045)
  org.adroitlogic.x.base.util.HotSwapClassLoader.loadSwappableClass(HotSwapClassLoader.java:175)
  org.adroitlogic.x.base.util.HotSwapClassLoader.loadClass(HotSwapClassLoader.java:110)
  org.adroitlogic.x.base.util.HotSwapClassLoaderTest.testServiceLoader(HotSwapClassLoaderTest.java:128)
  sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
  sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
  sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
  java.lang.reflect.Method.invoke(Method.java:498)
  org.testng.internal.MethodInvocationHelper.invokeMethod(MethodInvocationHelper.java:86)
  org.testng.internal.Invoker.invokeMethod(Invoker.java:643)
  org.testng.internal.Invoker.invokeTestMethod(Invoker.java:820)
  org.testng.internal.Invoker.invokeTestMethods(Invoker.java:1128)
  org.testng.internal.TestMethodWorker.invokeTestMethods(TestMethodWorker.java:129)
  org.testng.internal.TestMethodWorker.run(TestMethodWorker.java:112)
  org.testng.TestRunner.privateRun(TestRunner.java:782)
  org.testng.TestRunner.run(TestRunner.java:632)
  org.testng.SuiteRunner.runTest(SuiteRunner.java:366)
  org.testng.SuiteRunner.runSequentially(SuiteRunner.java:361)
  org.testng.SuiteRunner.privateRun(SuiteRunner.java:319)
  org.testng.SuiteRunner.run(SuiteRunner.java:268)
  org.testng.SuiteRunnerWorker.runSuite(SuiteRunnerWorker.java:52)
  org.testng.SuiteRunnerWorker.run(SuiteRunnerWorker.java:86)
  org.testng.TestNG.runSuitesSequentially(TestNG.java:1244)
  org.testng.TestNG.runSuitesLocally(TestNG.java:1169)
  org.testng.TestNG.run(TestNG.java:1064)
  org.testng.IDEARemoteTestNG.run(IDEARemoteTestNG.java:72)
  org.testng.RemoteTestNGStarter.main(RemoteTestNGStarter.java:123)

Gotcha!

  java.io.FilterInputStream.<init>(FilterInputStream.java:13)
  ...
  sun.net.www.protocol.jar.JarURLConnection.getInputStream(JarURLConnection.java:162)
  java.net.URL.openStream(URL.java:1045)
  org.adroitlogic.x.base.util.HotSwapClassLoader.loadSwappableClass(HotSwapClassLoader.java:175)

But still, that didn't tell the whole story. If URL.openStream() opens the JAR, why does it not get closed when we return from the try-with-resources block?

        try (InputStream is = jarURI.toURL().openStream()) {
            byte[] bytes = IOUtils.toByteArray(is);
            Class<?> clazz = defineClass(className, bytes, 0, bytes.length);
            ...
            logger.trace(15, "Loaded class {} as a swappable class", className);
            return clazz;

        } catch (IOException e) {
            logger.warn(16, "Class {} located as a swappable class, but couldn't be loaded due to : {}, " +
                    "trying to load the class as a usual class", className, e.getMessage());
            ...
        }

Into the wild: JarURLConnection, URLConnection, and beyond

Thanks to Sun Microsystems who made it OSS, I could browse through the JDK source, right up to this shocking comment - all the way down, in java.net.URLConnection:

    private static boolean defaultUseCaches = true;

   /**
     * If <code>true</code>, the protocol is allowed to use caching
     * whenever it can. If <code>false</code>, the protocol must always
     * try to get a fresh copy of the object.
     * <p>
     * This field is set by the <code>setUseCaches</code> method. Its
     * value is returned by the <code>getUseCaches</code> method.
     * <p>
     * Its default value is the value given in the last invocation of the
     * <code>setDefaultUseCaches</code> method.
     *
     * @see     java.net.URLConnection#setUseCaches(boolean)
     * @see     java.net.URLConnection#getUseCaches()
     * @see     java.net.URLConnection#setDefaultUseCaches(boolean)
     */
    protected boolean useCaches = defaultUseCaches;

Yep, Java does cache JAR streams!

From sun.net.www.protocol.jar.JarURLConnection:

    class JarURLInputStream extends FilterInputStream {
        JarURLInputStream(InputStream var2) {
            super(var2);
        }

        public void close() throws IOException {
            try {
                super.close();
            } finally {
                if (!JarURLConnection.this.getUseCaches()) {
                    JarURLConnection.this.jarFile.close();
                }
            }

        }
    }

Gotcha!

If (well, because) useCaches is true by default, we're in for a big surprise!

Let Java cache its JARs, but don't break my test!

JAR caching would probably improve performance; but does that mean I should stop cleaning up after - and leave behind stray files after each test?

(Of course I could say file.deleteOnExit(); but since I was dealing with a directory hierarchy, there was no guarantee that things would get deleted in order, and undeleted directories would be left behind.)

So I wanted a way to clean up the JAR cache - or at least purge just my JAR entry; after I am done, but before the JVM shuts down.

Disabling JAR caching altogether - probably not a good idea!

URLConnection does offer an option to avoid caching connection entries:

   /**
     * Sets the default value of the <code>useCaches</code> field to the
     * specified value.
     *
     * @param   defaultusecaches   the new value.
     * @see     #getDefaultUseCaches()
     */
    public void setDefaultUseCaches(boolean defaultusecaches) {
        defaultUseCaches = defaultusecaches;
    }

It would have been perfect if caching could be disabled per file/URL, as above; our classloader caches all entries as soon as it opens a JAR, so it never needs to open/read that file again. However, once a JAR is open, caching cannot be disabled on it; so once our classloader has opened the JAR, there's no getting rid of the cached file handle - until the JVM itself shuts down!

URLConnection also allows you to disable caching by default for all subsequent connections:

   /**
     * Sets the default value of the <code>useCaches</code> field to the
     * specified value.
     *
     * @param   defaultusecaches   the new value.
     * @see     #getDefaultUseCaches()
     */
    public void setDefaultUseCaches(boolean defaultusecaches) {
        defaultUseCaches = defaultusecaches;
    }

However, if you disable it once, the whole JVM could be affected from that moment onwards - since it probably applies to all URLConnection-based implementations. As I said before, that could hinder performance - not to mention deviating my test from cache-enabled, real-world behavior.

Down the rabbit hole (again!): purging manually from the JarFileFactory

The least-invasive option is to remove my own JAR from the cache, when I know I'm done.

And good news, the cache - sun.net.www.protocol.jar.JarFileFactory - already has a close(JarFile) method that does the job.

But sadly, the cache class is package-private; meaning there's no way to manipulate it from within my test code.

Reflection to the rescue!

Reflection it is.

Thanks to reflection, all I needed was one little "bridge" that would access and invoke jarFactory.close(jarFile) on behalf of me:

class JarBridge {

    static void closeJar(URL url) throws Exception {

        // JarFileFactory jarFactory = JarFileFactory.getInstance();
        Class<?> jarFactoryClazz = Class.forName("sun.net.www.protocol.jar.JarFileFactory");
        Method getInstance = jarFactoryClazz.getMethod("getInstance");
        getInstance.setAccessible(true);
        Object jarFactory = getInstance.invoke(jarFactoryClazz);

        // JarFile jarFile = jarFactory.get(url);
        Method get = jarFactoryClazz.getMethod("get", URL.class);
        get.setAccessible(true);
        Object jarFile = get.invoke(jarFactory, url);

        // jarFactory.close(jarFile);
        Method close = jarFactoryClazz.getMethod("close", JarFile.class);
        close.setAccessible(true);
        //noinspection JavaReflectionInvocation
        close.invoke(jarFactory, jarFile);

        // jarFile.close();
        ((JarFile) jarFile).close();
    }
}

And in my test, I just have to say:

        JarBridge.closeJar(jarPath.toUri().toURL());

Right before deleting the temp directory.

Yay!

So, what's the take-away?

Nothing much for you, if you are not directly dealing with JAR files; but if you are, you might run into this kind of obscure "file in use" errors. (That would hold true for other URLConnection-based streams as well.)

If you happen to be as (un)lucky as I was, just recall that some notorious blogger had written some hacky "leak dumper" patch JAR that would show you exactly where your JAR (or non-JAR) leak is.

Adieu!

Friday, October 4, 2019

Is your JVM leaking file descriptors - like mine?

Okay, there's definitely a leak somewhere.

Foreword: The two issues described here, were discovered and fixed more than a year ago. This article only serves as historical proof, and a beginners' guide on tackling file descriptor leaks in Java.

In Ultra ESB we use an in-memory RAM disk file cache for fast and garbage-free payload handling. Some time back, we faced an issue on our shared SaaS AS2 Gateway where this cache was leaking file descriptors over time. Eventually leading to too many open files errors when the system ulimit was hit.

The Legion of the Bouncy Castle: leftovers from your stream-backed MIME parts?

One culprit, we found, was Bouncy Castle - the famous security provider that had been our profound love since the Ultra ESB Legacy days.

Bouncy Castle FTW!

With some simple tooling we found that BC had the habit of calling getContent() on MIME parts in order to determine their type (say, instanceof checks). True, this wasn't a crime in itself; but most of our MIME parts were file-backed, with a file-cache file on the other end - meaning that every getContent() opens a new stream to the file. So now there are stray streams (and hence file descriptors) pointing to our file cache.

Enough of these, and we would exhaust the file descriptor quota allocated to the Ultra ESB (Java) process.

Solution? Make 'em lazy!

We didn't want to mess with the BC codebase. So we found a simple solution: create all file-backed MIME parts with "lazy" streams. Our (former) colleague Rajind wrote a LazyFileInputStream - inspired by LazyInputStream from jboss-vfs - that opens the actual file only when a read is attempted.

Yeah, lazy.

BC was happy, and so was the file cache; but we were the happiest.

Hibernate JPA: cleaning up after supper, a.k.a closing consumed streams

Another bug we spotted was that some database operations were leaving behind unclosed file handles. Apparently this was only when we were feeding stream-backed blobs to Hibernate, where the streams were often coming from file cache entries.

Hibernate: hassle-free ORM, but with leaks?

After some digging, we came up with a theory that Hibernate was not closing the underlying streams of these blob entries. (It made sense because the java.sql.Blob interface does not expose any methods that Hibernate could use to manipulate the underlying data sources.) This was a problem, though, because the discarded streams (and the associated file handles) would not get released until the next GC.

This would have been fine for a short-term app, but a long-running one like ours could easily run out of file descriptors; such as in case of a sudden and persistent spike.

Solution? Make 'em self-closing!

We didn't want to lose the benefits of streaming, but we didn't have control over our streams either. You might say we should have placed our streams in auto-closeable constructs (say, try-with-resources). Nice try; but sadly, Hibernate was reading them outside of our execution scope (especially in @Transactional flows). As soon as we started closing the streams within our code scope, our database operations started to fail miserably - screaming "stream already closed!".

When in Rome, do as Romans do, they say.

So, instead of messing with Hibernate, we decided we would take care of the streams ourselves.

Rajind (yeah, him again) hacked together a SelfClosingInputStream wrapper. This would keep track of the amount of data read from the underlying stream, and close it up as soon as the last byte was read.

Self-closing. Takes care of itself!

(We did consider using existing options like AutoCloseInputStream from Apache commons-io; but it occurred that we needed some customizations here and there - like detailed trace logging.)

Bottom line

When it comes to resource management in Java, it is quite easy to over-focus on memory and CPU (processing), and forget about the rest. But virtual resources - like ephemeral ports and per-process file descriptors - can be just as important, if not more.

Especially on long-running processes like our AS2 Gateway SaaS application, they can literally become silent killers.

You can detect this type of "leaks" in two main ways:

  • "single-cycle" resource analysis: run a single, complete processing cycle, comparing resource usage before and after
  • long-term monitoring: continuously recording and analyzing resource metrics to identify trends and anomalies

In any case, fixing the leak is not too difficult; once you have a clear picture of what you are dealing with.

Good luck with hunting down your resource-hog d(a)emons!

Monday, September 16, 2019

Sigma IDE now supports Python serverless Lambda functions!

Think Serverless, go Pythonic - all in your browser!

Python. The coolest, craziest, sexiest, nerdiest, most awesome language in the world.

(Okay, this news is several weeks stale, but still...)

If you are into this whole serverless "thing", you might have noticed us, a notorious bunch at SLAppForge, blabbering about a "serverless IDE". Yeah, we have been operating the Sigma IDE - the first of its kind - for quite some time now, getting mixed feedback from users all over the world.

Our standard feedback form had a question, "What is your preferred language to develop serverless applications?"; with options Node, Java, Go, C#, and a suggestion box. Surprisingly (or perhaps not), the suggestion box was the most popular option; and except for two, all other "alternative" options were one - Python.

User is king; Python it is!

We even had some users who wanted to cancel their brand new subscription, because Sigma did not support Python as they expected.

So, in one of our roadmap meetings, the whole Python story came out; and we decided to give it a shot.

Yep, Python it is!

Before the story, some credits are in order.

Hasangi, one of our former devs, was initially in charge of evaluating the feasibility of supporting Python in Sigma. After she left, I took over. Now, at this moment of triumph, I would like to thank you, Hasangi, for spearheading the whole Pythonic move. 👏

Chathura, another of our former wizards, had tackled the whole NodeJS code analysis part of the IDE - using Babel. Although I had had some lessons on abstract syntax trees (ASTs) in my compiler theory lectures, it was after going through his code that I really "felt" the power of an AST. So this is to you, Chathura, for giving life to the core of our IDE - and making our Python journey much, much faster! 🖖

And thank you Matt - for filbert.js!

Chathura's work was awesome; yet, it was like, say, "water inside water" (heck, what kind of analogy is that?). In other words, we were basically parsing (Node)JS code inside a ReactJS (yeah, JS) app.

So, naturally, our first question - and the million-dollar one, back then - was: can we parse Python inside our JS app? And do all our magic - rendering nice popups for API calls, autodetecting resource use, autogenerating IAM permissions, and so on?

Hasangi had already hunted down filbert.js, a derivative of acorn that could parse Python. Unfortunately, before long, she and I learned that it could not understand the standard (and most popular) format of AWS SDK API calls - namely named params:

s3.put_object(
  Bucket="foo",
  Key="bar",
  Body=our_data
)

If we were to switch to the "fluent" format instead:

boto.connect_s3() \
  .get_bucket("foo") \
  .new_key("bar") \
  .set_contents_from_string(our_data)

we would have to rewrite a whole lotta AST parsing logic; maybe a whole new AST interpreter for Python-based userland code. We didn't want that much of adventure - not yet, at least.

Doctor Watson, c'mere! (IT WORKS!!)

One fine evening, I went ahead to play around with filbert.js. Glancing at the parsing path, I noticed:

...
    } else if (!noCalls && eat(_parenL)) {
      if (scope.isUserFunction(base.name)) {
        // Unpack parameters into JavaScript-friendly parameters, further processed at runtime
        var pl = parseParamsList();
...
        node.arguments = args;
      } else node.arguments = parseExprList(_parenR, false);
...

Wait... are they deliberately skipping the named params thingy?

What if I comment out that condition check?

...
    } else if (!noCalls && eat(_parenL)) {
//    if (scope.isUserFunction(base.name)) {
        // Unpack parameters into JavaScript-friendly parameters, further processed at runtime
        var pl = parseParamsList();
...
        node.arguments = args;
//    } else node.arguments = parseExprList(_parenR, false);
...

And then... well, I just couldn't believe my eyes.

Two lines commented out, and it already started working!

That was my moment of truth. I am gonna bring Python into Sigma. No matter what.

Yep. A Moment of Truth.

I just can't give up. Not after what I just saw.

The Great Refactor

When we gave birth to Sigma, it was supposed to be more of a PoC - to prove that we can do serverless development without a local dev set-up, dashboard and documentation round-trips, and a mountain of configurations.

As a result, extensibility and customizability weren't quite in our plate back then. Things were pretty much bound to AWS and NodeJS. (And to think that we still call 'em "JavaScript" files... 😁)

So, starting from the parser, a truckload of refactoring was awaiting my eager fingers. Starting with a Language abstraction, I gradually worked my way through editor and pop-up rendering, code snippet generation, building the artifacts, deployment, and so forth.

(I had tackled a similar challenge when bringing in Google Cloud support to Sigma - so I had a bit of an idea on how to approach the whole thing.)

Test environment

Ever since Chathura - our ex-Adroit wizard - implemented it single-handedly, the test environment was a paramount one among Sigma's feature set. If Python were to make an impact, we were also gonna need a test environment for Python.

Things start getting a bit funky here; thanks to its somewhat awkward history, Python has two distint "flavours": 2.7 and 3.x. So, in effect, we need to maintain two distinct environments - one for each version - and invoke the correct one based on the current function's runtime setting.

(Well now, in fact we do have the same problem for NodeJS as well (6.x, 8.x, 10.x, ...); but apparently we haven't given it much thought, and it hasn't caused any major problems as well! 🙏)

pip install

We also needed a new contraption for handling Python (pip) dependencies. Luckily pip was already available on the Lambda container, so installation wasn't a major issue; the real problem was that they had to be extracted right into the project root directory in the test environment. (Contrary to npm, where everything goes into a nice and manageable node_modules directory - so that we can extract and clean up things in one go.) Fortunately a little bit of (hopefully stable!) code, took us through.

`pip`, and the Python Package Index

Life without __init__.py

Everything was running smoothly, until...

from subdirectory.util_file import util_func
  File "/tmp/pypy/ding.py", line 1, in <module>
    from subdirectory.util_file import util_func
ImportError: No module named subdirectory.util_file

Happened only in Python 2.7, so this one was easy to figure out - we needed an __init__.py inside subdirectory to mark it as an importable module.

Rather than relying on the user to create one, we decided to do it ourselves; whenever a Python file gets created, we now ensure that an __init__.py also exists in its parent directory; creating an empty file if one is absent.

Dammit, the logs - they are dysfunctional!

SigmaTrail is another gem of our Sigma IDE. When writing a Lambda piece by piece, it really helps to have a logs pane next to your code window. Besides, what good is a test environment, if you cannot see the logs of what you just ran?

Once again, Chathura was the mastermind behind SigmaTrail. (Well, yeah, he wrote more than half of the IDE, after all!) His code was humbly parsing CloudWatch logs and merging them with LogResults returned by Lambda invocations; so I thought I could just plug it in to the Python runtime, sit back, and enjoy the view.

I was terribly wrong.

Raise your hand, those who use logging in Python!

In Node, the only (obvious) way you're gonna get something out in the console (or stdout, technically) is via one of those console.{level}() calls.

But Python gives you options - say the builtin print, vs the logging module.

If you go with logging, you have to:

  1. import logging,
  2. create a Logger and set its handler's level - if you want to generate debug logs etc.
  3. invoke the appropriate logger.{level} or logging.{level} method, when it comes to that

Yeah, on Lambda you could also

context.log("your log message\n")

if you have your context lying around - still, you need that extra \n at the end, to get it to log stuff to its own line.

But it's way easier to just print("your log message") - heck, if you are on 2.x, you don't even need those braces!

Good for you.

But that poses a serious problem to SigmaTrail.

Yeah. We have a serious problem.

All those print lines, in one gook of text. Yuck.

For console.log in Node, Lambda automagically prepends each log with the current timestamp and request ID (context.awsRequestId). Chathura had leveraged this data to separate out the log lines and display them as a nice trail in SigmaTrail.

But now, with print, there were no prefixes. Nothing was getting picked up.

Fixing this was perhaps the hardest part of the job. I spent about a week trying to understand the code (thanks to the workers-based pattern); and then another week trying to fix it without breaking the NodeJS flow.

By now, it should be fairly stable - and capable of handling any other languages that could be thrown at it as time passes by.

The "real" runtime: messing with PYTHONPATH

After the test environment came to life, I thought all my troubles were over. The "legacy" build (CodeBuild-driven) and deployment were rather straightforward to refactor, so I was happy - and even about to raise the green flag for an initial release.

But I was making a serious mistake.

I didn't realize it, until I actually invoked a deployed Lambda via an API Gateway trigger.

{"errorMessage": "Unable to import module 'project-name/func'"}

What the...

Unable to import module 'project-name/func': No module named 'subdirectory'

Where's ma module?

The tests work fine! So why not production?

After a couple of random experiments, and inspecting Python bundles generated by other frameworks, I realized the culprit was our deployment archive (zipfile) structure.

All other bundles have the functions at top level, but ours has them inside a directory (our "project root"). This wasn't a problem for NodeJS so far; but now, no matter how I define the handler path, AWS's Python runtime fails to find it!

Changing the project structure would have been a disaster; too much risk in breaking, well, almost everything else. A safer idea would be to override one of the available settings - like a Python-specific environmental variable - to somehow get our root directory on to PYTHONPATH.

A simple hack

Yeah, the answer is right there, PYTHONPATH; but I didn't want to override a hand-down from AWS Gods, just like that.

So I began digging into the Lambda runtime (yeah, again) to find if there's something I could use:

import os

def handler(event, context):
    print(os.environ)

Gives:

{'PATH': '/var/lang/bin:/usr/local/bin:/usr/bin/:/bin:/opt/bin',
'LD_LIBRARY_PATH': '/var/lang/lib:/lib64:/usr/lib64:/var/runtime:/var/runtime/lib:/var/task:/var/task/lib:/opt/lib',
...
'LAMBDA_TASK_ROOT': '/var/task',
'LAMBDA_RUNTIME_DIR': '/var/runtime',
...
'AWS_EXECUTION_ENV': 'AWS_Lambda_python3.6', '_HANDLER': 'runner_python36.handler',
...
'PYTHONPATH': '/var/runtime',
'SIGMA_AWS_ACC_ID': 'nnnnnnnnnnnn'}

LAMBDA_RUNTIME_DIR looked like a promising alternative; but unfortunately, AWS was rejecting it. Each deployment failed with the long, mean error:

Lambda was unable to configure your environment variables because the environment variables
you have provided contains reserved keys that are currently not supported for modification.
Reserved keys used in this request: LAMBDA_RUNTIME_DIR

Nevertheless, that investigation revealed something important: PYTHONPATH in Lambda wasn't as complex or crowded as I imagined.

'PYTHONPATH': '/var/runtime'

And apparently, Lambda's internal agents don't mess around too much with it. Just pull out and read /var/runtime/awslambda/bootstrap.py and see for yourself. 😎

PYTHONPATH works. Phew.

It finally works!!!

So I ended up overriding PYTHONPATH, to include the project's root directory, /var/task/project-name (in addition to /var/runtime). If you want something else to appear there, feel free to modify the environment variable - but leave our fragment behind!

On the bright side, this should mean that my functions should work in other platforms as well - since PYTHONPATH is supposed to be cross-platform.

Google Cloud for Python - Coming soon!

With a few tune-ups, we could get Python working on Google Cloud Functions as well. It's already in our staging environment; and as soon as it goes live, you GCP fellas would be in luck! 🎉

Still a long way to go... But Python is already alive and kicking!

You can enjoy writing Python functions in our current version of the IDE. Just click the plus (+) button on the top right of the Projects pane, select New Python Function File (or New Python File), and let the magic begin!

And of course, let us - and the world - know how it goes!

Thursday, July 4, 2019

Try out Serverless Framework projects - online, in your browser!

Serverless Framework is the unanimous leader in serverless tooling. Yet, there is no easy way to try out Serverless Framework projects online; you do need a decent dev set-up, and a bit of effort to set up sls, npm etc.

To be precise, you did - until now.

Serverless project - in your browser?!

Sigma - the cloud based IDE for serverless application development - can now open, edit and deploy Serverless projects, online - all in your browser!

Sigma: Think Serverless!

Nothing to install, nothing (well, to be extra honest: very little) to configure, and very little to worry about!

  1. Fire up Sigma.
  2. On the Projects page, you'll see a new import a Serverless Framework project option at the bottom.

    Import a Serverless Framework Project!

  3. Enter the path to your serverless.yml file (or the project root).
  4. Goes without saying: click that ⚡ thunderbolt!

Serverless projects online: Sigma's insider story

Internally, Sigma converts your Serverless template and opens it as a Sigma project. From there onwards, you can enjoy all Sigma goodies on your Serverless project; add dependencies, drag-n-drop coding, one-click deploy, sub-second testing, and more!

We are still working on improving support for all sorts of serverless.yml variations, but many of the generic ones should work fine.

By the way, one important thing to note: although we import from Serverless format (serverless.yml), we don’t save content in that format - yet. So if you import a project, make some changes and save it, things will get saved in Sigma’s internal format.

(You can - and probably should - always pick a different repository to save your project, to prevent the original Serverless Framework repo from getting messed up.)

Serverless on Sigma: the missing pieces

Well, as with any new feature, the usual disclaimers apply - this is highly experimental, and could fail to load most, if not all, of your project; it could crash your IDE, kill your cat, bla bla bla.

And, on top of all that, we still do need to:

  • provide support for externalized parameters, based on options (${opt:...}) and external environment variables; currently we take the default value if one is available
  • work something out for plugins
  • add support for a ton of options like API Gateway authorizers and various trigger types that Sigma does not currently support
  • do something about all sorts of things that we find in the custom field

What it all means - for you

We guess this would be a good opportunity for folks to quickly try out Serverless apps and projects "off the shelf" - without actually installing anything on their own systems.

This would be great news for Sigma users as well; because it literally "explodes" the number of samples you can try out on Sigma!

But wait - there's more!

In parallel, we have (correction: we had to) introduced a few other cool improvements:

More control on utility files

Now you have the key utility files (package.json, .gitignore, README.md etc.) exposed at project root. Earlier they were internally managed by Sigma - hidden, out of your sight... but now you can add your own NPM configs, dependencies, scripts and whatnot; write your readme right inside Sigma; and much more! Any dependencies you add via Sigma's built-in Dependency Manager will be automatically added to package.json so you're covered.

Custom resources for your Sigma project!

You can add custom resource definitions to your project! Earlier this was limited to IAM role statements (with the cool Permission Manager), but now you can add whatever you like. EC2 instances, CloudFront distros, IoT stuff, AppSync... anything that you can define in CloudFormation (or GCP's Deployment Manager syntax, for that matter).

The all-new 'Customize Resources' button!

We do hope to introduce Terraform support as well, although the ETA is nowhere in sight yet... so much cool stuff to do, with so few people!

Coming up next...

We do hope to support other project formats - like SAM and raw CloudFormation - in Sigma, pretty soon. So, as always, stay tuned; and, more importantly, shout out loud with what you would like to see in the next Sigma release!

Tuesday, May 28, 2019

AWS Lambda Event Source Mappings: bringing your triggers to order from chaos

Event-driven: it's the new style. (ShutterStock)

Recently we introduced two new AWS Lambda event sources (trigger types) for your serverless projects on Sigma cloud IDE: SQS queues and DynamoDB Streams. (Yup, AWS introduced them months ago; but we're still a tiny team, caught up in a thousand and one other things as well!)

While developing support for these triggers, I noticed a common (and yeah, pretty obvious) pattern on Lambda event source trigger configurations; that I felt was worth sharing.

Why AWS Lambda triggers are messed up

Lambda - or rather AWS - has a rather peculiar and disorganized trigger architecture; to put it lightly. For different trigger types, you have to put up configurations all over the place; targets for CloudWatch Events rules, integrations for API Gateway endpoints, notification configurations for S3 bucket events, and the like. Quite a mess, considering other platforms like GCP where you can configure everything in one place: the "trigger" config of the actual target function.

Configs. Configs. All over the place.

If you have used infrastructure as code (IAC) services like CloudFormation (CF) or Terraform (TF), you would already know what I mean. You need mappings, linkages, permissions and other bells and whistles all over the place to get even a simple HTTP URL working. (SAM does simplify this a bit, but it comes with its own set of limitations - and we have tried our best to avoid such complexities in our Sigma IDE.)

Maybe this is to be expected, given the diversity of services offered by AWS, and their timeline (Lambda, after all, is just a four-year-old kid). AWS should surely have had to do some crazy hacks to support triggering Lambdas from so many diverse services; and hence the confusing, scattered configurations.

Event Source Mappings: light at the end of the tunnel?

Event Source Mappings: light at the end of the tunnel (ShutterStock)

Luckily, the more recently introduced, stream-type triggers follow a common pattern:

This way, you know exactly where you should configure the trigger, and how you should allow the Lambda to consume the event stream.

No more jumping around.

This is quite convenient when you are based on an IAC like CloudFormation:

{
  ...

    // event source (SQS queue)

    "sqsq": {
      "Type": "AWS::SQS::Queue",
      "Properties": {
        "DelaySeconds": 0,
        "MaximumMessageSize": 262144,
        "MessageRetentionPeriod": 345600,
        "QueueName": "q",
        "ReceiveMessageWaitTimeSeconds": 0,
        "VisibilityTimeout": 30
      }
    },

    // event target (Lambda function)

    "tikjs": {
      "Type": "AWS::Lambda::Function",
      "Properties": {
        "FunctionName": "tikjs",
        "Description": "Invokes functions defined in \
tik/js.js in project tik. Generated by Sigma.",
        ...
      }
    },

    // function execution role that allows it (Lambda service)
    // to query SQS and remove read messages

    "tikjsExecutionRole": {
      "Type": "AWS::IAM::Role",
      "Properties": {
        "ManagedPolicyArns": [
          "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
        ],
        "AssumeRolePolicyDocument": {
          "Version": "2012-10-17",
          "Statement": [
            {
              "Action": [
                "sts:AssumeRole"
              ],
              "Effect": "Allow",
              "Principal": {
                "Service": [
                  "lambda.amazonaws.com"
                ]
              }
            }
          ]
        },
        "Policies": [
          {
            "PolicyName": "tikjsPolicy",
            "PolicyDocument": {
              "Statement": [
                {
                  "Effect": "Allow",
                  "Action": [
                    "sqs:GetQueueAttributes",
                    "sqs:ReceiveMessage",
                    "sqs:DeleteMessage"
                  ],
                  "Resource": {
                    "Fn::GetAtt": [
                      "sqsq",
                      "Arn"
                    ]
                  }
                }
              ]
            }
          }
        ]
      }
    },

    // the actual event source mapping (SQS queue -> Lambda)

    "sqsqTriggertikjs0": {
      "Type": "AWS::Lambda::EventSourceMapping",
      "Properties": {
        "BatchSize": "10",
        "EventSourceArn": {
          "Fn::GetAtt": [
            "sqsq",
            "Arn"
          ]
        },
        "FunctionName": {
          "Ref": "tikjs"
        }
      }
    },

    // grants permission for SQS service to invoke the Lambda
    // when messages are available in our queue

    "sqsqPermissiontikjs": {
      "Type": "AWS::Lambda::Permission",
      "Properties": {
        "Action": "lambda:InvokeFunction",
        "FunctionName": {
          "Ref": "tikjs"
        },
        "SourceArn": {
          "Fn::GetAtt": [
            "sqsq",
            "Arn"
          ]
        },
        "Principal": "sqs.amazonaws.com"
      }
    }

  ...
}

(In fact, that was the whole reason/purpose of this post.)

Tip: You do not need to worry about this whole IAC/CloudFormation thingy - or writing lengthy JSON/YAML - if you go with a fully automated resource management tool like SLAppForge Sigma serverless cloud IDE.

But... are Event Source Mappings ready for the big game?

Ready for the Big Game? (Wikipedia)

They sure look promising, but it seems event source mappings do need a bit more maturity, before we can use them in fully automated, production environments.

You cannot update an event source mapping via IAC.

For example, even after more than four years from their inception, event sources cannot be updated after being created via an IaC like CloudFormation or Serverless Framework. This causes serious trouble; if you update the mapping configuration, you need to manually delete the old one and deploy the new one. Get it right the first time, or you'll have to run through a hectic manual cleanup to get the whole thing working again. So much for automation!

The event source arn (aaa) and function (bbb) provided mapping already exists. Please update or delete the existing mapping...

Polling? Sounds old-school.

There are other, less-evident problems as well; for one, event source mappings are driven by polling mechanisms. If your source is an SQS queue, the Lambda service will keep polling it until the next message arrives. While this is fully out of your hands, it does mean that you pay for the polling. Plus, as a dev, I don't feel that polling exactly fits into the event-driven, serverless paradigm. Sure, everything boils down to polling in the end, but still...

In closing: why not just try out event source mappings?

Event Source Mappings FTW! (AWS docs)

Ready or not, looks like event source mappings are here to stay. With the growing popularity of data streaming (Kinesis), queue-driven distributed processing and coordination (SQS) and event ledgers (DynamoDB Streams), they will become ever more popular as time passes.

You can try out how event source mappings work, via numerous means: the AWS console, aws-cli, CloudFormation, Serverless Framework, and the easy-as-pie graphical IDE SLAppForge Sigma.

Easily manage your event source mappings - with just a drag-n-drop!

In Sigma IDE you can simply drag-n-drop an event source (SQS queue, DynamoDB table or Kinesis stream) on to the event variable of your Lambda function code. Sigma will pop-up a dialog with available mapping configurations, so you can easily configure the source mapping behavior. You can even configure an entirely new source (queue, table or stream) instead of using an existing one, right there within the pop-up.

Sigma is the shiny new thing for serverless.

When deployed, Sigma will auto-generate all necessary configurations and permissions for your new event source, and publish them to AWS for you. It's all fully managed, fully automated and fully transparent.

Enough talk. Let's start!

Greasemonkey Script Recovery: getting the Great Monkey back on its feet

GreaseMonkey - the Greatest Script Monkey of All Time! (techspot.com)

You just lost all your Greasemonkey scripts? And the complete control of your browser, painfully built over several days of work? Looking all over the place for a Greasemonkey recovery technique posted by some tech magician?

Don't worry, I have been there too.

Damn you, BSOD! You Ate My Monkey! 😱

My OS - Windows 10 - crashed (that infamous ":(" BSOD) and restarted itself, and when I was back, I could see only a blank square where the Great Monkey used to be:

The Monkey was here.

My natural instinct was thinking that my copy of GM had outlived its max life (I have a notorious track record of running with obsolete extensions); so I just went ahead and updated the extension (all the way from April 2018).

And then:

The Monkey says: '   '

Well said. That's all I needed to hear.

Since I'm running Firefox Nightly, I didn't have the luxury of putting the blame on someone else:

Firefox Nightly: the 'insane' build from last night! (Twitter)

Nightly is experimental and may be unstable. - Dear ol' Mozilla

No Greasemonkey. Helpless. Powerless. In a world of criminals... 🐱‍💻

To champion the cause of the innocent, the helpless, the powerless, in a world of criminals who operate above the law. (GreenWiseDesign)

So, Knight Rider quotes aside, what are my options? Rewrite all my life's work?

Naah. Not yet.

The scripts are probably still there; for whatever reason, GM cannot read them now.

Hunting for the lost GM scripts

Earlier, GM scipts were being saved in plaintext somewhere inside the Firefox user profile directory; but not anymore, it seems.

So I picked up the 61413404gyreekansoem.sqlite file from {FireFox profile home}/storage/default/moz-extension+++d2345083-0b49-4052-ac13-2cefd89be9b5/idb/, opened it quick-n-dirty in a text editor, and searched for some of my old script fragments.

Phew, they are there! But they seem to be fragmented; possibly because it's SQLite.

sqlite3.exe to the rescue

So I fired up Android SDK's SQLite tool (I didn't have anything else lying around), and loaded the file:

sqlite> .open 'C:\Users\janaka\AppData\Roaming\Mozilla\Firefox\Profiles\a4And0w1D.default\storage\default\moz-extension+++d2345083-0b49-4052-ac13-2cefd89be9b5\idb\61413404gyreekansoem.sqlite'

sqlite> .dump

PRAGMA foreign_keys=OFF;
BEGIN TRANSACTION;
CREATE TABLE database( name TEXT PRIMARY KEY, ...
...
INSERT INTO object_data VALUES(1,X'30313137653167...00001300ffff');
INSERT INTO object_data VALUES(1,X'30313965386367...00001300ffff');
...

Looks good! So why can't GM read this?

Probably their data model changed so dramatically so that my DB file no longer makes any sense?

Let's reboot Greasemonkey ⚡

FF browser console was also spewing out several errors at load time; I didn't bother delving into the sources, but it looked like relaunching GM with the existing DB was a no-go.

So I moved the DB file to a safe location, deleted the whole extension data directory (C:\Users\janaka\AppData\Roaming\Mozilla\Firefox\Profiles\a4And0w1D.default\storage\default\moz-extension+++d2345083-0b49-4052-ac13-2cefd89be9b5\) and restarted FF.

(Correction: I did back up the whole extension data directory, but it is probably not necessary.)

Nice, the Great Monkey is back!

GreaseMonkey clean startup: at least The Monkey is back - but... but...

FF also opened up the Greasemonkey startup page indicating that it treated the whole thing as a clean install. The extension data directory was also back, with a fresh DB file.

Greasemonkey script migration: first attempt

So, the million-dollar question: how the heck am I gonna load my scripts back to the new DB?

First, let's see if the schemas have actually changed:

Old DB:

CREATE TABLE database( name TEXT PRIMARY KEY, origin TEXT NOT NULL, version INTEGER NOT NULL DEFAULT 0, last_vacuum_time INTEGER NOT NULL DEFAULT 0, last_analyze_time INTEGER NOT NULL DEFAULT 0, last_vacuum_size INTEGER NOT NULL DEFAULT 0) WITHOUT ROWID;
CREATE TABLE object_store( id INTEGER PRIMARY KEY, auto_increment INTEGER NOT NULL DEFAULT 0, name TEXT NOT NULL, key_path TEXT);
CREATE TABLE object_store_index( id INTEGER PRIMARY KEY, object_store_id INTEGER NOT NULL, name TEXT NOT NULL, key_path TEXT NOT NULL, unique_index INTEGER NOT NULL, multientry INTEGER NOT NULL, locale TEXT, is_auto_locale BOOLEAN NOT NULL, FOREIGN KEY (object_store_id) REFERENCES object_store(id) );

// skimmin...

CREATE INDEX index_data_value_locale_index ON index_data (index_id, value_locale, object_data_key, value) WHERE value_locale IS NOT NULL;
CREATE TABLE unique_index_data( index_id INTEGER NOT NULL, value BLOB NOT NULL, object_store_id INTEGER NOT NULL, object_data_key BLOB NOT NULL, value_locale BLOB, PRIMARY KEY (index_id, value), FOREIGN KEY (index_id) REFERENCES object_store_index(id) , FOREIGN KEY (object_store_id, object_data_key) REFERENCES object_data(object_store_id, key) ) WITHOUT ROWID;

// skim...

CREATE TRIGGER object_data_delete_trigger AFTER DELETE ON object_data FOR EACH ROW WHEN OLD.file_ids IS NOT NULL BEGIN SELECT update_refcount(OLD.file_ids, NULL); END;
CREATE TRIGGER file_update_trigger AFTER UPDATE ON file FOR EACH ROW WHEN NEW.refcount = 0 BEGIN DELETE FROM file WHERE id = OLD.id; END;

New DB:

CREATE TABLE database( name TEXT PRIMARY KEY, origin TEXT NOT NULL, version INTEGER NOT NULL DEFAULT 0, last_vacuum_time INTEGER NOT NULL DEFAULT 0, last_analyze_time INTEGER NOT NULL DEFAULT 0, last_vacuum_size INTEGER NOT NULL DEFAULT 0) WITHOUT ROWID;
CREATE TABLE object_store( id INTEGER PRIMARY KEY, auto_increment INTEGER NOT NULL DEFAULT 0, name TEXT NOT NULL, key_path TEXT);

// skim...

CREATE TABLE unique_index_data( index_id INTEGER NOT NULL, value BLOB NOT NULL, object_store_id INTEGER NOT NULL, object_data_key BLOB NOT NULL, value_locale BLOB, PRIMARY KEY (index_id, value), FOREIGN KEY (index_id) REFERENCES object_store_index(id) , FOREIGN KEY (object_store_id, object_data_key) REFERENCES object_data(object_store_id, key) ) WITHOUT ROWID;

// skim...

CREATE TRIGGER object_data_delete_trigger AFTER DELETE ON object_data FOR EACH ROW WHEN OLD.file_ids IS NOT NULL BEGIN SELECT update_refcount(OLD.file_ids, NULL); END;
CREATE TRIGGER file_update_trigger AFTER UPDATE ON file FOR EACH ROW WHEN NEW.refcount = 0 BEGIN DELETE FROM file WHERE id = OLD.id; END;

Well, looks like both are identical!

So theoretically, I should be able to dump data from the old DB (as SQL) and insert 'em to the new DB.

no such function: update_refcount? WTF?

Old DB:

sqlite> .dump

PRAGMA foreign_keys=OFF;
...
INSERT INTO object_data VALUES(1,X'3031...ffff');
...

New DB:

sqlite> INSERT INTO object_data VALUES(1,X'3031...ffff');

Error: no such function: update_refcount

Bummer.

Is FF actually using non-standard SQLite? I didn't wait to find out.

GreaseMonkey, hang in there buddy... I WILL fix ya! (me.me)

Greasemonkey Import/Export, to the rescue!

If you haven't seen or used it before, GM comes with two handy Export a backup and Import a backup options; they basically allow you to export all your scripts as a zip file, and later import them back.

And, from earlier, you would have guessed that our "barrier to entry" is most probably the SQLite trigger statement for AFTER INSERT ON object_data:

CREATE TRIGGER object_data_insert_trigger AFTER INSERT ON object_data FOR EACH ROW WHEN NEW.file_ids IS NOT NULL BEGIN SELECT update_refcount(NULL, NEW.file_ids); END;

So, what if I:

  • drop that trigger,
  • import the old data dump into the new DB,
  • fire up the fox,
  • use Greasemonkey's Export a backup feature to export (download) the recovered scripts,
  • shut down the fox,
  • delete the new DB,
  • restart the fox (so it creates a fresh DB), and
  • import the zip file back-up?

Yay! that worked! 🎉

The second step caused me a bit of confusion. I was copying the dumped data right off the SQLite screen and pasting it into the SQLite console of the new DB; but it appeared that some lines were too long for the console input, and those inserts were causing cascading failures. Hard to figure out when you copy a huge chunk with several inserts, paste and run them, and all - except the first few - fail miserably.

So I created a SQL file out of the dumped data, and imported the file via the .read command:

Old DB:

sqlite> .output 'C:\Windows\Temp\dump.sql'
sqlite> .dump
sqlite> .quit

Cleanup:

Now, open dump.sql and remove the DDL statements; CREATE TABLE, CREATE INDEX, CREATE TRIGGER, etc.

New DB:

sqlite> DROP TRIGGER object_data_insert_trigger;
DROP TRIGGER object_data_update_trigger;
DROP TRIGGER object_data_delete_trigger;
DROP TRIGGER file_update_trigger;

sqlite> .read 'C:\Windows\Temp\dump.sql'

All else went exactly as planned.

Now, now, not so fast! Calm down... Take a deep breath. It works. (CrazyFunnyPhotos)

Welcome back, O Great Monkey! 🙏

So, my Greasemonkey set up is back online, running as smoothly as ever!

Final words: if you ever hit an issue with GM:

  • don't panic.
  • back up your extension data directory right away.
  • start from scratch (get the monkey running), and
  • carefully feed the monkey (yeah, Greasemonkey) with the old configs again.