Technologies I’m Impressed by

Following is the list of technologies I’m Impressed by, a completely subjective metric. Since Iโ€™m not impressed often, these deserve to be listed.

Impressed

In arbitrary order:

  1. JetBrains IDEs. The thorough understanding of the code is top notch.
  2. Vim. Amazing productivity when editing text.
  3. Linux. No explanation required ๐Ÿ™‚
  4. FHIR. Very well thought through data exchange standard for healthcare. Extensible everywhere. Profiles to mandate constraints.
  5. AWS CDK. Amazing productivity gain over CloudFormation due to semantic involvement of the tool. It’s about expressing what you want to achieve, not assembling the solution from what you have.

Hope you have found something new to you in this list. In this case, I recommend taking a look at it.

Subjective Python or why I “hate” Python

Different people look at different facts and also interpret them differently. Following are my chosen facts about Python programming language and my subjective opinion about these facts, the Python language, and its use for DevOps.

Background

As I do from time to time, I post comparisons between Next Generation Shell (NGS), a shell I’m working on, and other languages, Python included. The comparisons are typically involving some small task that I consider a “typical DevOps task” (defined later). As you can imagine, in these comparisons NGS always wins. First, because NGS was specifically designed for and is actually better for many DevOps tasks. Second, because there is no reason for me to post a comparison where NGS is not better. Anyhow…

