Sunday, March 5, 2023

JavaScript infinite streams

This post was heavily inspired by this post: Infinite Data structures in Javascript  (author: Dimitris Papadimitriou)

After reading the article, I also wanted to have the bare minimum, and most simple implementation of a Stream - and surprisingly, it is very easy (in terms of code), but also, kinda complicated. Kind of the sweetspot I really like to learn about!

In this implementation, we add a filter method to the stream object, which takes a predicate function pred and returns a new stream object with only the elements that pass the predicate. If the current value passes the predicate, then the resulting stream will include the current value and the result of calling filter on the next property. Otherwise, the resulting stream will only include the result of calling filter on the next property. If there is no next property, then the emptyStream object is returned.

The resulting mappedStream is a stream of strings with the same structure as the original stream, but with each element converted to a string. The resulting filteredStream is a stream with only the elements that are greater than 1.


What's wrong with eager evaluation? (And why you should use fp-ts/Fluture for that)

Eager evaluation in JavaScript Promises can lead to several problems. Eager evaluation means that a Promise's executor function is executed immediately when the Promise is created, rather than when it is needed later on. Here are some potential issues with eager evaluation:

Increased resource usage: If the Promise's executor function performs a resource-intensive operation, eager evaluation can cause unnecessary resource usage. For example, if the Promise is used to fetch data from a server, eager evaluation would mean that the fetch operation is performed immediately, even if the data is not needed until later.

  • Unnecessary blocking: If the Promise's executor function performs a blocking operation (such as a long-running loop), eager evaluation can cause unnecessary blocking of the main thread. This can lead to unresponsive user interfaces and other performance issues.
  • Wasted work: If the Promise's executor function performs work that is not needed (for example, if it fetches data that is never used), eager evaluation can result in wasted work and unnecessary network traffic.
  • Race conditions: Eager evaluation can also lead to race conditions, where multiple Promises are created but only one of them is needed. This can result in unnecessary resource usage and can make code harder to reason about.

To avoid these problems, it's generally better to use lazy evaluation with Promises. In lazy evaluation, the Promise's executor function is only executed when the Promise is needed (for example, when it is passed to a then() method or when it is awaited in an async/await function). This approach allows for more efficient use of resources and can help prevent performance issues.

The fp-ts library provides several abstractions and functions that can help avoid the problems associated with eager evaluation in Promises. For example, the type Task is essentially a Promise wrapped in a function, allowing you to control when it's executed, therefore it's considered "lazy".

import { task } from 'fp-ts/lib/Task'

const fetchTask = task(() => fetch('https://example.com/data'))

If you call fetchTask(), only then will your HTTP call be executed.

Fluture

There are also other viable options, like Fluture, which is a Future implementation in Javascript. I'm not aiming to discuss Future-s here, but it might be mentioned, that it is a monadic interface that is lazy by it's nature - in a way it's similar to Promise, but more functional, with it's advantages.

OPENAPI discriminators

 OpenAPI discriminators are a feature of the OpenAPI Specification (formerly known as Swagger) that allows you to define alternative schemas for different values of a property.

In other words, you can use a discriminator to specify different schema definitions based on the value of a property. For example, you may have an "animal" schema with a discriminator property called "type". The "type" property could have possible values of "cat" or "dog". You could then define different schema definitions for the "cat" and "dog" types.

Discriminators are especially useful for modeling inheritance in your API schema. By using discriminators, you can define common properties and behaviors for a group of related schemas, while still allowing for differences in their specific implementations.

The discriminator property is used to select the appropriate schema definition for a given value. The value of the discriminator property is typically found in the JSON payload of a request or response.

Let's say we want to define a schema for different types of animals, with common properties like "name" and "age", but with different properties based on the type of animal. We can use a discriminator to define alternative schemas based on the value of the "type" property:

components:

  schemas:

    Animal:

      type: object

      properties:

        name:

          type: string

        age:

          type: integer

      discriminator:

        propertyName: type

      required:

        - type

    

    Dog:

      allOf:

        - $ref: '#/components/schemas/Animal'

        - type: object

          properties:

            breed:

              type: string

    

    Cat:

      allOf:

        - $ref: '#/components/schemas/Animal'

        - type: object

          properties:

            color:

              type: string

In this example, we define a base schema called "Animal" with properties for "name" and "age", and a discriminator called "type" that will be used to select the appropriate schema for each animal. We also define two alternative schemas called "Dog" and "Cat", which extend the "Animal" schema with additional properties specific to dogs and cats.
If a request or response payload includes an animal with a "type" property of "dog", the schema definition for "Dog" will be used. Similarly, if the "type" property is "cat", the "Cat" schema will be used.
Here's an example payload for a dog:
{
  "type": "dog",
  "name": "Fido",
  "age": 3,
  "breed": "Golden Retriever"
}
And here's an example payload for a cat:
{
  "type": "cat",
  "name": "Whiskers",
  "age": 2,
  "color": "tabby"
}

