The Daily WTF
Error'd: Nicknamed Nil
Michael R. is back with receipts. "I have been going to Tayyabs for >20 years. In the past they only accepted cash tips. Good to see they are testing a new way now."
An anonymous murmers of Outlook 365: "I appreciate being explicit about the timezone for the appointments, but I am wondering how those \" got there. (And the calender in german should start on Monday not Sunday)"
"Only my friends call me {0}," complains Alejandro D. "But wait! I haven't logged in yet, how does DHL know my name?"
"Prices per square foot are through the roof," puns Mr. TA "In fact, I'm guessing 298 sq ft is the area of the kitchen cabinets alone." The price isn't so bad, it's the condo fees that will kill you.
TheRealSteveJudge writes "Have a look at the cheapest ticket price which is available for a ride of 45 km from Duisburg to Xanten -- Günstiger Ticketpreis in German. That's really affordable!" If you've just purchased a 298 ft^2 condo at the Ritz.
[Advertisement] Utilize BuildMaster to release your software with confidence, at the pace your business demands. Download today!
CodeSOD: Just a Few Updates
Misha has a co-worker who has unusual ideas about how database performance works. This co-worker, Ted, has a vague understanding that a SQL query optimizer will attempt to find the best execution path for a given query. Unfortunately, Ted has just enough knowledge to be dangerous; he believes that the job of a developer is to write SQL queries that will "trick" the optimizer into doing an even better job, somehow.
This means that Ted loves subqueries.
For example, let's say you had a table called tbl_updater, which is used to store pending changes for a batch operation that will later get applied. Each change in updater has a unique change key that identifies it. For reasons best not looked into too deeply, at some point in the lifecycle of a record in this table, the application needs to null out several key fields based on the change value.
If you or I were writing this, we might do something like this:
update tbl_updater set id = null, date = null, location = null, type = null, type_id = null where change = @changeAnd this is how you know that you and I are fools, because we didn't use a single subquery.
update tbl_updater set id = null where updater in (select updater from tbl_updater where change = @change) update tbl_updater set date = null where updater in (select updater from tbl_updater where change = @change) update tbl_updater set location = null where updater in (select updater from tbl_updater where change = @change) update tbl_updater set type = null where updater in (select updater from tbl_updater where change = @change) update tbl_updater set date = null where updater in (select updater from tbl_updater where change = @change) update tbl_updater set type_id = null where updater in (select updater from tbl_updater where change = @change)So here, Ted uses where updater in (subquery) which is certainly annoying and awkward, given that we know that change is a unique key. Maybe Ted didn't know that? Of course, one of the great powers of relational databases is that they offer data dictionaries so you can review the structure of tables before writing queries, so it's very easy to find out that the key is unique.
But that simple ignorance doesn't explain why Ted broke it out into multiple updates. If insanity is doing the same thing again and again expecting different results, what does it mean when you actually do get different results but also could have just done all this once?
Misha asked Ted why he took this approach. "It's faster," he replied. When Misha showed benchmarks that proved it emphatically wasn't faster, he just shook his head. "It's still faster this way."
Faster than what? Misha wondered.
[Advertisement] Picking up NuGet is easy. Getting good at it takes time. Download our guide to learn the best practice of NuGet for the Enterprise.Representative Line: National Exclamations
Carlos and Claire found themselves supporting a 3rd party logistics package, called IniFreight. Like most "enterprise" software, it was expensive, unreliable, and incredibly complicated. It had also been owned by four different companies during the time Carlos had supported it, as its various owners underwent a series of acquisitions. It kept them busy, which is better than being bored.
One day, Claire asked Carlos, "In SQL, what does an exclamation point mean?"
"Like, as a negation? I don't think most SQL dialects support that."
"No, like-" and Claire showed him the query.
select * from valuation where origin_country < '!'"IniFreight, I presume?" Carlos asked.
"Yeah. I assume this means, 'where origin country isn't blank?' But why not just check for NOT NULL?"
The why was easy to answer: origin_country had a constraint which prohibited nulls. But the input field didn't do a trim, so the field did allow whitespace only strings. The ! is the first printable, non-whitespace character in ASCII (which is what their database was using, because it was built before "support wide character sets" was a common desire).
Unfortunately, this means that my micronation, which is simply spelled with the ASCII character 0x07 will never show up in their database. You might not think you're familiar with my country, but trust me- it'll ring a bell.
[Advertisement] Utilize BuildMaster to release your software with confidence, at the pace your business demands. Download today!CodeSOD: Born Single
Alistair sends us a pretty big blob of code, but it's a blob which touches upon everyone's favorite design pattern: the singleton. It's a lot of Java code, so we're going to take this as chunks. Let's start with the two methods responsible for constructing the object.
The purpose of this code is to parse an XML file, and construct a mapping from a "name" field in the XML to a "batch descriptor".
/** * Instantiates a new batch manager. */ private BatchManager() { try { final XMLReader xmlReader = XMLReaderFactory.createXMLReader(); xmlReader.setContentHandler(this); xmlReader.parse(new InputSource(this.getClass().getClassLoader().getResourceAsStream("templates/" + DOCUMENT))); } catch (final Exception e) { logger.error("Error parsing Batch XML.", e); } } /* * (non-Javadoc) * * @see nz.this.is.absolute.crap.sax.XMLEntity#initChild(java.lang.String, * java.lang.String, java.lang.String, org.xml.sax.Attributes) */ @Override protected ContentHandler initChild(String uri, String localName, String qName, Attributes attributes) throws SAXException { final BatchDescriptor batchDescriptor = new BatchDescriptor(); // put it in the map batchMap.put(attributes.getValue("name"), batchDescriptor); return batchDescriptor; }Here we see a private constructor, which is reasonable for a singleton. It creates a SAX based reader. SAX is event driven- instead of loading the whole document into a DOM, it emits an event as it encounters each new key element in the XML document. It's cumbersome to use, but far more memory efficient, and I'd hardly say this.is.absolute.crap, but whatever.
This code is perfectly reasonable. But do you know what's unreasonable? There's a lot more code, and these are the only things not marked as static. So let's keep going.
// singleton instance so that static batch map can be initialised using // xml /** The Constant singleton. */ @SuppressWarnings("unused") private static final Object singleton = new BatchManager();Wait… why is the singleton object throwing warnings about being unused? And wait a second, what is that comment saying, "so the static batch map can be initalalised"? I saw a batchMap up in the initChild method above, but it can't be…
private static Map<String, BatchDescriptor> batchMap = new HashMap<String, BatchDescriptor>();Oh. Oh no.
/** * Gets the. * * @param batchName * the batch name * * @return the batch descriptor */ public static BatchDescriptor get(String batchName) { return batchMap.get(batchName); } /** * Gets the post to selector name. * * @param batchName * the batch name * * @return the post to selector name */ public static String getPostToSelectorName(String batchName) { final BatchDescriptor batchDescriptor = batchMap.get(batchName); if (batchDescriptor == null) { return null; } return batchDescriptor.getPostTo(); }There are more methods, and I'll share the whole code at the end, but this gives us a taste. Here's what this code is actually doing.
It creates a static Map. static, in this context, means that this instance is shared across all instances of BatchManager.They also create a static instance of BatchManager inside of itself. The constructor of that instance then executes, populating that static Map. Now, when anyone invokes BatchManager.get it will use that static Map to resolve that.
This certainly works, and it offers a certain degree of cleanness in its implementation. A more conventional singleton would have the Map being owned by an instance, and it's just using the singleton convention to ensure there's only a single instance. This version's calling convention is certainly nicer than doing something like BatchManager.getInstance().get(…), but there's just something unholy about this that sticks into me.
I can't say for certain if it's because I just hate Singletons, or if it's this specific abuse of constructors and static members.
This is certainly one of the cases of misusing a singleton- it does not represent something there can be only one of, it's ensuring that an expensive computation is only allowed to be done once. There are better ways to handle that lifecycle. This approach also forces that expensive operation to happen at application startup, instead of being something flexible that can be evaluated lazily. It's not wrong to do this eagerly, but building something that can only do it eagerly is a mistake.
In any case, the full code submission follows:
package nz.this.is.absolute.crap.server.template; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.ResourceBundle; import nz.this.is.absolute.crap.KupengaException; import nz.this.is.absolute.crap.SafeComparator; import nz.this.is.absolute.crap.sax.XMLEntity; import nz.this.is.absolute.crap.selector.Selector; import nz.this.is.absolute.crap.selector.SelectorItem; import nz.this.is.absolute.crap.server.BatchValidator; import nz.this.is.absolute.crap.server.Validatable; import nz.this.is.absolute.crap.server.ValidationException; import nz.this.is.absolute.crap.server.business.BusinessObject; import nz.this.is.absolute.crap.server.database.EntityHandler; import nz.this.is.absolute.crap.server.database.SQLEntityHandler; import org.apache.log4j.Logger; import org.xml.sax.Attributes; import org.xml.sax.ContentHandler; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import org.xml.sax.XMLReader; import org.xml.sax.helpers.XMLReaderFactory; /** * The Class BatchManager. */ public class BatchManager extends XMLEntity { private static final Logger logger = Logger.getLogger(BatchManager.class); /** The Constant DOCUMENT. */ private final static String DOCUMENT = "Batches.xml"; /** * The Class BatchDescriptor. */ public class BatchDescriptor extends XMLEntity { /** The batchSelectors. */ private final Collection<String> batchSelectors = new ArrayList<String>(); /** The dependentCollections. */ private final Collection<String> dependentCollections = new ArrayList<String>(); /** The directSelectors. */ private final Collection<String> directSelectors = new ArrayList<String>(); /** The postTo. */ private String postTo; /** The properties. */ private final Collection<String> properties = new ArrayList<String>(); /** * Gets the batch selectors iterator. * * @return the batch selectors iterator */ public Iterator<String> getBatchSelectorsIterator() { return this.batchSelectors.iterator(); } /** * Gets the dependent collections iterator. * * @return the dependent collections iterator */ public Iterator<String> getDependentCollectionsIterator() { return this.dependentCollections.iterator(); } /** * Gets the post to. * * @return the post to */ public String getPostTo() { return this.postTo; } /** * Gets the post to business object. * * @param businessObject * the business object * @param postHandler * the post handler * * @return the post to business object * * @throws ValidationException * the validation exception */ private BusinessObject getPostToBusinessObject( BusinessObject businessObject, EntityHandler postHandler) throws ValidationException { if (this.postTo == null) { return null; } final BusinessObject postToBusinessObject = businessObject .getBusinessObjectFromMap(this.postTo, postHandler); // copy properties for (final String propertyName : this.properties) { String postToPropertyName; if ("postToStatus".equals(propertyName)) { // status field on batch entity refers to the batch entity // itself // so postToStatus is used for updating the status property // of the postToBusinessObject itself postToPropertyName = "status"; } else { postToPropertyName = propertyName; } final SelectorItem destinationItem = postToBusinessObject .find(postToPropertyName); if (destinationItem != null) { final Object oldValue = destinationItem.getValue(); final Object newValue = businessObject.get(propertyName); if (SafeComparator.areDifferent(oldValue, newValue)) { destinationItem.setValue(newValue); } } } // copy direct selectors for (final String selectorName : this.directSelectors) { final SelectorItem destinationItem = postToBusinessObject .find(selectorName); if (destinationItem != null) { // get the old and new values for the selectors Selector oldSelector = (Selector) destinationItem .getValue(); Selector newSelector = (Selector) businessObject .get(selectorName); // strip them down to bare identifiers for comparison if (oldSelector != null) { oldSelector = oldSelector.getAsIdentifier(); } if (newSelector != null) { newSelector = newSelector.getAsIdentifier(); } // if they're different then update if (SafeComparator.areDifferent(oldSelector, newSelector)) { destinationItem.setValue(newSelector); } } } // copy batch selectors for (final String batchSelectorName : this.batchSelectors) { final Selector batchSelector = (Selector) businessObject .get(batchSelectorName); if (batchSelector == null) { throw new ValidationException( "\"PostTo\" selector missing."); } final BusinessObject batchObject = postHandler .find(batchSelector); if (batchObject != null) { // get the postTo selector for the batch object we depend on final BatchDescriptor batchDescriptor = batchMap .get(batchObject.getName()); if (batchDescriptor.postTo != null && postToBusinessObject .containsKey(batchDescriptor.postTo)) { final Selector realSelector = batchObject .getBusinessObjectFromMap( batchDescriptor.postTo, postHandler); postToBusinessObject.put(batchDescriptor.postTo, realSelector); } } } businessObject.put(this.postTo, postToBusinessObject); return postToBusinessObject; } /* * (non-Javadoc) * * @see * nz.this.is.absolute.crap.sax.XMLEntity#initChild(java.lang.String, * java.lang.String, java.lang.String, org.xml.sax.Attributes) */ @Override protected ContentHandler initChild(String uri, String localName, String qName, Attributes attributes) throws SAXException { if ("Properties".equals(qName)) { return new XMLEntity() { @Override protected ContentHandler initChild(String uri, String localName, String qName, Attributes attributes) throws SAXException { BatchDescriptor.this.properties.add(attributes .getValue("name")); return null; } }; } else if ("DirectSelectors".equals(qName)) { return new XMLEntity() { @Override protected ContentHandler initChild(String uri, String localName, String qName, Attributes attributes) throws SAXException { BatchDescriptor.this.directSelectors.add(attributes .getValue("name")); return null; } }; } else if ("BatchSelectors".equals(qName)) { return new XMLEntity() { @Override protected ContentHandler initChild(String uri, String localName, String qName, Attributes attributes) throws SAXException { BatchDescriptor.this.batchSelectors.add(attributes .getValue("name")); return null; } }; } else if ("PostTo".equals(qName)) { return new XMLEntity() { @Override protected ContentHandler initChild(String uri, String localName, String qName, Attributes attributes) throws SAXException { BatchDescriptor.this.postTo = attributes .getValue("name"); return null; } }; } else if ("DependentCollections".equals(qName)) { return new XMLEntity() { @Override protected ContentHandler initChild(String uri, String localName, String qName, Attributes attributes) throws SAXException { BatchDescriptor.this.dependentCollections .add(attributes.getValue("name")); return null; } }; } return null; } } /** The batchMap. */ private static Map<String, BatchDescriptor> batchMap = new HashMap<String, BatchDescriptor>(); /** * Gets the. * * @param batchName * the batch name * * @return the batch descriptor */ public static BatchDescriptor get(String batchName) { return batchMap.get(batchName); } /** * Gets the post to selector name. * * @param batchName * the batch name * * @return the post to selector name */ public static String getPostToSelectorName(String batchName) { final BatchDescriptor batchDescriptor = batchMap.get(batchName); if (batchDescriptor == null) { return null; } return batchDescriptor.getPostTo(); } // singleton instance so that static batch map can be initialised using // xml /** The Constant singleton. */ @SuppressWarnings("unused") private static final Object singleton = new BatchManager(); /** * Post. * * @param businessObject * the business object * * @throws Exception * the exception */ public static void post(BusinessObject businessObject) throws Exception { // validate the batch root object only - it can validate the rest if it // needs to if (businessObject instanceof Validatable) { if (!BatchValidator.validate(businessObject)) { logger.warn(String.format("Validating %s failed", businessObject.getClass().getSimpleName())); throw new ValidationException( "Batch did not validate - it was not posted"); } ((Validatable) businessObject).validator().prepareToPost(); } final SQLEntityHandler postHandler = new SQLEntityHandler(true); final Iterator<BusinessObject> batchIterator = new BatchIterator( businessObject, null, postHandler); // iterate through batch again posting each object try { while (batchIterator.hasNext()) { post(batchIterator.next(), postHandler); } postHandler.commit(); } catch (final Exception e) { logger.error("Exception occurred while posting batches", e); // something went wrong postHandler.rollback(); throw e; } return; } /** * Post. * * @param businessObject * the business object * @param postHandler * the post handler * * @throws KupengaException * the kupenga exception */ private static void post(BusinessObject businessObject, EntityHandler postHandler) throws KupengaException { if (businessObject == null) { return; } if (Boolean.TRUE.equals(businessObject.get("posted"))) { return; } final BatchDescriptor batchDescriptor = batchMap.get(businessObject .getName()); final BusinessObject postToBusinessObject = batchDescriptor .getPostToBusinessObject(businessObject, postHandler); if (postToBusinessObject != null) { postToBusinessObject.save(postHandler); } businessObject.setItemValue("posted", Boolean.TRUE); businessObject.save(postHandler); } /** * Instantiates a new batch manager. */ private BatchManager() { try { final XMLReader xmlReader = XMLReaderFactory.createXMLReader(); xmlReader.setContentHandler(this); xmlReader.parse(new InputSource(this.getClass().getClassLoader().getResourceAsStream("templates/" + DOCUMENT))); } catch (final Exception e) { logger.error("Error parsing Batch XML.", e); } } /* * (non-Javadoc) * * @see nz.this.is.absolute.crap.sax.XMLEntity#initChild(java.lang.String, * java.lang.String, java.lang.String, org.xml.sax.Attributes) */ @Override protected ContentHandler initChild(String uri, String localName, String qName, Attributes attributes) throws SAXException { final BatchDescriptor batchDescriptor = new BatchDescriptor(); // put it in the map batchMap.put(attributes.getValue("name"), batchDescriptor); return batchDescriptor; } } .comment { border: none; } [Advertisement] Keep all your packages and Docker containers in one place, scan for vulnerabilities, and control who can access different feeds. ProGet installs in minutes and has a powerful free version with a lot of great features that you can upgrade when ready.Learn more.CodeSOD: Back Up for a Moment
James's team has a pretty complicated deployment process implemented as a series of bash scripts. The deployment is complicated, the scripts doing the deployment are complicated, and failures mid-deployment are common. That means they need to gracefully roll back, and they way they do that is by making backup copies of the modified files.
This is how they do that.
DATE=`date '+%Y%m%d'` BACKUPDIR=`dirname ${DESTINATION}`/backup if [ ! -d $BACKUPDIR ] then echo "Creating backup directory ..." mkdir -p $BACKUPDIR fi FILENAME=`basename ${DESTINATION}` BACKUPFILETYPE=${BACKUPDIR}/${FILENAME}.${DATE} BACKUPFILE=${BACKUPFILETYPE}-1 if [ -f ${BACKUPFILE} ] || [ -f ${BACKUPFILE}.gz ] ; then BACKUPFILE=${BACKUPFILETYPE}-2 ; fi if [ -f ${BACKUPFILE} ] || [ -f ${BACKUPFILE}.gz ] ; then BACKUPFILE=${BACKUPFILETYPE}-3 ; fi if [ -f ${BACKUPFILE} ] || [ -f ${BACKUPFILE}.gz ] ; then BACKUPFILE=${BACKUPFILETYPE}-4 ; fi if [ -f ${BACKUPFILE} ] || [ -f ${BACKUPFILE}.gz ] ; then BACKUPFILE=${BACKUPFILETYPE}-5 ; fi if [ -f ${BACKUPFILE} ] || [ -f ${BACKUPFILE}.gz ] ; then BACKUPFILE=${BACKUPFILETYPE}-6 ; fi if [ -f ${BACKUPFILE} ] || [ -f ${BACKUPFILE}.gz ] ; then BACKUPFILE=${BACKUPFILETYPE}-7 ; fi if [ -f ${BACKUPFILE} ] || [ -f ${BACKUPFILE}.gz ] ; then BACKUPFILE=${BACKUPFILETYPE}-8 ; fi if [ -f ${BACKUPFILE} ] || [ -f ${BACKUPFILE}.gz ] ; then BACKUPFILE=${BACKUPFILETYPE}-9 ; fi if [ -f ${BACKUPFILE} ] || [ -f ${BACKUPFILE}.gz ] ; then cat <<EOF You have already had 9 rates releases in one day. ${BACKUPFILE} already exists, do it manually !!! EOF exit 2 fiLook, I know that loops in bash can be annoying, but they're not that annoying.
This code creates a backup directory (if it doesn't already exist), and then creates a file name for the file we're about to backup, in the form OriginalName.Ymd-n.gz. It tests to see if this file exists, and if it does, it increments n by one. It does this until either it finds a file name that doesn't exist, or it hits 9, at which point it gives you a delightfully passive aggressive message:
You have already had 9 rates releases in one day. ${BACKUPFILE} already exists, do it manually !!!
Yeah, do it manually. Now, admittedly, I don't think a lot of folks want to do more than 9 releases in a given day, but there's no reason why they couldn't just keep trying until they find a good filename. Or even better, require each release to have an identifier (like the commit or build number or whatever) and then use that for the filenames.
Of course, just fixing this copy doesn't address the real WTF, because we laid out the real WTF in the first paragraph: deployment is a series of complicated bash scripts doing complicated steps that can fail all the time. I've worked in places like that, and it's always a nightmare. There are better tools! Our very own Alex has his product, of course, but there are a million ways to get your builds repeatable and reliable that don't involve BuildMaster but also don't involve fragile scripts. Please, please use one of those.
[Advertisement] Plan Your .NET 9 Migration with ConfidenceYour journey to .NET 9 is more than just one decision.Avoid migration migraines with the advice in this free guide. Download Free Guide Now!
Error'd: Another One Rides the Bus
"Toledo is on Earth, Adrian must be on Venus," remarks Russell M. , explaining "This one's from weather.gov. Note that Adrian is 28 million miles away from Toledo. Being raised in Toledo, Michigan did feel like another world sometimes, but this is something else." Even Toledo itself is a good bit distant from Toledo. Definitely a long walk.
"TDSTF", reports regular Michael R. from London, well distant from Toledo OH and Toledo ES.
Also on the bus, astounded Ivan muses "It's been a long while since I've seen a computer embedded in a piece of public infrastructure (here: a bus payment terminal) literally snow crash. They are usually better at listening to Reason..."
From Warsaw, Jaroslaw time travels twice. First with this entry "Busses at the bus terminus often display time left till departure, on the front display and on the screens inside. So one day I entered the bus - front display stating "Departure in 5 minutes". Inside I saw this (upper image)... After two minutes the numbers changed to the ones on the lower image. I'm pretty sure I was not sitting there for six hours..."
And again with an entry we dug out of the way back bin while I was looking for more bus-related items. Was it a total concidence this bus bit also came from Jaroslaw? who just wanted to know "Is bus sharing virtualised that much?" I won't apologize, any kind of bus will do when we're searching hard to match a theme.
[Advertisement] ProGet’s got you covered with security and access controls on your NuGet feeds. Learn more.
The Middle(ware) Child
Once upon a time, there was a bank whose business relied on a mainframe. As the decades passed and the 21st century dawned, the bank's bigwigs realized they had to upgrade their frontline systems to applications built in Java and .NET, but—for myriad reasons that boiled down to cost, fear, and stubbornness—they didn't want to migrate away from the mainframe entirely. They also didn't want the new frontline systems to talk directly to the mainframe or vice-versa. So they tasked old-timer Edgar with writing some middleware. Edgar's brainchild was a Windows service that took care of receiving frontline requests, passing them to the mainframe, and sending the responses back.
Edgar's middleware worked well, so well that it was largely forgotten about. It outlasted Edgar himself, who, after another solid decade of service, moved on to another company.
A few years later, our submitter John F. joined the bank's C# team. By this point, the poor middleware seemed to be showing its age. A strange problem had arisen: between 8:00AM and 5:00PM, every 45 minutes or so, it would lock up and have to be restarted. Outside of those hours, there was no issue. The problem was mitigated by automatic restarts, but it continued to inflict pain and aggravation upon internal users and external customers. A true solution had to be found.
Unfortunately, Edgar was long gone. The new "owner" of the middleware was an infrastructure team containing zero developers. Had Edgar left them any documentation? No. Source code? Sort of. Edgar had given a copy of the code to his friend Bob prior to leaving. Unfortunately, Bob's copy was a few point releases behind the version of middleware running in production. It was also in C, and there were no C developers to be found anywhere in the company.
And so, the bank's bigwigs cobbled together a diverse team of experts. There were operating system people, network people, and software people ... including the new guy, John. Poor John had the unenviable task of sifting through Edgar's source code. Just as the C# key sits right next to the C key on a piano, reasoned the bigwigs, C# couldn't be that different from C.
John toiled in an unfamiliar language with no build server or test environment to aid him. It should be no great surprise that he got nowhere. A senior coworker suggested that he check what Windows' Process Monitor registered when the middleware was running. John allowed a full day to pass, then looked at the results: it was now clear that the middleware was constantly creating and destroying threads. John wrote a Python script to analyze the threads, and found that most of them lived for only seconds. However, every 5 minutes, a thread was created but never destroyed.
This only happened during the hours of 8:00AM to 5:00PM.
At the next cross-functional team meeting behind closed doors, John finally had something of substance to report to the large group seated around the conference room table. There was still a huge mystery to solve: where were these middleware-killing threads coming from?
"Wait a minute! Wasn't Frank doing something like that?" one of the other team members piped up.
"Frank!" A department manager with no technical expertise, who insisted on attending every meeting regardless, darted up straight in his chair. For once, he wasn't haranguing them for their lack of progress. He resembled a wolf who'd sniffed blood in the air. "You mean Frank from Accounting?!"
This was the corporate equivalent of an arrest warrant. Frank from Accounting was duly called forth.
"That's my program." Frank stood before the table, laid back and blithe despite the obvious frayed nerves of several individuals within the room. "It queries the middleware every 5 minutes."
They were finally getting somewhere. Galvanized, John's heart pounded. "How?" he asked.
"Well, it could be that the middleware is down, so first, my program opens a connection just to make sure it's working," Frank explained. "If that works, it opens another connection and sends the query."
John's confusion mirrored the multiple frowns that filled the room. He forced himself to carefully parse what he'd just heard. "What happens to the first connection?"
"What do you mean?" Frank asked.
"You said your program opens two connections. What do you do with the first one?"
"Oh! I just use that one to test whether the middleware is up."
"You don't need to do that!" one of the networking experts snarled. "For Pete's sake, take that out of your code! Don't you realize you're tanking this thing for everyone else?"
Frank's expression made clear that he was entirely oblivious to the chaos wrought by his program. Somehow, he survived the collective venting of frustration that followed within that conference room. After one small update to Frank's program, the middleware stabilized—for the time being. And while Frank became a scapegoat and villain to some, he was a hero to many, many more. After all, he single-handedly convinced the bank's bigwigs that the status quo was too precarious. They began to plan out a full migration away from mainframe, a move that would free them from their dependence upon aging, orphaned middleware.
Now that the mystery had been solved, John knew where to look in Edgar's source code. The thread pool had a limit of 10, and every thread began by waiting for input. The middleware could handle bad input well enough, but it hadn't been written to handle the case of no input at all.
[Advertisement] Utilize BuildMaster to release your software with confidence, at the pace your business demands. Download today!CodeSOD: The XML Dating Service
One of the endless struggles in writing reusable API endpoints is creating useful schemas to describe them. Each new serialization format comes up with new ways to express your constraints, each with their own quirks and footguns and absolute trainwrecks.
Maarten has the "pleasure" of consuming an XML-based API, provided by a third party. It comes with an XML schema, for validation. Now, the XML Schema Language has a large number of validators built in. For example, if you want to restrict a field to being a date, you can mark it's type as xsd:date. This will enforce a YYYY-MM-DD format on the data.
If you want to ruin that validation, you can do what the vendor did:
<xsd:simpleType name="DatumType"> <xsd:annotation> <xsd:documentation>YYYY-MM-DD</xsd:documentation> </xsd:annotation> <xsd:restriction base="xsd:date"> <xsd:pattern value="(1|2)[0-9]{3}-(0|1)[0-9]-[0-3][0-9]" /> </xsd:restriction> </xsd:simpleType>You can see the xsd:pattern element, which applies a regular expression to validation. And this regex will "validate" dates, excluding things which are definitely not dates, and allowing very valid dates, like February 31st, November 39th, and the 5th of Bureaucracy (the 18th month of the year), as 2025-02-31, 2025-11-39 and 2025-18-05 are all valid strings according to the regex.
Now, an astute reader will note that this is a xsd:restriction on a date; this means that it's applied in addition to ensuring the value is a valid date. So this idiocy is harmless. If you removed the xsd:pattern element, the behavior would remain unchanged.
That leads us to a series of possible conclusions: either they don't understand how XML schema restrictions work, or they don't understand how dates work. As to which one applies, well, I'd say 1/3 chance they don't understand XML, 1/3 chance they don't understand dates, and a 1/3 chance they don't understand both.
[Advertisement] Picking up NuGet is easy. Getting good at it takes time. Download our guide to learn the best practice of NuGet for the Enterprise.CodeSOD: Off Color
Carolyn inherited a somewhat old project that had been initiated by a "rockstar" developer, and then passed to developer after developer over the years. They burned through rockstars faster than Spinal Tap goes through drummers. The result is gems like this:
private void init(){ ResourceHelper rh = new ResourceHelper(); for ( int i = 0; i < 12; i++) { months[i] = rh.getResource("calendar."+monthkeys[i]+".long"); months_s[i] = rh.getResource("calendar."+monthkeys[i]+".short"); } StaticData data = SomeService.current().getStaticData(); this.bankHolidayList = data.getBankHolidayList(); colors.put("#dddddd", "#dddddd"); colors.put("#cccccc", "#cccccc"); colors.put("#e6e6e6", "#e6e6e6"); colors.put("#ff0000", "#ffcccc"); colors.put("#ffff00", "#ffffcc"); colors.put("#00ff00", "#ccffcc"); colors.put("#5050ff", "#ccccff"); colors.put("#aa0000", "#ff9999"); colors.put("#ff8000", "#ffcc99"); colors.put("#99ff99", "#ccffcc"); colors.put("#ffcc99", "#ffffcc"); colors.put("#ff9966", "#ffcc99"); colors.put("#00c040", "#99cc99"); colors.put("#aadddd", "#ccffff"); colors.put("#e0e040", "#ffff99"); colors.put("#6699ff", "#99ccff"); }There are plenty of things in this function that raise concerns- whatever is going on with the ResourceHelper and the monthkeys array, for example. But let's just breeze past that into that colors lookup table, because boy oh boy.
There's the obvious issue of using JavaScript to manage colors instead of CSS, which is bad, sure. But this translation table which converts some colors (presumably already used in the display?) to some other colors (presumably to replace the display colors) is downright mystifying. How did this happen? Why did this happen? What happens when we attempt to apply a color not in the lookup table?
I want to say more mean things about this, but the more I stare at the original colors and what they get translated to, I think this lookup table is trying to tell me I should…
…
…
lighten up.
[Advertisement] Keep the plebs out of prod. Restrict NuGet feed privileges with ProGet. Learn more.CodeSOD: All Locked Up
Dan was using a third-party database which provided a PHP API. At one point, Dan was running into an issue where he actually needed locks on the database. Fortunately for him, the vendor documentation told him that there was a method called obtainRowLock.
obtainRowLock($table, $where) - Attempt to lock a row, will escalate and lock the table if row locking is not supported, will escalate and lock the database if table locking is not supported; returns true on success, false on failure
$table - name of table to lock
$where - WHERE clause to define rows, ex: "WHERE id=52". If left empty, function will assume a table lock
That was exactly what Dan needed, so he called it. It returned false, implying a failure. He changed the parameters. He discarded his where clause. He tried all sorts of things, and it always returned false. So he dug into the source code, to see how it actually worked.
function obtainRowLock($table, $where) { return false; }Is it truly a failure if you don't even try?
[Advertisement] Keep all your packages and Docker containers in one place, scan for vulnerabilities, and control who can access different feeds. ProGet installs in minutes and has a powerful free version with a lot of great features that you can upgrade when ready.Learn more.Error'd: Better Nate Than Lever
Happy Friday. For those of us in America, today is a political holiday. But let's avoid politics for the moment. Here's a few more wtfs.
"Error messages are hard," sums Ben Holzman , mock-replying "Your new puzzle games are fun, LinkedIn, but your error messages need a little work…"
Orin S. chooses wisely "These should behave like radio buttons, so… No?" I get his point, but I think the correct answer is "Yes, they are checkboxes".
Mark W. refreshes an occasionally seen issue. "Fair enough, Microsoft Office - I don't trust those guys either." Without more diagnostics it's hard to say what's going here but maybe some of you have seen this before.
ANONYMOVS chiseled out an email to us. "Maybe it really is Roman numerals? I never did find the tracking ID..."
And finally, Jonathan described this final entry as "String locationalization resource names showing," jibing that "Monday appears to be having a bad Monday." So they were.
[Advertisement] Picking up NuGet is easy. Getting good at it takes time. Download our guide to learn the best practice of NuGet for the Enterprise.
CodeSOD: The Last Last Name
Sometimes, you see some code which is perfectly harmless, but illustrates an incredibly dangerous person behind them. The code isn't good, but it isn't bad in any meaningful way, but they were written by a cocaine addled pomeranian behind the controls of a bulldozer: it's full of energy, doesn't know exactly what's going on, and at some point, it's going to hit something important.
Such is the code which Román sends us.
public static function registerUser($name, $lastName, $username, ...) { // 100% unmodified first lines, some comments removed $tsCreation = new DateTime(); $user = new User(); $name = $name; $lastname = $lastName; $username = $username; $user->setUsername($username); $user->setLastname($lastname); $user->setName($name); // And so on. }This creates a user object and populates its fields. It doesn't use a meaningful constructor, which is its own problem, but that's not why we're here. We're here because for some reason the developer behind this function assigns some of the parameters to themselves. Why? I don't know, but it's clearly the result of some underlying misunderstanding of how things work.
But the real landmine is the $lastname variable- which is an entirely new variable which has slightly different capitalization from $lastName.
And you've all heard this song many times, so sing along with the chorus: "this particular pattern shows up all through the codebase," complete with inconsistent capitalization.
.comment { border: none; } [Advertisement] Utilize BuildMaster to release your software with confidence, at the pace your business demands. Download today!CodeSOD: And Config
It's not unusual to store format templates in your application configuration files. I'd argue it's probably a good and wise thing to do. But Phillip inherited a C# application from a developer woh "abandoned" it, and there were some choices in there.
<appSettings> <add key="xxxurl" value="[http://{1}:7777/pls/xxx/p_pristjek?i_type=MK3000{0}i_ean={3}{0}i_style=http://{2}/Content/{0}i_red=http://{2}/start.aspx/]http://{1}:7777/pls/xxx/p_pristjek?i_type=MK3000{0}i_ean={3}{0}i_style=http://{2}/Content/{0}i_red=http://{2}/start.aspx"/> </appSettings>Okay, I understand that this field contains URLs, but I don't understand much else about what's going on here. It's unreadable, but also, it has some URLs grouped inside of a [] pair, but others which aren't, and why oh why does the {0} sigil keep showing up so much?
Maybe it'll make more sense after we fill in the template?
var url = string.Format(xxxUrl, "&", xxxIp, srvUrl, productCode);Oh. It's an "&". Because we're constructing a URL query string, which also seems to contain URLs, which I suspect is going to have some escaping issues, but it's for a query string.
At first, I was wondering why they did this, but then I realized: they were avoiding escape characters. By making the ampersand a formatting parameter, they could avoid the need to write & everywhere. Which… I guess this is a solution?
Not a good solution, but… a solution.
I still don't know why the same URL is stored twice in the string, once surrounded by square brackets and once not, and I don't think I want to know. Only bad things can result from knowing that.
[Advertisement] Plan Your .NET 9 Migration with ConfidenceYour journey to .NET 9 is more than just one decision.Avoid migration migraines with the advice in this free guide. Download Free Guide Now!
CodeSOD: It's Not Wrong to Say We're Equal
Aaron was debugging some C# code, and while this wasn't the source of the bug, it annoyed him enough to send it to us.
protected override int DoCompare(Item item1, Item item2) { try { DateTime thisDate = ((DateField)item1.Fields["Create Date"]).DateTime; DateTime thatDate = ((DateField)item2.Fields["Create Date"]).DateTime; return thatDate.CompareTo(thisDate); } catch (Exception) { return 0; // Sorry, ran out of budget! } }Not to be the pedantic code reviewer, but the name of this function is terrible. Also, DoCompare clearly should be static, but this is just pedantry.
Now, there's a lot of implied WTFs hidden in the Item class. They're tracking fields in a dictionary, or maybe a ResultSet, but I don't think it's a ResultSet because they're converting it to a DateField object, which I believe to be a custom type. I don't know what all is in that class, but the whole thing looks like a mess and I suspect that there are huge WTFs under that.
But we're not here to look at implied WTFs. We're here to talk about that exception handler.
It's one of those "swallow every error" exception handlers, which is always a "good" start, and it's the extra helpful kind, which returns a value that is likely incorrect and provides no indication that anything failed.
Now, I suspect it's impossible for anything to have failed- as stated, this seems to be some custom objects and I don't think anything is actively talking to a database in this function (but I don't know that!) so the exception handler likely never triggers.
But hoo boy, does the comment tell us a lot about the codebase. "Sorry, ran out of budget!". Bugs are inevitable, but this is arguably the worst way to end up with a bug in your code: because you simply ran out of money and decided to leave it broken. And ironically, I suspect the code would be less broken if you just let the exception propagate up- if nothing else, you'd know that something failed, instead of incorrectly thinking two dates were the same.
.comment { border: none; } [Advertisement] BuildMaster allows you to create a self-service release management platform that allows different teams to manage their applications. Explore how!CodeSOD: A Highly Paid Field
In ancient times, Rob's employer didn't have its own computer; it rented time on a mid-range computer and ran all its jobs using batch processing in COBOL. And in those ancient times, these stone tools were just fine.
But computing got more and more important, and the costs for renting time kept going up and up, so they eventually bought their own AS/400. And that meant someone needed to migrate all of their COBOL to RPG. And management knew what you do for those kinds of conversions: higher a Highly Paid Consultant.
On one hand, the results weren't great. On the other, the code is still in use, though has been through many updates and modernizations and migrations in that time. Still, the HPC's effects can be felt, like this block, which hasn't been touched since she was last here:
// CHECK FOR VALID FIELD IF FIELD1 <> *BLANKS AND FIELD1 < '1' AND FIELD1 > '5'; BadField1 = *ON; LEAVESR; ENDIF;This is a validation check on a field (anonymized by Rob), but the key thing I want you to note is that what the field stores are numbers, but it stores those numbers as text- note the quotes. And the greater-than/less-than operators will do lexical comparisons on text, which means '21' < '5' is true.
The goal of this comparison was to require the values to be between 1 and 5. But that's not what it's enforcing. The only good(?) news is that this field also isn't used. There's one screen where users can set the value, but no one has- it's currently blank everywhere- and nothing else in the system references the value. Which raises the question of why it's there at all.
But those kinds of questions are par for the course for the HPC. When they migrated a bunch of reports and the users compared the results with the original versions, the results didn't balance. The HPC's explanation? "The users are changing the data to make me look bad."
[Advertisement] BuildMaster allows you to create a self-service release management platform that allows different teams to manage their applications. Explore how!Error'd: Button, button, who's got the button?
Wikipedia describes the (very old) English children's game. I wonder if there's a similar game in Germany. In any case, the Worcester News is definitely confused about how this game is played.
Martin I. explains "This is a cookie acceptance dialog. It seems to struggle with labeling the buttons when the user's browser is not set to English ..."
In Dutch, Robert R. is playing a different game. "Duolingo is teaching users more than just languages - apparently web development fundamentals are included when HTML entities leak into the user interface. That's one way to make " " part of your vocabulary!" We wonder why the webdev would want to use a nbsp in this location.
Ninja Squirrel shares a flubstitution nugget. "Since I've been waiting a long time for a good deal on a new gaming keyboard and the Logitech Play Days started today, I thought I'd treat myself. I wasn't prepared for what Logitech then treated me to - free gifts and wonderful localization errors in the productive WebShop. What started with a simple “Failed to load resource [Logitech.checkout.Total]” in the order overview ended with this wonderful total failure after the order was placed. What a sight to behold - I love it! XD"
David P. imagines that Tesla's web devs are allowed near embedded systems. "If Tesla can't even do dates correctly, imagine how much fun Full Self Driving is." Given how often FSD has been promised imminently, I conclude that date confusion is simply central to the corporate culture. Embrace it.
But it's not only Tesla that bungles whens. Neil T. nails another big name. "Has Google's Gemini AI hallucinated a whole new calendar? I'm pretty sure the Gregorian calendar only has 30 days in June."
And that's it for this week. Next Friday is definitely not June
[Advertisement] Utilize BuildMaster to release your software with confidence, at the pace your business demands. Download today!Classic WTF: NoeTimeToken
"Have you had a chance to look at that JIRA ticket yet?"
Marge debated pretending she hadn't seen the Slack message yet—but, if she did, she knew Gary would just walk over to her desk and badger her further. In truth, she didn't want to look at the ticket: it was a low priority ticket, and worse, it only affected a small fraction of one client's customers, meaning it was likely to be some weird edge case bug nobody would ever run into again. Maybe if I ignore it long enough, it'll go away on its own, she thought.
The client was a bookseller with a small but signifigant-to-them online presence; the software they used to sell books, including your standard e-commerce account functionality, was made by Marge's company. The bug was somewhere in the password reset feature: some customers, seemingly at random, were unable to use the password reset link the software emailed out.
Marge pulled up the ticket, looking over the half-hearted triage work that had been done before it landed on her desk to solve. The previous guy had pulled logs and figured out that all the customers who were complaining were using the same ISP based out of Germany. He'd recommended reaching out to them, but had been transferred to another division before he'd gotten around to it.
When Marge realized that the contact information was all in German, she almost gave up then and there. But with the magic of Google Translate, she managed to get in touch with a representative via email. After a bit of back and forth, she noticed this gem in one of his (translated) replies:
We want to display mails in our webmail client as close to the original as possible. Since most mails are HTML formatted, the client supports the full HTTP protocol and can display (almost) all HTML tags. Unfortunately, this means that "evil" JS-Content in such mails can do all kinds of stuff in the browser and therefore on the customer's PC.
To avert this, all mails are processed by a "SafeBrowsing"-module before they are displayed, to recognize and circumvent such manipulations. One of those security measures is the recognition of js-modules that begin with "on...", since that are mostly js functions that are triggered by some event in the browser. Our "countermeasure" is to just replace "on..." with "no..." before the HTML content is sent to the rendering process.
Marge frowned at the answer for a bit, something nagging at her mind. "There's no way," she murmured as she pulled up the access logs. Sure enough, the url for the reset link was something like https://bookseller.com?oneTimeToken=deadbeef ... and the customers in question had accessed https://bookseller.com?noeTimeToken=deadbeef instead.
A few lines of code and it was resolved: a conditional would check for the incorrect query string parameter and copy the token to the correct query string parameter instead. Marge rolled her eyes, merged her change into the release branch, and finally, at long last, closed that annoying low-priority ticket once and for all.
hljs.initHighlightingOnLoad(); code { font-family: Consolas, monospace; } [Advertisement] Keep the plebs out of prod. Restrict NuGet feed privileges with ProGet. Learn more.CodeSOD: Classic WTF: When it's OK to GOTO
Everybody knows that you should never use "goto" statements. Well, except in one or two rare circumstances that you won't come across anyway. But even when you do come across those situations, they're usually "mirage cases" where there's no need to "goto" anyway. Kinda like today's example, written by Jonathan Rockway's colleague. Of course, the irony here is that the author likely tried to use "continue" as his label, but was forced to abbreviate it to "cont" in order to skirt compiler "reserved words" errors.
while( sysmgr->getProcessCount() != 0 ) { // Yes, I realize "goto" statements are considered harmful, // but this is a case where it is OK to use them cont: //inactivation is not guaranteed and may take up to 3 calls sysmgr->CurrentProcess()->TryInactivate(); if( sysmgr->CurrentProcess()->IsActive() ) { Sleep(DEFAULT_TIMEOUT); goto cont; } /* ED: Snip */ //disconnect child processes if( sysmgr->CurrentProcess()->HasChildProcesses() ) { /* ED: Snip */ } /* ED: Snip */ if( sysmgr->CurrentProcess()->IsReusable() ) { sysmgr->ReuseCurrentProcess(); goto cont; } sysmgr->CloseCurrentProcess(); } [Advertisement] ProGet’s got you covered with security and access controls on your NuGet feeds. Learn more.
Classic WTF: The Core Launcher
“You R haccking files on my computer~!!!” Charles Carmichael read in a newly-submitted support ticket, “this is illigle and I will sue your whoal compiny. But first I will tell every1 nevar to buy youre stupid game agin.”
The bizarre spelling and vague threats were par for the course. After all, when you market and sell a game to the general public, you can expect a certain percentage of bizarre and vague customer communications. When that game is a popular MMPORG (no, not that one), that percentage tends to hover around the majority.
It took a few days to see the pattern, but the string of emails started to make sense. “Uh, when did your game become spyware?” said one email. “Are you doing this just to force us to play more often?” another customer asked. “I know you have a lot of AI and whatnot, so I think it leaked out. Because now my whole computer wants me to play all the time… like my dog bringing me his chew toy.”
As it turned out, the problem started happening a few days after an update to the core launcher was published. The core launcher was one of those terrifically handy executables that could download all of the assets for any single game that was published, scan them for completeness, replace bad or missing files, and then launch the game itself after the user signed in. It’s a must-have for any modern multiplayer online game.
This core launcher could also patch itself. Updates to this executable were fairly rare, but had to be made whenever a new title launched, as was recently the case. Obviously, a large battery of automated and manual testing is done to ensure that there are no problems after publishing, yet something seemed to have slipped through the cracks… at least for some customers.
After a whole lot of back and forth with customers, Chris was able to compile dozens of detailed process lists, startup program launches, newly installed applications, and firewall usage rules. As he pored over the collected information, one program was always there. It was Interfersoft’s fairly popular anti-virus suite.
It took a solid two days of research, but Chris was finally able to uncover the new “feature” in Interfersoft’s Advanced Firewall Protector that was causing the problems. Like many similar anti-virus suites, when a program wanted to use network services, Interfersoft would pop-up a dialog confirming that the program’s operation was authorized. Behind the scenes, if the user allowed the program, Interfersoft would make a hash of that executable file, and would allow its communications to pass through the firewall every time thereafter.
Users who had this antivirus solution installed had, at one time, allowed the launcher through their firewall. The first time they connected to the game server after the launcher patch was released, their executable would download its patch, apply it to itself, and restart itself. But then of course, the executable hash didn’t match any more, and the program was no longer able to go through the firewall.
Rather than asking users if they wanted to allow the program to connect to the internet, in the new version of Interfersoft’s suite, the anti-virus system would rename the executable and move it. The logic being that, if it was changed after connecting to the internet, it was probably malware.
But what did they name the file? Program.exe. Unless that was already taken, then they would name it Progra~1.exe or Progra~2.exe and so forth. And where did they place this file? Well, in the root directory of C of course!
This naming convention, as it turned out, was a bad idea. Back in the very old, Windows 3 days, Windows did not support long file names. It wasn’t until Windows NT 3.5.1 (and then Windows 95 later) that long file names were supported. Prior to this, there were a lot of limitations on what characters could be part of a filename or directory, one of those being a space.
In fact, any space in a shell command execution was seen to be an argument. This made sense at the time so you could issue a command like this:
C:\DOOM\doom.exe -episode 3That, of course, would start Doom at episode 3. However, when Microsoft switched to Long File Names, it still had to support this type of invocation. So, the way the windows cmd.exe shell works is simple. You pass it a string like this:
C:\Program Files\id Software\Doom\Doom.exe -nomusicAnd it will try to execute “C:\Program” as a file, passing it “Files\id Software\Doom\Doom.exe -nomusic” as argument to that executable. Of course, this program doesn’t exist, so it will then try to execute “C:\Program Files\id”, passing it “Software\Doom\Doom.exe -nomusic” as argument. If this doesn’t exist, it will try to execute “C:\Program Files\id Software\Doom\Doom.exe” passing in “-nomusic” as an argument. It would continue this way until a program existed and started, or until the path was depleted and no program was to be found.
And on top of all this, desktop shortcuts on Windows are mostly just invocations of the shell, with the actual location of the executable you want to start (the path) stored in text inside the shortcut. When you click it, it reads this path, and passes it to the shell to start up the program. And this is why Intersoft’s process of moving files to the root directory was the worst decision they could have made.
Most of the programs installed in Windows at this time were installed to the “Program Files” directory by default. This was a folder in the root (C:\) directory. So when you wanted to launch, for instance, Microsoft Word, the shortcut on your Desktop pointed to “C:\Program Files\Microsoft\Office\Word.exe” or Firefox, which was in “C:\Program Files\Mozilla\Firefox\”. But thanks to Program.exe in the root directory, you ended up doing this:
C:\Program.exe “Files\Microsoft\Office\Word.exe”and
C:\Program.exe “Files\Mozilla\Firefox\”So, when users were trying to launch their application – applications which resided in the Program Files directory on their C drive – they were getting the launcher instead.
Chris explained all of this in great detail to Interfersoft, all the while explaining to customers how to fix the problem with the firewall. It helped some, but several hundred customers ended up closing their accounts a direct result of the “hacking”.
A few weeks later, Interfersoft started responding to the issues with their customers. Fortunately (for them), they decided to not use their own auto-update process to deliver a new version of the firewall.
[Advertisement] Plan Your .NET 9 Migration with ConfidenceYour journey to .NET 9 is more than just one decision.Avoid migration migraines with the advice in this free guide. Download Free Guide Now!
Classic WTF: Take the Bus
Rachel started working as a web developer for the local bus company. The job made her feel young, since the buses, the IT infrastructure, and most of their back-office code was older than she was. The bus fare-boxes were cash only, and while you could buy a monthly pass, it was just a little cardboard slip that you showed the driver. Their accounting system ran on a mainframe, their garage management software was a 16-bit DOS application. Email ran on an Exchange 5.5 server.
In charge of all of the computing systems, from the web to DOS, was Virgil, the IT director. Virgil had been hired back when the accounting mainframe was installed, and had nestled into his IT director position like a tick. The bus company, like many such companies in the US, was ostensibly a private company, but chartered and subsidized by the city. This created a system which had all the worst parts of private-sector and public-sector employment merged together, and Virgil was the master of that system.
Rachel getting hired on was one of his rare “losses”, and he wasn’t shy about telling her so.
“I’ve been doing the web page for years,” Virgil said. “It has a hit counter, so you can see how many hits it actually gets- maybe 1 or 2 a week. But management says we need to have someone dedicated to the website.” He grumbled. “Your salary is coming out of my budget, you know.”
That website was a FrontPage 2000 site, and the hit-counter was broken in any browser that didn’t have ActiveX enabled. Rachel easily proved that there was far more traffic than claimed, not that there was a lot. And why should there be? You couldn’t buy a monthly pass online, so the only feature was the ability to download PDFs of the hand-schedules.
With no support, Rachel did her best to push things forward. She redesigned the site to be responsive. She convinced the guy who maintained their bus routes (in a pile of Excel spreadsheets) to give her regular exports of the data, so she could put the schedules online in a usable fashion. Virgil constantly grumbled about wasting money on a website nobody used, but as she made improvements, more people started using it.
Then it was election season. The incumbent mayor had been complaining about the poor service the bus company was offering, the lack of routes, the costs, the schedules. His answer was, “cut their funding”. Management started talking about belt-tightening, Virgil started dropping hints that Rachel was on the chopping block, and she took the hint and started getting resumes out.
A miracle occurred. The incumbent mayor’s campaign went off the rails. He got caught siphoning money from the city to pay for private trips. A few local cops mentioned that they’d been called in to cover-up the mayor’s frequent DUIs. His re-election campaign’s finances show strange discrepancies, and money had come in that couldn’t be tied back to a legitimate contribution. He tried to get a newly built stadium named after himself, which wasn’t illegal, but was in poor taste and was the final straw. He dropped out of the election, paving the way for “Mayor Fred” to take over.
Mayor Fred was a cool Mayor. He wanted to put in bike lanes. He wanted to be called “Mayor Fred”. He wanted to make it easier for food trucks to operate in the city. And while he shared his predecessor’s complaints about the poor service from the bus company, he had a different solution, which he revealed while taking a tour of the bus company’s offices.
“I’m working right now to secure federal grants, private sector funding, to fund a modernization project,” Mayor Fred said, grinning from behind a lectern. “Did you know we’re paying more to keep our old buses on the road for five years than it would cost to buy new buses?” And thus, Mayor Fred made promises. Promises about new buses, promises about top-flight consultants helping them plan better routes, promises about online functionality.
Promises that made Virgil grumble and whine. Promises that the mayor… actually kept.
New buses started to hit the streets. They had GPS and a radio communication system that gave them up-to-the-second location reporting. Rachel got put in charge of putting that data on the web, with a public API, and tying it to their schedules. A group of consultants swung through to help, and when the dust settled, Rachel’s title was suddenly “senior web developer” and she was in charge of a team of 6 people, integrating new functionality to the website.
Virgil made his opinion on this subject clear to her: “You are eating into my budget!”
“Isn’t your budget way larger?” Rachel asked.
“Yes, but there’s so much more to spend it on! We’re a bus company, we should be focused on getting people moving, not giving them pretty websites with maps that tell them where the buses are! And now there’s that new FlashCard project!”
FlashCard was a big project that didn’t involve Rachel very much. Instead of cash fares and cardboard passes, they were going to get an RFID system. You could fill your card at one of the many kiosks around the city, or even online. “Online” of course, put it in Rachel’s domain, but it was mostly a packaged product. Virgil, of all people, had taken over the install and configuration, Rachel just customized the stylesheet so that it looked vaguely like their main site.
Rachel wasn’t only an employee of the bus company, she was also a customer. She was one of the first in line to get a FlashCard. For a few weeks, it was the height of convenience. The stop she usually needed had a kiosk, she just waved her card at the farebox and paid. And then, one day, when her card was mostly empty and she wasn’t anywhere near a kiosk, she decided to try filling her card online.
Thank you for your purchase. Your transaction will be processed within 72 hours.
That was a puzzle. The kiosks completed the transaction instantly. Why on Earth would a website take 3 days to do the same thing? Rachel became more annoyed when she realized she didn’t have enough on her card to catch the bus, and she needed to trudge a few blocks out of her way to refill the card. That’s when it started raining. And then she missed her bus, and had to wait 30 minutes for the next one. Which is when the rain escalated to a downpour. Which made the next bus 20 minutes late.
Wet, cold, and angry, Rachel resolved to figure out what the heck was going on. When she confronted Virgil about it, he said, “That’s just how it works. I’ve got somebody working full time on keeping that system running, and that’s the best they can do.”
Somebody working full time? “Who? What? Do you need help? I’ve done ecommerce before, I can-”
“Oh no, you’ve already got your little website thing,” Virgil said. “I’m not going to let you try and stage a coup over this.”
With an invitation like that, Rachel decided to figure out what was going on. It wasn’t hard to get into the administration features of the FlashCard website. From there, it was easy to see the status of the ecommerce plugin for processing transactions: “Not installed”. In fact, there was no sign at all that the system could even process transactions at all.
The only hint that Rachel caught was the configuration of the log files. They were getting dumped to /dev/lp1. A printer. Next came a game of hide-and-seek- the server running the FlashCard software wasn’t in their tiny data-center, which meant she had to infer its location based on which routers were between her and it. It took a few days of poking around their offices, but she eventually found it in the basement, in an office.
In that office was one man with coke-bottle glasses, an antique continuous feed printer, a red document shredder, and a FlashCard kiosk running in diagnostic mode. “Um… can I help you?” the man asked.
“Maybe? I’m trying to track down how we’re processing credit card transactions for the FlashCard system?”
The printer coughed to life, spilling out a new line. “Well, you’re just in time then. Here’s the process.” He adjusted his glasses and peered at the output from the printer:
TRANSACTION CONFIRMED: f6ba779d22d5;4012888888881881;$25.00
The man then kicked his rolly-chair over to the kiosk. The first number was the FlashCard the transaction was for, the second was the credit card number, and the third was the amount. He punched those into the kiosk’s keypad, and then hit enter.
“When it gets busy, I get real backed up,” he confessed. “But it’s quiet right now.”
Rachel tracked down Virgil, and demanded to know what he thought he was doing.
“What? It’s not like anybody wants to use a website to buy things,” Virgil said. “And if we bought the ecommerce module, the vendor would have charged us $2,000/mo, on top of an additional transaction fee. This is cheaper, and I barely have enough room in my budget as it is!”
[Advertisement] Plan Your .NET 9 Migration with ConfidenceYour journey to .NET 9 is more than just one decision.Avoid migration migraines with the advice in this free guide. Download Free Guide Now!