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]