TODD KERPELMAN: Serverless computing, it's
all the rage these days, kind of like unicorn-flavored food
or the return of the fanny pack.
But unlike those other trends, I think serverless computing is
going to be around for a while.
Because the idea that you can run a complete working
app without ever needing to set up a server of your own,
that's pretty neat.
Now, there are a lot of pieces involved in a serverless
computing setup, but I think from Cloud Firestore's
perspective, there are two big pieces.
First, you've got the security rules,
the bouncer at the door to make sure those requests coming in
from the client are legit.
This lets you query the database directly from the clients,
and we've talked about this a lot in an earlier video,
so go back and watch that one if you haven't.
But there will still be times you're
going to want to run some kind of logic on the back end,
and that's where Cloud Functions come in.
They let you run what is essentially
server-side code without actually
needing to set up a server.
So let's talk about Cloud Functions
and how they can make your Cloud Firestore implementation more
effective.
[MUSIC PLAYING]
Oh, I hate when they add the fake unicorn flavoring.
Do you want any of this?
No?
All right.
I'll save it for later.
So let's talk about Cloud Functions
and why it's sometimes important for you
to run code on the back end instead of putting
all that logic in your clients.
Often, it's for security.
There are times when you just can't trust clients
to make the changes you want, or the security rules
required to support making these changes from the client
would just be too convoluted.
Sometimes, going with the server-side function
is just plain easier.
Right?
Like, if you've got business logic that you always
want to run on certain bits of data,
it's often a heck of a lot simpler
to write that logic server side and deploy it immediately,
rather than try and ship out updates
to like your mobile and web clients all exactly
at the same time.
And sometimes, it's to perform database-wide maintenance logic
that would be weird to perform on a client,
even if you could work out all those other security issues.
Now Cloud Functions is a pretty weighty topic,
and I can't cover everything in one 15-minute video.
So if you really want all the details
on how to get started implementing Cloud Functions,
I highly recommend you check out our other series on our YouTube
channel, and we willing link to that below,
but let's see if we can give you the executive summary here.
Generally speaking, Cloud Functions
are functions that sit around in the cloud
and are activated when a particular event happens
in your Google Cloud or Firebase project.
For those of you building Cloud Firestore-powered apps,
this is going to be like when a document is created or changed
in your database.
But it can be for other things as well, like if a new user
account is created in Firebase Auth,
or a conversion event has been fired in analytics.
And they can sometimes be in response to a direct HTTP
request which is one way you can use Cloud Firestore to power
like a dynamic website or even create
your own API for your database, but we'll
talk more about that later.
Now, when these events happen, Cloud Functions
kind of spins up a little server instance
in the cloud, in which your function is activated
and asked to do some kind of work in response
to these events, when they happen.
Very often, this will be performing
some kind of calculation based on the incoming data
and then writing or changing information elsewhere
in Cloud Firestore.
But you could also do things like send a notification
or save a newly generated image to cloud storage
or pin your support team Slack channel.
Now, you'll notice most of these final tasks
involve pinging other cloud-based services, which
means they're not going to happen immediately.
Right?
It still takes some amount of time for your event handler
to talk to Cloud Firestore or FCM
or whatever other service you're calling out to.
And in the meantime, Cloud Functions
is sitting there, monitoring your function,
waiting for it to finish, so it can
like shut down the environment and reuse those resources.
So how does it know when all your calls are done?
Most of the time, you're going to have
to do this by having your function return a promise.
A promise is basically a JavaScripty way of saying,
this is a process that's going to do some work,
and when the work is done, I'm going
to change my status either to complete or failed.
In other words, I promise, I will
resolve the status of this object, once the work is done.
Or maybe to put it another way, it's
our functions way of saying, wait, don't terminate me.
Yet I still have so much to live for,
or at least one database call's worth.
Now, generally speaking, returning
a promise from a Cloud Function, it's pretty easy.
Because when you perform an action,
like writing to the database, that call
is already set up to return a promise.
So you really just need to make sure
you're capturing the promise that's
being returned from your database write,
or what have you, and returning that within your function.
Now, maybe this is a good time to point out
there are actually two flavors of Cloud Functions.
You've got Google Cloud Functions which is essentially
the Google Cloud Platform product that's
kind of running this whole thing behind the scenes.
Cloud Functions for Firebase is, to oversimplify
a little bit, a wrapper around Cloud Functions that
makes deploying Cloud Functions and working
with Firebase products easier.
The biggest drawback is that, while Cloud Functions,
the Google product, supports several different languages,
including Python and Go, Cloud Functions for Firebase
only supports TypeScript and JavaScript with no JS.
So if you're coming here from the Firebase world,
and don't mind writing the occasional JavaScript,
I would recommend using Cloud Functions for Firebase,
as it generally makes things fairly easy.
However, if you're coming here from the Google Cloud Platform
world, or you're a Firebase developer who hates JavaScript
with a burning passion of 1,000 suns
and doesn't mind a more complex to deployment process,
maybe go with Google Cloud Functions.
Now, for the rest of this video, I'm
mostly going to be talking about Cloud Functions
for Firebase, because they're the ones that gave me
this T-shirt, but most of these lessons
should still apply to both scenarios.
So to use a semi-realistic example,
let's take a look at my restaurant review app here,
and let's suppose that I want to remove
any bad language that my users might
be submitting in their reviews.
So that's swear word, swear word, swear--
I'm going to check Urban Dictionary for this one.
Whoa, how does something so cute have so much hate in its heart?
Since this is a family-friendly video--
hi, kids-- I won't use any actual bad language.
Let's just replace the words fat-free cheese--
which I think we can all agree is an abomination--
with just cheese.
Obviously, I can't really count on the client
to fix this for me.
Sure, some client-side code might
prevent some casual swearing, but you
have to remember that clients are untrustworthy by nature.
Any halfway decent hacker could modify their client or network
calls to sneak in this undesirable phrase.
So if I really want to be sure no bad words are making it
into my reviews, I need that server side
with the Cloud Function.
So let's see what that looks like.
So because I'm using Cloud Functions for Firebase, which
runs using no JS, I would be writing
this thing in JavaScript, or more likely TypeScript
which you can kind of think of is
like strongly-typed JavaScript, which I prefer.
And in fact, even if you've never
written a single line of TypeScript before,
I recommend starting with that as your language of choice.
Not only does the language itself
have some pretty nifty features, but often, your IDE and/or
linter can analyze your code and catch some common errors,
particularly around dealing with promises.
So give it a try if you haven't.
So generally speaking, after doing a little bit
of setup work, like importing Firebase Functions,
you're going to start by defining a function that looks
a little something like this.
You can see that I'm using the onCreate handler here
to handle cases where a new document is created,
but this could also be onUpdate or onDelete or onWrite which
covers all three of those cases.
Now, we don't want this event to fire anytime
a document is created anywhere in our database, just
when a review is added.
So our document method takes a path argument
that specifies which documents we care about.
You can see that our path looks very similar to how
paths work in security rules.
By adding a string in brackets here,
we're basically saying, hey, make this
a wild card that matches any component in our path
but then store that value in a variable named,
in this case, Review ID that it could access later.
So this onCreate event handler takes in two arguments.
You've got the snapshot which contains information
about the document that's being added
and the context which includes some other data
about the function, like those path wildcards
we just captured.
Now, I'm going to get the fields of this document
by calling snapshot.data, and I'll
want to check to make sure my data isn't null.
And then I can grab the text from the data,
and then finally, I can sanitize it
by calling whatever custom function I want on the text
field of this document.
Then, I can set the new value by taking snapshot.ref--
that's the reference to the document
that was just created--
and calling update on it with this new text.
Now, there are three notes I want to point out here.
First, this call to Cloud Firestore
is something that's happening asynchronously.
Right?
It's making a call to another resource in the cloud.
So this update call immediately returns a promise.
Now, if I wanted, I could chain on additional work
to this promise by adding dot then
which is basically my way of telling my code,
OK, do more work after this promise is resolved.
But in my case, I have no more work to do,
so I'm just going to return this promise.
And that will ensure that my function stays
alive long enough to update my document, which will then
resolve this promise.
Incidentally, you can see that TypeScript is warning me
that I'm not returning anything here if review data is null.
So I'll fix that by explicitly returning
null in a little else block here.
Second, I don't have to limit my document
right to just this document.
By using the Admin SDK, which is basically
our server-side library for talking to Firebase resources,
I could write to any document in my Cloud Firestore database
just by calling admin.firestore.doc
and entering a path to my document.
This library also lets me do things like talk
to the real-time database or send a notification.
Finally, those of you out there who are paying close attention
might have realized that this function only
activates if I create a new review,
but my users probably have the ability
to edit their reviews too.
So really, we want to sanitize this text anytime a user
creates or edits a review.
So we can do that either by writing
a separate onUpdate event handler or, in this case,
just creating an onWrite handler which combines those two cases.
Now, onWrite and onUpdate work almost the same as onCreate.
The biggest difference is that, instead
of being passed a single document snapshot to represent
the document being written, I now have a Change object,
and this object contains two document snapshots--
Before to represent the object that was there before and After
to represent the object post change.
In my case, I'm really only interested in the After object.
That represents the text after the review has been created
or after the edit has been made.
So with a few small changes, I can now make this function work
both for new reviews and for edits,
but there's one more teeny, tiny, little problem here.
I have now introduced an infinite loop into my function.
Now, infinite loops are generally
considered to be a terrible thing.
In fact, for more important information on infinite loops,
make sure you follow the link below.
The issue here is that, when my Cloud Function writes
its sanitized version to the database, that
write is triggering a brand new onWrite event.
And that's going to wake up this function, which
is going to rewrite the sanitized version
of my sanitized version to the database, which
will trigger a new onWrite event, and so on and so forth
forever.
Now, there are a few ways to fix this.
I think the simplest would just be to add a simple check here
that says, hey, did my sanitize text actually
change from the original?
If it did, write it to the database.
Otherwise, return null.
By returning null, I won't create another write event,
and this function won't trigger again.
And in fact, let me go in and change one of these reviews
here to mention the fat-free cheese pizza.
I can make this change directly in the Firebase console,
and you can see that almost immediately Cloud Functions
has fixed my terrible culinary mistake.
So I fixed that infinite loop issue here,
but this is something you should watch out for,
particularly when you're dealing with any onUpdate or onWrite
handlers that write back to the same document that was changed.
Now, when you write Cloud Functions,
you're generally using our server SDK,
or what we like to call the Admin SDK,
to perform a lot of these Cloud Firestore actions.
It works very similar to the client libraries
you're probably used to but with a couple of key differences.
For one thing, transactions work a little differently.
Unlike the client libraries, these
perform more traditional lock down the database kinds
of transactions, and you can do things
like perform actual queries inside of them.
But probably the biggest difference
from the Cloud Firestore perspective
is that the Admin SDK is not subject to the same security
rules that you have written for your project.
Which if you think about it makes sense, right?
Like, you can't trust a request from any client that comes in,
because who knows where that client's been
or who's hacked it?
But Cloud Functions, those are run in a trusted environment
which means, much like a cop-out for revenge in an '80s movie,
the rules don't apply to them.
Esposito, I might be your chief, but I'm also your friend.
You're running too hot, and you're going to burn up.
And this is often a good thing, as you
will see you in the next video.
But do keep in mind, this also means your validation rules
are getting ignored as well.
So like, test your functions, and make
sure they really are setting data in the format
that you are expecting.
So let's say I were to actually deploy
this function using Firebase.
What would happen?
Well, there's a little work the Firebase command line
tool does to take my code and convert it
into the little bits of serverless magic
that are needed for it to run in Google Cloud Platform land.
For starters, the way Cloud Functions are architected,
every function that responds to an event
lives in its own separate container.
So even if I end up writing all my functions in one file,
the first thing the Firebase command line tool does
is analyze my file and create as many containers
for as many individual functions that it needs to.
In my case, I only have this one clean up new review function,
but if I had say like a calculate average score
function, that would exist in its own separate container.
The command line also does the heavy lifting
of creating the equivalent of those individual deploy
commands for each one of these functions.
If you were using Google Cloud Functions,
you'd need to create commands like this in order
to deploy each visual function, but the Firebase deploy tool,
it handles this for you.
Finally, Firebase does some work to make
sure each one of these functions has all of your environment
variables set up properly.
So that when you call Initialize App on the Admin SDK,
your Cloud Function has access to your database
without your needing to explicitly add
any of your project configuration
information, which is nice.
So then, later on, when a user writes a new restaurant review,
Cloud Functions will go ahead and spin up
a server instance that contains my clean up reviews function.
It will then go ahead and load up node and the Admin SDK
and configure my environment variables,
and then it will run my function in response to this event.
And so understanding all this might explain some the quirks
you might notice when you're dealing with Cloud Functions.
In fact, let me hit you up with the top five quirks
that you will probably run into.
OK, quirk number one, performance might not always
be what you expect.
The very first time you run a function
is when it's going to take the longest.
This is because Cloud Functions needs
to start up that whole server instance
and load in all of your libraries and dependencies
in order to run your function.
This is known as a cold start, and it's often quite slow.
But the more often your function is being called,
the more likely Cloud Functions will keep the server
spun up and ready to go.
Which means that, in general, the busier and more popular
your app is, and the more consistent
your traffic, the faster your Cloud Function will probably
be, which I know is a little counterintuitive.
The flip side of this is that you, as a developer,
will encounter cold starts a lot,
probably about 100 times more often than your customers,
so yay, lucky you.
For example, the first time I tested
that review sanitizing function, it took a second or two
after deploying.
But the second time, it took about 60 milliseconds,
so you got a big difference there.
OK, quick number two, global variables
can be a little weird.
Because each of these functions are essentially
being run in their own environments,
you can't expect them to share any state between them.
If my two Cloud Functions both use the same global variable,
for instance, that global variable
would live separate lives on each server.
Function B wouldn't know what's happening with function A's
global variable.
In fact, if my function is very popular,
I might have multiple servers all handling function A,
and each of their global variables
are going to be different too.
But on the flip side, if a function
does get called repeatedly on the same server instance,
and that server instance hasn't been reclaimed,
then your global variable will persist from one function call
to the next.
So it's a little unpredictable.
OK, quirk number three, speaking of global variables,
I should probably point out that any global variables
that you create--
and this includes things helper functions
or global variables that reference libraries--
will essentially be loaded up in every single Cloud Function
container, whether you need it or not.
So like, if I were to leave in that Admin SDK, that
would get loaded up in the container for my clean up
review function but also any future like welcome new user
function or calculate restaurant review average function
or any other function I write.
Now, in this specific case, this is probably fine,
since most of these functions are
going to need the Admin SDK, but imagine
that one of my functions needed to load up
some really heavy math or an ML library or something.
In this case, it probably doesn't make sense
to be loading that library in the global namespace,
like this, because it'll get loaded up
by every other function instance out there,
and that will hurt all of their cold start times.
So instead, I'd probably want to lazily load it
inside my function itself, and there are ways to do that,
and I would go ahead and check out Doug's video series exactly
on that topic.
OK, quirk number four, since these things are
off on their own little container,
subject to the whims of the internet
and/or cold start times, there's no guarantee
that functions will be performed in the order
that these events were received.
Now, most of the time, this shouldn't be a big deal,
but double check your assumptions.
Like, if my app, for instance, automatically
created a user document after a user created a new account,
I might expect that my user has created
an account function would fire before my document was written
function.
And most of the time, that will be true, but not always.
So as with most things in life, you
should generally be OK, as long as you code defensively.
Watch out for any assumptions that your functions might
be making and like fail gracefully.
Quirk number five is that function sometimes
can be called more than once for the same event.
It's not something that happens often, but it does happen,
and you need to make sure something catastrophic doesn't
occur in those cases.
Now, one thing that does help this, though,
is the event ID property.
This is a unique identifier that's
sent as part of the context parameter with each event,
and so if a function does get triggered twice,
each of those instances will have the same event ID.
Now, you can take advantage of this in several ways.
Some services, like the stripe payment API,
let you pass an item potency key, essentially,
a unique identifier along with every call,
to make sure that a user doesn't get charged twice.
This would be a great situation to use the event ID,
but honestly, duplicate functions in item potency
are big enough topics that I don't think
I can really cover them here.
So again, check out Doug's series or the Cloud Functions
documentation, if you want to find out more.
Hey, haven't I seen you there already?
Oh, deja vu.
But hey, I think we spent a lot of time
talking about Cloud Functions in the abstract,
when we want to use them in practice, beyond eliminating
fat-free cheese from the earth.
Well, I'm going to cover five common scenarios for making
use of Cloud Functions, and we will go over
those in the next video.
No, don't you walk away.
Come back here, Esposito.
Esposito!
[MUSIC PLAYING]