This repository holds the third homework for the DevOps course. It builds up on the code that Dr. Parnin had provided here. The homework specification can be found here. I will first introduce the goal in brief and then describe the different use cases in the following sections, ending with a screencast showing the capabilities of this program.
The goal here is to get comfortable with using redis, a server based key-value store. Consequently, this includes spawning Node.js servers on different ports and maintaining them through redis lists. Finally, this homework allows us to create proxies for all the requests to our application with the help of the servers we spawned in the previous step.
- Clone my repository.
- Install
Node.jsif you don't already have it. - Run
npm installon the root directory. - Run
node main.jsto start off the main server (link will be shown in console). - Open a browser and go to the link shown (with speecific requests) to do all the functionalities listed as the requirement of the homework. It is important that you follow the link shown in the console as simply going to
localhostwon't do. This is because I have usedos.hostname()to get the hostname from the operating system and then fed that toExpress.js. Thus when it does a DNS lookup for the hostname, it gets the current IP and that's where it starts the server, not the loopback address (whichlocalhostresolves to).
client here is the redis client obtained by require('redis').createClient.
-
/set/key: This portion was completed in the workshop. Basically, I form a route inmain.jsto catch the/set/:keypattern. The key is automatically extracted byExpress.jsand aredisvariable with the key name set as the key provided is created (usingclient.set) with the value "This message will self-destruct in 10 seconds.". Thenclient.expireis called on thatrediskey and the timer is set to10. -
/get/key: Here, I have formed another route matching/get/:key. The key is extracted andclient.getis used to get the value for that specific key. The obtained value is returned to the user as an HTTP response. I have formatted the response to have some more information that just the value. -
/upload: Usecurlto upload an image to the server. This image will be processed and the data will be stored in aredislist usingclient.lpush. I use the followingcurlcommand to achieve this. The-Lflag at the end is to make it follow redirects, which will be important later on. Be sure to provide your own IP and a proper image name.curl -F "image=@./img/image_name.jpg" http://IP:3000/upload -L -
/meow: This call from a browser gets the most recent image data stored in theredislist usingclient.lpopand feeds it back as a response to the browser in an<img>tag. If there are no images, then it will give an appropriate error message. -
/recent: Whenever any request is made, an all-catcher pattern usingapp.useregisters the specific URL requested in anotherredislist byclient.lpushand consequentlyclient.ltrimto keep its size to5. Then, when/recentis visited from the browser, this recently visited URL list is fetched usingclient.lrangeand shown to the user. -
/spawn: The program keeps a track of the current maximum port number used. This starts from3000as that is where the main HTTP server had been initially started. In my program, the main server acts almost the same as any other later-spawned proxy server, with only a few differences:- You cannot destroy the server running at the
3000port. - This server is not stored in the spawned servers
redislist. - If no proxy servers exist, this server will handle all the requests by itself.
Coming back to the
spawnfunctionality, whenever this is requested, I increment the current maximum port and start a newExpress.jsserver at that port. The server URL is then pushed to aredislist usingclient.rpush. The response to the browser contains the new server URL that has been formed. - You cannot destroy the server running at the
-
/listservers: Later on, for the proxy part of this assignment, I have used a separateredislist to store my currently busy servers. So to list all the servers, I do aclient.lrangecall for both the lists and show them to the user appropriately. -
/destroy: For this part of the assignment, I first take the length of my free serversredislist usingclient.llen. Then I randomly choose an interger between0andlength - 1, wherelengthis the return value of theclient.llencall. This random index is the one which I will remove from theredislist. I first get the value from that index usingclient.lindexand then remove that value from the list usingclient.lrem. A/listserverscall after that should show that the server is not there. Ideally, removing a server should also mean that the server is completely destroyed, i.e. it cannot be accessed anymore through the browser, but as per Professor's suggestions (in Slack), for the assignment, I have only removed the server from theredislist and nothing more. -
Proxies: I have implemented this in two ways - using the
requestNode.js module and using redirection. When any request comes in to my program, I do the following things:- I have an initial all-catcher pattern which sees if this current server URL is present in my busy servers
redislist. If so, then this particular server process (or port) must have been previously added to this list by another server process to delegate this particular request. So I move on to handle this request. Before that, I also remove myself from the busy servers list and add to the free servers list, so that I can be available for later requests. - If this server is not present in the list, then this is the initial entry point of the request from the browser and I can proxy this request to any other available server. I do that by simply doing a
client.rpoplpushwhich takes the last server in the free serversredislist and puts it in the top of the busy serversredislist. Now two things can follow:- There was nothing in the free servers
redislist to do arpoplpush. In that case, the current server, which was trying to proxy the request, handles the request itself. - If the
rpoplpushreturned a non-null value, then it was able to push something to the busy serversredislist. Now, this current server just needs to forward the request to the server URL that it just pushed to the busy serversredislist. Then two different cases can ensue:- The request was a
GETrequest. This is simple - just use therequestmodule to make aGETrequest to the specified server and once it returns, send the response back to the user. In this case, the user won't be able to see the proxy server URL in the browser address bar and hence won't be able to determine which server process actually handled its request. - For
POSTrequests (like/upload), I used redirection. I could use either for both of these cases, but using both allowed me to learn the pros and cons of them. RedirectingPOSTrequests is a little tricky, because just a normalres.redirect(url)will not make the requestPOSTbut will rather convert it to a defaultGETrequest, thus losing thePOSTparameters. This is a norm for HTTP requests (read here). To maintain the form data and the request method, the redirect has to be done with the status code of307. This is what I have done -res.redirect(307, url). Also, to usecurlproperly with this redirection, you have to add the-Lflag which tellscurlto follow redirects.
- The request was a
- There was nothing in the free servers
- I have an initial all-catcher pattern which sees if this current server URL is present in my busy servers
https://www.youtube.com/watch?v=aZ9JBP2wp14&feature=youtu.be