In this way, discriminators allow you to define flexible, extensible API schemas that can adapt to a variety of use cases.

AsyncGenerator

In TypeScript, an AsyncGenerator is a special type of generator function that can be used to asynchronously generate a sequence of values.

Like a regular generator function, an AsyncGenerator is defined using the function* syntax, but with the addition of the async keyword before the function keyword:

async function* myAsyncGenerator() {  // ...}

An AsyncGenerator function can use the yield keyword to return values one at a time, just like a regular generator. However, because it is an asynchronous function, it can also use the await keyword to pause execution until an asynchronous operation completes before continuing to the next yield statement.

Here is an example of an AsyncGenerator that asynchronously generates a sequence of random numbers:

async function* randomNumbers(count: number): AsyncGenerator<number> {  for (let i = 0; i < count; i++) {    // Wait for a random number to be generated asynchronously    await new Promise(resolve => setTimeout(resolve, 1000));  }}

To use an AsyncGenerator, you can call it like a regular generator and iterate over the values it generates using a for-await-of loop:

async function printRandomNumbers(count: number) {
  for await (const number of randomNumbers(count)) {
    console.log(number);
  }
}
This will asynchronously generate and print count random numbers, one per second.

Employee Stock Options

Stock option: the opportunity to buy a stock at a set price up until some date in the future.

So what does this actually mean for an employee? Let's say you are given 100 options for the price 15 USD for 5 years. That would be worth now (if you could sell it) 1500 USD-s. Let's say in five years the stock of the company goes up 5 USD-s, so now it's 20 USD-s. Because you got the options for 15 USD, and now it's worth 20 USD-s, the company practically gives you 5 USD-s (20-15) per options, so you have 100*5 = 500 USD profit!

Vesting refers to the process by which an employee earns ownership of an asset, such as stock options, over time. In the context of employee stock options, vesting typically means that an employee gains the right to exercise (i.e., purchase) a certain number of shares of their company's stock at a set price (known as the "strike price") over a predetermined period of time, known as the "vesting period."

The vesting period is usually several years long and is intended to incentivize employees to stay with the company and contribute to its success over the long term. As the employee meets certain milestones or stays with the company for a certain length of time, they earn the right to exercise more of their stock options. Once the options have vested, the employee has the choice to exercise them, usually by paying the strike price, and can then sell the shares on the open market or hold onto them.

Employee stock options are a common form of equity compensation used by companies to attract and retain talented employees. They are typically offered to employees at all levels of the organization, from executives to rank-and-file workers. The number of options granted and the terms of the vesting schedule vary from company to company and can depend on factors such as the employee's role, tenure, and performance. The hope is that by providing employees with a financial stake in the company's success, they will be more motivated to work hard and help the company achieve its goals.

Saturday, March 4, 2023

PromQL (Prometheus Query Language) notes

PromQL (Prometheus Query Language) is a query language used to retrieve and manipulate time series data stored in Prometheus. Mos important aspects of PromQL:

  • Selecting metrics: You can select metrics by their name or by using regular expressions. For example, up selects all metrics with the name up, while node_cpu.* selects all metrics with names starting with node_cpu.
  • Filtering metrics: You can filter metrics by their labels using curly braces {}. For example, up{job="prometheus"} selects all metrics with the name up and the label job equal to prometheus.
  • Aggregating data: PromQL provides a variety of functions to aggregate time series data. For example, sum() calculates the sum of values across multiple time series, while avg() calculates the average value across multiple time series.
  • Grouping data: You can group data by one or more labels using the by keyword. For example, sum(rate(http_requests_total{method="GET"}[5m])) by (status_code) groups the data by the status_code label.
  • Working with time: PromQL supports a variety of time-related functions, such as time() to get the current timestamp, offset() to shift the data by a certain time interval, and rate() to calculate the rate of change over a time period.

PromQL provides a powerful and flexible way to analyze and query time series data stored in Prometheus.

Consistent hashing via hash ring

 Consistent hashing via hash ring is a technique used in distributed computing systems to evenly distribute data and workload across a group of servers, while also minimizing the impact of server failures and additions on the system.

In this technique, servers are arranged in a circular ring, with each server represented by a point on the ring. The ring is typically implemented using a hash function that maps keys to points on the ring. The keys in this context refer to the data or workload that needs to be distributed across the servers.

To assign a key to a server, the hash function is applied to the key to obtain its corresponding point on the ring. The server whose point on the ring is the next highest to the key's point is then responsible for handling that key. If a server fails or is added to the system, only the keys that were previously assigned to that server need to be reassigned to a new server. The other keys remain with their previously assigned server.

By using this technique, the system can achieve good load balancing, as each server is responsible for handling an approximately equal portion of the keys on the ring. Additionally, the impact of server failures and additions is minimized, as only a small portion of the keys need to be reassigned when a server is added or removed.

Overall, consistent hashing via hash ring is a powerful technique for distributing workload and data in distributed systems, and has been widely used in many large-scale systems, such as content delivery networks and distributed databases.

Backend system design interview notes