After such post on LinkedIn (“Still using #Python for #DevOps? You like to suffer?”), where I compared sample NGS and Python code, a friend asked me why I “hate” Python. My answer was that I’m feeling neutral about Python.

I found the topic of my perception of Python interesting and deserving elaboration.

Python

From my perspective, Python is okayish language, relative to other programming languages.

While I understand people that are annoyed by Python 2 to Python 3 migration and breakage, I also realize that it’s very hard to get things right from the start (or even later).

Few things in Python from the top of my head that are annoying for me:

  • Python does not encourage nor supports well basic functional programming: map() and filter(). Due to Python’s syntax, the callback lambda can only be single expression.
  • Instead, it encourages list comprehensions which act as both map() and filter(). I don’t like list comprehensions, they don’t align with how I think. Dict comprehensions too.
  • Typing is afterthought and therefore “The Python runtime does not enforce function and variable type annotations. They can be used by third party tools such as type checkers, IDEs, linters, etc.”
  • Conditional expressions (A if C else B, where C is the condition) are breaking my head.
  • if __name__ == "__main__" hack idiom is ugly as fuck.

There are annoying things in any language though.

My strongest negative feeling about Python, for which I should mostly blame myself, is parameters handling. While working on NGS, early on, I copied was inspired by parameters handling in Python. The thought was that parameter handling is such a basic thing that (A) I shouldn’t spend much time on it and (B) such established language like Python would get it right. Python didn’t get it right. And I copied that atrocity. They ugly-fixed it later. I’m still thinking how to do it right. The “right” for NGS is likely to differ from the “right” for Python. Also, I would like to avoid making the same mistake for the second time.

What do I actually “hate” about Python?

What I “hate” about Python is surprisingly not Python-specific. It’s the use of square pegs for round holes:

Using Python, Ruby, Go, and other languages that were not intended for “typical DevOps tasks” (looking at resulting code for that judgement) when you have clearly better matching alternatives, including NGS and some other modern shells.

“Typical DevOps tasks” include running external programs and small scale data manipulation (files, structured data, mapping, filtering, pattern matching).

Justifying the Square Pegs

This one is annoying, especially hearing the same arguments for 1000th time.

“But I can do it with libraries”. You can. Also you can do it in Assembler, C or FORTRAN with or without libraries. I do not except the argument. The solutions are not equal.

“It’s not that much of a difference”. Depends on the use case. Run external program in Python and handle exit codes properly and let’s talk then.

More legitimate arguments include:

  • “Our team uses Python and so we do DevOps in Python too”
  • “We are afraid we will not find Stack Overflow answers if we are stuck”
  • “We are afraid we won’t be able to hire developers in this new language”
  • “Where is the tooling?”

In my mind, the legitimacy of the arguments above is in reverse proportion with DevOps work volume. The more “DevOps” tasks you do as percentage of your total work, the more advantages of better suited programming languages shine through.

While sometimes the arguments are justified, other times it’s inertia which is holding us back. Like switching away from JavaScript which is not going according to the plan.

Programming Languages in General

Subpar. All of them. At least the ones that I have seen (dozens). Python included. NGS included. We as humanity didn’t figure that out yet. I mean programming languages. We are stuck in paradigms which hold us back.

NGS aims to be less bad for DevOps than the rest because it was designed for the niche. Almost round peg for round hole, way better than the square pegs.

Conclusion

While it might look like I “hate” Python,

  • it’s actually all programming languages
  • and even more so their inappropriate use and the worst is
  • justifying and perpetuating inappropriate use of programming languages

Thanks for reading, have a nice ${TIME_OF_DAY} !


For the curious ones, code comparison follows.

The Code Comparison

During my quest to understand CloudFormation better, I stumbled upon this code:

"Gets the name of the folder with the handler code"

import json
import sys

def main(rpdk_path):
    "Get the source folder from rpdk config"

    with open(rpdk_path) as f:
        obj = json.load(f)
        entrypoint = obj["entrypoint"]
        print(entrypoint.split(".")[0])

if __name__ == "__main__":
    main(sys.argv[1])

To which I responded with NGS version (renaming mostly meaningless obj to conf on the way):


#!/usr/bin/env ngs

# Gets the name of the folder with the handler code

F main(rpdk_path) {
	conf = read(rpdk_path).decode_json()
	echo(conf.entrypoint.split(".")[0])
}

To which my friend responded with another Python version, which was shorter and did not have main().

Command Line Arguments

Both the original and the shorter version in Python require import sys which shows that dealing with command line arguments is not as “important” in Python as in NGS.

The gap is between additional import in Python and out-of-the-box automatic parsing of command line arguments and passing them to main() in NGS.

JSON

Both Python versions require import json, showing again that it’s easier to deal with JSON in NGS than in Python.

The example would be even shorter if the use case was slightly more typical – the JSON file would have .json extension. read(rpdk_path).decode_json() would become fetch(rpdk_path), leaving Python far behind.


Amazing! You stayed here till the end. Congrats and thanks!

Motivation to Keep Going on a Project

I have a friend that likes the feeling of progress in the professional area. He considers working on a project as part of his professional development. Following our discussion with regards to staying motivated on such a project, I would like to describe the factors that keep me going. Your factors are likely to be different but I would like to share mine as a starting point.

I hope that this article will help you, the reader, to pick and/or stay motivated on your own project. Note that you should first think hard about what you are trying to achieve and whether a project is the right solution to that.

My Project

The project I’m basing my self-observations on is Next Generation Shell, an open source shell which doesn’t ignore advances that happened outside of the terminal for last decades (do you feel the pain in these words?). I started working on the project in 2013. As of time of writing, which is 2024, I’m still on it.

The project has two main parts:

  • Programming language – it’s in a good shape and we use it at work
  • User Interface – early stage, too early to impress anyone

Motivation

I can not separate motivation from picking the project. In my case, all the original motivation behind starting the project is still relevant. It’s worth noting that my feeling is that this project chose me, not the other way around.

Roughly from more significant motivators to less significant ones:

  1. Pain. My project solves my own pain. I was programming in bash and Python. They both suck for DevOps scripting. Looked around for other languages and projects that would be aligned with my vision of the solution. Found none. Decided to do it myself. It was very clear from the beginning though that the amount of effort to solve my own pain doesn’t justify working on the project… if not for the next point:
  2. I like helping others to be more productive. Software is a very scalable way to do exactly that. While you can maybe generalize this as “the project is aligned with my values”, I tend to think about this as “the project is aligned with my intrinsic motivation”. While the programming language should make the user more productive, the UI should really take the productivity to the next level (some would categorize the UI as “groundbreaking”).
  3. The project became usable long time ago. Around 2016 I think. Seeing that it actually works for yourself is a great motivator. From GitHub issues that are opened from time to time, I assume that others are using NGS too. That means that NGS works for others too, at least to some extent.
  4. The project is mentally challenging but not too hard (ignoring speed optimizations here, which could be). When I started, I knew roughly how to do it. It touches two topics that I enjoy: programming language design and semantics.
  5. While I’m writing an NGS script I’m always thinking “how this can be more ergonomic?” and constantly improving the language. When I do the appropriate changes, it’s “NGS just became a bit more useful”. That contributes to the feeling of progress.
  6. While looking at any piece of code in any other language, it’s always a challenge: should NGS do it better? If yes, how? It’s very motivational to see that in many cases NGS does it better that the piece of code in front of me.
  7. Speaking of comparison, it does make me feel good that the niche is still not covered (amazing how bad the things still are) and therefore has huge potential for improvement of productivity. Looking at other projects that showcase anything but working with the cloud I’m thinking “Yep, we still need a shell to work with the cloud”.
  8. It’s something to show to a potential future employer.
  9. Not a near future plan but there is a potential for a startup here which would provide cloud-based enterprise features (I had a list of such features written down somewhere). If you can do that around a terminal, I guess you can do it around a shell.

Demotivation

Some things are demotivating when working on the project. Here they are and how I handle the ones that I know how (after “//”).

  1. “You don’t understand, bash is perfect, we don’t need another shell” comments. // There are people that just can’t be moved easily.
    • I’m sure some assembler programmers didn’t want to move to Fortran or COBOL or C. Some didn’t want to move from these to let’s say Java or Python. There is no more “right” motivation behind creating a programming language than dissatisfaction with existing ones.
    • With regards to UI, some people think that Command Line Interface which mostly limits the interaction to a single line is the pinnacle of UX today. Well, I disagree and I’m about to prove that we can do better. It’s actually partially proven since 1976, when Bill Joy released vi which used the whole screen for text editing. Now I just need to prove the same for the shell instead of text editing.
  2. UI is not ready yet and I can’t show it. I assume it’s way easier to sell productive UI than a programming language. // It shouldn’t be to far away from bare minimum demo though.
  3. Attracting users and developers is hard. I suspect few reasons:
    • It’s harder market now. When you have C and you release awk the amount of convincing is way less than when you have Python and Ruby and many other languages which are popular and kind-of do the job.
    • My personal skills in attracting people to a project are not great I assume // Need to team up with someone but again – need to find that person first
    • I’m prioritizing technical work above finding users. I guess that “solving my own pain” dominates here.
  4. The changes needed to be done to the language to make scripts at work more ergonomic (which I just like doing) come at the expense of working on the UI. It’s just really a hard tradeoff.

What motivates you? What demotivates you and how you handle that? Share in the comments.

Have a nice day!

Readable TypeScript debug() Output for Protobuf Messages

For search engine indexing and for understanding: Protobuf is frequently used with GRPC. Protobuf is how the requests and responses are serialized.

The Problem

Look at the output below. Printing protobuf messages using debug(‘%j’) produces unreadable output (the first one below).

$ DEBUG=myapp npx ts-node tojson.ts 

  myapp My object without toJSON {"wrappers_":null,"arrayIndexOffset_":-1,"array":[null,null,null,null,null,"Session name 01"],"pivot_":1.7976931348623157e+308,"convertedPrimitiveFields_":{}} +0ms

  myapp My object with toJSON {"id":"","CENSORED4":"","CENSORED3":"","version":"","name":"Session name 01","CENSORED1":"","status":0,"CENSORED2":""} +1ms

Solution

Here is all you need to make it work. SessionResponse below is a sample message (export class SessionResponse extends jspb.Message in another file, after import * as jspb from "google-protobuf";)

// --- Setup ---
import {Message} from "google-protobuf";

import {
    SessionResponse
} from "./PATH/TO/YOUR_GENERATED_PROTOBUF_FILE_pb";

import debug0 from 'debug';
const debug = debug0('myapp');

const req = new SessionResponse().setName("Session name 01");

// --- Test before ---
debug('My object without toJSON %j', req)

// --- Add this to your code --- start ---

declare module "google-protobuf" {
    interface Message {
        toJSON(): any;
    }
}
Message.prototype.toJSON = function () {
    return this.toObject();
}

// --- Add this to your code --- end ---

// --- Test after ---
debug('My object with toJSON %j', req)

Hope this helps. Let me know if you have questions.

Type Casting in TypeScript

( During code review, looking at TypeScript code. )

Colleague: … type casting …

Me: hold a sec, I don’t remember about anything called type casting in TypeScript

Colleague: you know, “as”

Me: yep, there is “as”, I just don’t think it was called type casting when I last re-read the official documentation a couple of months ago.

Colleague: hold a sec, here (shows me Google search results)

Search Results

My suspicion that something is off just grew when no result came from the official documentation.

TypeScript’s “as”

Clicked on few of this articles. They are talking about “as”. What does official TypeScript documentation tell us about “as”? It’s found under the heading Type Assertions. I didn’t see anything about type casting in the documentation.

Type Assertion vs Type Casting

Same thing, you are just showing off by being pedantic about naming!

Statistically probable response

What if I told you there is a practical difference between type assertion and type casting which confused my colleague?

Sometimes you will have information about the type of a value that TypeScript canโ€™t know about… Like a type annotation, type assertions are removed by the compiler and wonโ€™t affect the runtime behavior of your code.

Official TypeScript documentation

You are letting TS know which type the value is. This information is used at compile time. Nothing happens at runtime. “cast” to the wrong object and no exception is thrown. That’s type assertion in TS.

In other languages casting has runtime implications.

Reality Check 1

Googled “type assertion vs casting typescript”. First result is What are the difference between these type assertion or casting methods in TypeScript (Stack Overflow). Oops. People are using “casting” all over, even on Stack Overflow… except that one comment at the end of the page:

(myStr as string) isn’t a cast, it’s a type assertion. So you are telling the compiler that even though it doesn’t know for sure that myStr is a string you know better and it should believe you. It doesn’t actually affect the object itself.

Duncan, Apr 13, 2018 at 14:26

… which is countered by

Coming from C# and since Typescript has many C# features, I’d say it is

user2347763, Apr 13, 2018 at 15:17

I’ll have to disagree on the grounds that cast expression in C# “At run time, … might throw an exception.”

Reality Check 2

… whether the underlying data representation is converted from one representation into another, or a given representation is merely reinterpreted as the representation of another data type …

https://en.wikipedia.org/wiki/Type_conversion

So, apparently, type assertion is a specific case of type casting.

Conclusion

I was wrong thinking that “type assertion” is not “type casting”. Apparently, it is. It’s just a specific case. The phrase “In other languages casting has runtime implications.” (that I wrote above) is apparently not correct in the general sense despite Java and C# popping up immediately.

What I’m still having hard time to understand is why those articles choose to use the generic wording. I think we should always strive for precise wording.

If you want to be part of search results, fine, I get it. What’s preventing you from “In TypeScript, type casting is implemented using type assertions (a special case of type casting) that only have compile time implications” (or something similar) though?

Takeaways

  • Always check official documentation (apparently it needs to be said for the 1000th time)
  • If you are posting an article, I recommend sticking to the wording in official documentation, especially when it’s more precise.

Can we make this article more precise? Let me know ๐Ÿ™‚

Feedback Essentials

This is a concise post, summarizing Manager Tools podcasts about feedback. Examples are mine, I couldn’t remember specific examples.

Manager Tools recommendations are based on data about what’s effective gathered from companies from different industries and of different sizes.

Purpose of Feedback

The only purpose of feedback is to encourage productive behavior and to discourage unproductive behavior in the future.

Why Give Feedback?

As a manager, you are measured by results and retention. Results are achievable through behaviors. Your job as a manager is to adjust the behaviors, mostly through feedback, in order to improve results.

Prerequisites

For a feedback to be effective, there must be established relationship. In a professional context, the way to establish and maintain a relationship between a manager and their directs are weekly one-on-one meetings (which deserve a separate post). Without established relationship, feedback will most likely be ineffective. After having one-on-ones for some time, you can start with positive feedback, then add negative.

Be calm when giving feedback. If you are angry, wait until you are calm to give feedback.

Frequency

Overwhelming majority of directs want more feedback. Therefore, the feedback must be short. If it’s not short, it won’t be frequent.

Length

One feedback instance should take 30-90 seconds. (Don’t remember the exact numbers but something close to this).

Timing

The more adjacent in time the feedback is to the discussed behavior, the more effective it is.

Positive to Negative Ratio

Positive feedback is when you encourage productive behavior. Negative feedback is when you discourage unproductive behavior.

Ideally, for 10 positive pieces of feedback there would be one negative piece.

What’s Worthy of Feedback?

Pretty much any behavior, positive or negative.

Note that the amorphous “culture” is just a set of accepted behaviors. Maintaining “culture” can only be done through continuous feedback (by managers on all levels) and can not be done by producing the famous “our culture” style documents alone.

Delivery

Deliver in private. In order of decreasing effectiveness, as for any other communication:

  • Face to face
  • Video conference
  • Voice call

Procedure and Content

Giving feedback consists of the following steps.

Ask

Ask if the recipient is ready: “Can I give you some feedback?”

Do not proceed with feedback if the answer is no. In general, don’t ask questions when the response is irrelevant.

Describe the Situation

Example: “yesterday during the meeting”.

Describe the Behavior

Example: “you were talking over other people “

Describe the Effect

Example: “it made the discussion less effective” (TODO: better phrasing)

Ask to do More or Less

Example: “please don’t talk over other people in future meetings”

This part is left out if the person you are talking to is not under your management. You can’t ask of more or less of a particular behavior in this case. You operate under assumption that the person was not aware of the effect of their behavior and providing the insight can help. If you omit this part, the conversation is not called feedback (according to this model) anymore.

Positive Example

– Can I give you some feedback?

– Sure

– You had deliverable X. You had delivered it on time and the quality met the expectations. This unblocked team Y on time and we are on schedule to finish the whole project, which is important for the client. Please continue delivering on time and to the expected quality.

– Good. Thanks!

Notes:

  • The feedback is about specific behaviors.
  • Schedule and quality of the deliverables are also behaviors.

Common Mistakes

Discussing Attitudes

Do not discuss attitudes, only behaviors. “You have a shitty attitude” is too amorphous and will cause “no, I don’t”. That’s an argument that you can’t possibly win. You should only give feedback about behaviors.

Root Cause Analysis

Don’t try to do root cause analysis with the direct. You are not well equipped to analyze what’s going on inside a head of another person.

More generally, don’t slip into digging into the past. The focus of feedback is future behavior.

Accumulating

Don’t wait for several instances of behavior to occur before giving feedback. Common mistake is to wait “to make sure we have a real issue here”.

Ignoring “Normal” Behavior

The excuse “this is the expected behavior of a person in this role” is invalid. Do give positive feedback for any behavior you want to continue in the future.

Asymmetric Delivery

Making positive and negative feedback items delivery asymmetrical is a mistake. They should be delivered in an identical manner except for the last section when you ask to do more or less of the behavior.


I intend to update this post for better phrasing and possibly more content (still keeping it concise).

Ideally, each point should have a link to the original podcast for deeper understanding. Hope I’ll get to do this some day.

AWS SDK pagination – disregard for Developer Experience

According to Pagination using Async Iterators in modular AWS SDK for JavaScript, state of the art for pagination in AWS JS SDK is:

const client = new DynamoDBClient({}); 
const tableNames = [];
for await (const page of paginateListTables({ client }, {})) {
    // page contains a single paginated output.
    tableNames.push(...page.TableNames);
}

While having paginators is an improvement over previous atrocities like making the client code track the pagination state (through variously named next page fields of course), it’s still wrong.

Wouldn’t it make way more sense to have SDK where the developer doesn’t need to deal with pagination at all? Like this:

const client = new DynamoDBClient({}); 
const tableNames = [];
for await (const tableName of client.listTablesV2()) {
    tableNames.push(tableName);
}

… where pagination is hidden behind hypothetical listTablesV2(). Note that collecting into tableNames probably wouldn’t be necessary anymore, just do what you need with client.listTablesV2().

I’m thrilled to hear any justification for bothering developers with implementation detail such as pagination. Let me know.

If your justification is “… but that’s how API works” then you made a mental shortcut here. Why SDK should be one to one with API? Even AWS themselves think it shouldn’t. They even added the paginators abstraction. It’s just the wrong one.

If your justification is “there will be cases where you will want explicit control over pagination” – while it’s hard for me to imagine such use case, the solution would be to provide the low level API in addition.

If your justification involves how we got here and/or why it was easier for AWS to implement pagination this way – that’s not the justification I’m looking for. The answer to “daddy, what’s your job?” shouldn’t be “mostly working around AWS”.

Edit 2023-10-28: Clarification. In the proposed SDK, the pages should be fetched on demand so cost of API calls is not an argument for explicit pagination.

Nitpick: should probably be called tablesNames, not tableNames. These are names for different tables, not multiple names for the same table.

2023-10-30 Update (WIP, I’ll be editing)

The input for the update is the Reddit discussion.

The Reddit Discussion

The responses ranged from “I like the pagination” to “I think itโ€™s a fair point – perhaps in the evolution of the sdks, nobody raised your exact question (which I think is valid) …”

Chi Ma (u/chigia001) had an insightful perspective. I assume he spent quite some time thinking about pagination as he is a maintainer of an SDK that also has pagination.

Clarification

The original post doesn’t mention but the thought about the current low level API was: “it might need to be exposed too for some reason which is not currently apparent to me”.

What I Missed

I didn’t pay much attention that we are talking about AsyncIter, not the finest part of JavaScript particularly convenient feature of JavaScript. ( to be continued )

My Opinion – Unchanged Part

The basic premise of this blog post stays the same. My code should not have to deal with pagination when I don’t want to. It’s just unergonomic boilerplate. Same way I usually don’t care about lower level things such as TCP, TLS, and serialization, I don’t want to care about how the items I need are batched.

I propose to

  • Focus on finding better solutions rather than on justifying what we have.
  • Use dissatisfaction as a driver for progress.

Update 2023-11-25: boto3 (AWS SDK for Python) does have a way to iterate over collection of items where pagination is handled behind the scenes. From my perspective, it proves the basic premise.

My Opinion – Changed Parts

From 99% sure that I have obvious and straightforward solution (listTablesV2 above) it changed to

  • It’s a meh solution
    • AsyncIter is not very friendly and according to Chi Ma, not very well known.
  • There are other solutions
  • The low level API with pagination should be exposed
  • It’s a tradeoff
  • Exposing several similar APIs might be confusing and presents a challenge for SDK developers.
  • I don’t see a good solution. Maybe custom lazy list type. More thought should be invested in finding a better solution.

( to be continued )


Have a nice and productive day … to the extent possible of course ๐Ÿ™‚

Empty Arrays for Cleaner Code

Background

Let’s look at the following piece of code (in an abstract programming language) :

if chairs {
  for chair in chairs {
    clean(chair)
  }
}

The Problem

It’s more code that we would like to have. What’s holding us back? chairs can be null, which is not an array (aka list), therefore requiring the wrapping if. Without that if, for can fail with an error because it won’t be able to iterate over chairs (when it’s null).

Side note: any time a variable can be of different types (in our case chairs can be an array or null), there will be some code to deal with this situation. Try to avoid these situations.

Solution

Decide on a code convention that says that absence of items is represented by an empty array, not null. Your code will become more uniform and you will get rid of the if. Then the code above becomes:

for chair in chairs {
  clean(chair)
}

Please note that this solution is a tradeoff. If memory usage is very important in the application, this might not be a good solution.


Hope this helps. Have a nice day!

Update 2023-07-13

Real code example:

const tags = describeImagesCommandOutput.Images[0].Tags ?? [];
console.log('tags', tags);
const newTags = tags.filter(tag => tag.Key && TAGS_TO_COPY.has(tag.Key));
console.log('newTags', newTags);

My advice above would eliminate the ?? [] part.

Naming in Software – Practical Guide

The title of the post is the title of the book that I wanted to publish for quite some time now. While I was thinking about phrasing and gathering content, somebody else beat me to it with Naming Things: The Hardest Problem in Software Engineering. The main issue that I wanted to solve is now solved. Programmers don’t have an excuse for poor naming anymore.

In light of this event, I’ve decided to make small complementary post out of the materials that I have gathered and move on, focusing on Next Generation Shell.

Me and Naming

I have over 20 years of professional experience in programming. During that time, as many others, I’ve also noted the struggle when it comes to naming.

Here is a list of my accepted naming contributions to various projects.

  1. iterators – function shoes_in_my_size naming 2020-02-16, “The Rust Programming Language” book
  2. Constructors – Get_Contents() method is misnamed 2020-02-23, MS C++ Documentation
  3. Rename howMany() to countSelected() 2023-01, MDN
  4. nilJson naming issue in readme 2023-04, Otterize

Naming Things, the Book

I skimmed Tom’s book to understand how similar it was to what I was about to write. Quite close. If you are struggling with naming, go and read it.

There is some amount of fluff which I think my book would have less. Example: convincing people that naming is important while they are already reading the book.

Overall, I do recommend the book though.

Especially I recommend this book to AWS as an organization, I guess along with other books about code quality in general. AWS, your de-prioritization of code quality is staggering. I mean observable output here, not the stated “Insist on the Highest Standards”.

6.2.6 Use accurate parts of speech

Adding negative example from AWS:

https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/crpg-ref-responses.html

SUCCESS and FAILED are not the same part of speech.

7.2.3 Omit Metadata

Additional reason to exclude data type from the name is to avoid additional changes anywhere in the code except for the declaration.

elt in items

Is items a list here or a set? Probably not important, the code should work with either. On the other hand, changing data type from list to set in the following example will make the code incorrect:

elt in items_list

8.2.4 Use similar names for similar concepts

Adding negative example from AWS, which time after time fails to give consistent names across their APIs.

How do you limit number of results from an API? MaxResults, maxResults, MaxRecords, MaxItems, Limit, limit, … Details at AWS API pagination naming.

It looks like consistent naming is valued less than independence of teams and ability of teams to perform uncoordinated work.

8.2.5 Use consistent antonyms

Adding example.

When I’ve got to name Option type (represents a container that can hold a value or can be empty) in Next Generation Shell, I went with straightforward antonyms.

  • Box (super type)
  • EmptyBox
  • FullBox

Authors of other languages preferred other naming conventions:

Scala: Option, None, Some

Haskell: Maybe, Nothing, Just

Perspective

Information Loss

In my perspective, giving inadequate names is part of a larger issue – Information Loss. Each time you give a name, think which information is now in your head which will be helpful to the reader of the code. If you don’t phrase it concisely and precisely, information loss occurs between your head and the code you are working on. There are several common types of errors one can make:

  1. Don’t provide enough information. Causes the reader to investigate in order to recover the information.
  2. Provide wrong information. That’s the worst, it’s misleading the reader.
  3. Provide too much information. The reader then must sift through the information to get to the relevant parts.

API

Sometimes it’s useful to think of methods as an API. That’s why method names shouldn’t include implementation details (with rare exceptions when they are important to the caller). Think of methods’ names and parameters’ names as a short version of API specification.

Identifiers

Tom’s book deals with naming identifiers, such as functions, classes, variables, etc. One step before naming an identifier is the question whether there should be an identifier.

Avoid Naming

Sometimes, the additional cognitive load is not worth it.

# Bad
chairs = fetch_chairs()
sorted_chairs = chairs.sort()
# also, now have to use the longer identifier in the code below

# Good
chairs = fetch_chairs().sort()

Apply your judgement of course. If it’s a 20 step process, additional identifiers in the middle do contribute to understanding. You still probably don’t want an identifier for each and every step of the calculation.

Do Name – Magic Numbers

Avoid magic numbers through naming. Please ignore whether result is a good name ๐Ÿ™‚

# Bad
if result == 126 { ... }

# Good
NOT_EXECUTABLE = 126; # Or better, part of an Enum

if result == NOT_EXECUTABLE { ... }

Do Name – Repetitive Code

If you notice code that repeats, with rare exceptions, you should refactor your code extracting that code to a function or a method with a name.

Several Identifiers

Sometimes a function, a method, or a class do several things. In this case, you might struggle to name it. In a perfect world, the solution to this is refactoring to appropriate pieces.

Test Your Naming

You just named something: a function, a method or a class. Is there a change around the code that would make the name wrong? What if you copy+paste the named piece of code to another project? Would you need to change the name?

# Bad
function start_yellow_cars(cars) { ... } # The function doesn't know or care about the color
yellow_cars = ...
start_yellow_cars(yellow_cars)

# The change that would highlight the wrong naming
# while keeping the code completely functional
function start_yellow_cars(cars) { ... }
my_cars = ...
start_yellow_cars(my_cars)


# Good
function start_cars(cars) { ... }
yellow_cars = ...
start_cars(yellow_cars)

Common Naming Mistakes Observed

  1. Naming a data structure with “JSON” in name.
  2. Argument vs Parameter

Tooling

I highly recommend using IDEs that “understand” the code enough to be able to refactor/rename (classes, methods, functions, parameters) as opposed to text editors which can not assist with renaming to the same extent.


Hope this helps. Happy naming!

gRPC + REST API on AWS

Showing a setup of gRPC service which is also exposed as a REST API. It’s a setup that happens to work for us. No alternatives will be discussed in this post.

This is a concise blog post.

Architecture

  1. ALB with HTTPS listener (trivially configured, out of scope of this post)
  2. ECS running a task with 3 containers:
    • API Gateway. Implemented by Envoy. does:
      • requests authorization using the service in next container
      • proxies gRPC requests
      • proxies REST requests (converting them to upstream gRPC requests).
    • authorization service implemented with OPA
    • Our gRPC application

Notes

Health checks are not in very good shape yet

ECS Configuration (Simplified Excerpt)

In case the reader is not familiar, it CloudFormation below.

  TaskDefinition:
    Type: AWS::ECS::TaskDefinition
    Properties:
      ContainerDefinitions:
        - Name: apigw
          Image: !Ref ApiGwImage
          PortMappings:
            - ContainerPort: !Ref ContainerPort
        - Name: opa
          Image: !Ref OpaImage
          PortMappings:
            - ContainerPort: 9191
        - Name: app
          Image: !Ref AppImage
          PortMappings:
            - ContainerPort: 4000

  Service:
    DependsOn:
      - GrpcListenerRule
      - RestListenerRule
      - GrpcTargetGroup
      - RestTargetGroup
    Type: AWS::ECS::Service
    Properties:
      ServiceName: !Ref ServiceName
      Cluster: !Ref Cluster
      TaskDefinition: !Ref TaskDefinition
      LoadBalancers:
        - ContainerName: apigw
          ContainerPort: !Ref ContainerPort
          TargetGroupArn: !Ref GrpcTargetGroup
        - ContainerName: apigw
          ContainerPort: !Ref ContainerPort
          TargetGroupArn: !Ref RestTargetGroup

  GrpcTargetGroup:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties:
      HealthCheckIntervalSeconds: 10
      HealthCheckPath: /
      HealthCheckTimeoutSeconds: 5
      Matcher:
        GrpcCode: "0-99"
      UnhealthyThresholdCount: 2
      HealthyThresholdCount: 2
      Port: !Ref ContainerPort
      Protocol: HTTP
      ProtocolVersion: GRPC
      TargetGroupAttributes:
        - Key: deregistration_delay.timeout_seconds
          Value: 60 # default is 300
      TargetType: ip
      VpcId: !ImportValue VpcId

  RestTargetGroup:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties:
      HealthCheckIntervalSeconds: 10
      HealthCheckPath: /rest/not-found
      HealthCheckTimeoutSeconds: 5
      Matcher:
        HttpCode: 404
      UnhealthyThresholdCount: 2
      HealthyThresholdCount: 2
      Port: !Ref ContainerPort
      Protocol: HTTP
      ProtocolVersion: HTTP1
      TargetGroupAttributes:
        - Key: deregistration_delay.timeout_seconds
          Value: 60 # default is 300
      TargetType: ip
      VpcId: !ImportValue VpcId

  GrpcListenerRule:
    Type: AWS::ElasticLoadBalancingV2::ListenerRule
    Properties:
      Actions:
        - Type: forward
          TargetGroupArn: !Ref GrpcTargetGroup
      Conditions:
        - Field: path-pattern
          PathPatternConfig:
            Values:
              - '/censored.v1.CensoredService/*'
              - '/censored.v1.CensoredAdminService/*'
              - '/censored.v1.CensoredSystemService/*'
      ListenerArn: ...
      Priority: 1000

  RestListenerRule:
    Type: AWS::ElasticLoadBalancingV2::ListenerRule
    Properties:
      Actions:
        - Type: forward
          TargetGroupArn: !Ref RestTargetGroup
      Conditions:
        - Field: path-pattern
          PathPatternConfig:
            Values:
              - '/rest/v1/*'
      ListenerArn: ...
      Priority: 1001

Envoy Configuration (Simplified Excerpt)

static_resources:
  listeners:
    - address:
        socket_address:
          address: 0.0.0.0
          port_value: 8000
      filter_chains:
        - filters:
            - name: Connection Manager
              typed_config:
                "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
                via: CensoredGW
                route_config:
                  name: Static response for tests
                  virtual_hosts:
                    - name: backend
                      domains:
                        - "*"
                      routes:
                        - match:
                            prefix: "/test/static"
                          direct_response:
                            status: 200
                            body:
                              inline_string: "Static response for tests"
                        # Reference: https://envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/grpc_json_transcoder_filter#route-configs-for-transcoded-requests
                        - match:
                            prefix: "/"
                          route:
                            cluster: upstream
                            timeout: 60s
                http_filters:
                  - name: envoy.filters.http.grpc_json_transcoder
                    typed_config:
                      "@type": type.googleapis.com/envoy.extensions.filters.http.grpc_json_transcoder.v3.GrpcJsonTranscoder
                      # maybe disable later:
                      auto_mapping: true
                      proto_descriptor: "../path/to/proto_descriptor.bin" ### See next heading in this post
                      services:
                        - censored.v1.CensoredService
                        - censored.v1.CensoredAdminService
                        - censored.v1.CensoredSystemService
                      print_options:
                        add_whitespace: true
                        always_print_primitive_fields: true
                      request_validation_options:
                        reject_unknown_method: true
                        reject_unknown_query_parameters: true
                  - name: envoy.filters.http.cors
                    typed_config:
                      "@type": type.googleapis.com/envoy.extensions.filters.http.cors.v3.Cors
                  - name: envoy.ext_authz
                    typed_config:
                      "@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthz
                      failure_mode_allow: false
                      with_request_body:
                        max_request_bytes: 10485760 # 10M
                        allow_partial_message: false
                        pack_as_bytes: true
                      transport_api_version: V3
                      grpc_service:
                        envoy_grpc:
                          cluster_name: opa-agent
                        timeout: 10s
                  - name: envoy.filters.http.router
                    # https://github.com/envoyproxy/envoy/issues/21464
                    typed_config:
                      "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
                always_set_request_id_in_response: true
                access_log:
                  - typed_config:
                      "@type": type.googleapis.com/envoy.extensions.access_loggers.stream.v3.StdoutAccessLog
                      # https://www.envoyproxy.io/docs/envoy/latest/configuration/observability/access_log/usage#config-access-log-default-format

  # Based on https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/grpc_json_transcoder_filter
  clusters:
    - name: opa-agent
      connect_timeout: 0.25s
      type: STRICT_DNS
      typed_extension_protocol_options:
        envoy.extensions.upstreams.http.v3.HttpProtocolOptions:
          "@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions
          explicit_http_config:
            http2_protocol_options: { }
      lb_policy: ROUND_ROBIN
      load_assignment:
        cluster_name: service
        endpoints:
          - lb_endpoints:
              - endpoint:
                  address:
                    socket_address:
                      address: 127.0.0.1
                      port_value: 9191
    - name: upstream
      type: STRICT_DNS
      typed_extension_protocol_options:
        envoy.extensions.upstreams.http.v3.HttpProtocolOptions:
          "@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions
          explicit_http_config:
            http2_protocol_options: {}
      load_assignment:
        cluster_name: grpc
        endpoints:
          - lb_endpoints:
              - endpoint:
                  address:
                    socket_address:
                      address: 127.0.0.1
                      port_value: 4000

proto_descriptor.bin

GrpcJsonTranscoder must have the proto descriptor file in order to know how to transcode. The file contains:

  1. proto definitions of your services, including extension that describes how to expose the services as REST
  2. dependencies of the above proto definitions

The descriptor file is generated using a command similar to the following:

buf build -o proto_descriptor.bin --as-file-descriptor-set --path path/to/my.proto

buf is a way to manage .proto files and their dependencies (very imprecise definition, sorry)

If I remember correctly, you can generate the descriptor with protoc (without buf) but I don’t remember how.

grpcurl

Same descriptor file is used with grpcurl when you later test your service from the command line:

grpcurl -H "Authorization: Bearer ..." -protoset proto_descriptor.bin "example.com:443" censored.service.name/MyFunc

my.proto

This is how a protobuf definition with REST extension looks like (excerpt):

import "google/api/annotations.proto";

service Censored {
  rpc MyCreate(CreateRequest) returns (CreateResponse){
    option (google.api.http) = { post: "/rest/v1/my-objs" };
  }
  rpc MyGet(GetRequest) returns (GetResponse) {
    option (google.api.http) = { get: "/rest/v1/my-objs/{id}" };
  }
}

Excerpt from buf.yaml corresponding to the import above:

version: v1

deps:
  - buf.build/googleapis/googleapis


Hope this helps.

Sorry, I was in a rush to get this out. If anything is unclear or missing, please let me know.