text:
Maker & Marketer: An Interview With Caitlin Hamilton Summie
Post-40 Bloomers
Sonya Chung August 17, 2016 | 2 books mentioned 9 min read
This post was produced in partnership with Bloom, a literary site that features authors whose first books were published when they were 40 or older.
Take one or two steps into the world of literary marketing and commerce, and you will likely encounter the name Caitlin Hamilton Summie. In particular, if you are a champion of independent publishing and bookselling, the degrees of separation to Summie will be few. For literary writers coming out with a debut, or perhaps seeking to improve their second or third book's visibility, the search for an independent publicist will likely begin with personal recommendations; and it's via that word-of-mouth chain that Summie — a lover of books but also, clearly, of the human side of literary creation and marketing — rises to the top of the referral list.
It's commonplace these days for authors to participate actively in publicity efforts, and, while doing so, to comment on the fact that publicity requires an extremely different temperament and skill set from writing. And so it's not often that publicists double as authors. Here, too, Summie, age 47, is exceptional: next spring, her debut collection of stories, To Lay to Rest Our Ghosts, will be published by Fomite Press.
Bloom: You're a books and lit person through and through, that much is clear. When and how did it start? Did you grow up in a bookish family?
Caitlin Hamilton Summie: I grew up in a household filled with books, and both my parents are avid readers. I remember falling in love with writing before I fell in love with reading, oddly enough. I started writing as a preschooler. I wrote stories and gave them to my mother to read, but since I couldn't write yet and had only scribbles, she quite smartly asked me to read my stories to her instead. I have always appreciated the respect she gave me, even then. I can't remember when books became as important to me — lifeblood — but I believe I was an adult.
Bloom: You've worked as a bookseller, a marketing and publicity director for a corporate publisher, a marketing, publicity, and sales director for an independent publisher, and as an independent publicist for both individual authors and small presses. We who love books but have never worked inside the business don't realize how complex is the web of book publishing and selling. But we authors do hear stories of how dysfunctional the publishing world can be. I wonder if, as someone who loves books as much as you do, knowing so much of the inside baseball — the "sausage-making" as they say — is ever discouraging or demoralizing?
CHS: Yes, it can be discouraging. There are several things about the industry that can wear one down, especially for those of us deeply involved in the small press world — the fight for review space being one. There is the continual vast difference in resources in general — financial, staff — that make small press life more of a challenge.
But I am interested in what books and publishing can become. I am quite energized by the revolution in books, the different ways people can now publish — POD [print on demand], hybrid, traditional, large press, small press, self. There used to be only one real way to share stories, but now we have stories being published in a variety of ways, and I think we as an industry will benefit, that it will spur continued innovation.
Every discouraging moment comes with a moment of success or joy — a great and important review, the discovery of a new talent, that perfect pitch to a niche outlet — and so we here in this firm get up and turn the lights on to make certain those voices are heard.
Bloom: Of all the jobs above, which would you say is/was the most challenging, and why?
CHS: I think that publicity is the hardest right now. Things are changing quickly, as we go mobile and more communities and publications proliferate online, and more books hit the marketplace. But those very challenges keep it exciting, too. One has to keep learning, and I believe that is a great thing in a job.
Bloom: Most of your work has been in the indie world. Has that been a deliberate choice, or just how it turned out?
CHS: I began in the big house world, in editorial at Vintage. But I am not a New Yorker, and so my focus changed the day I started counting the trees in my neighborhood. I thought I was leaving books for good when I left New York, but of course I didn't. I worked at an indie bookstore, then slowly found my way back to publishing by joining MacMurray & Beck.
At the indie bookstore I handled events and also maintained the biography section. Sadly, I could never decide how I wanted the section to look: should I alphabetize by author or subject? On the floor, I hand sold the same two novels over and over (Ellen Foster by Kaye Gibbons and Floating in My Mother's Palm by Ursula Hegi) so I learned the power of handselling, the importance of independent bookstores, and what they mean to a community.
I remember that when we closed, I was somehow tasked with the job of announcing the discounts over the PA system. I felt such resentment as people swept through the doors, people I had never seen before. My dad came in looking guilty, and I told him to buy away. He was a genuine, regular customer, and he of all people should get a discount.
At MacMurray & Beck, I was the marketing director, but I was also the publicity director, and for two years roughly I managed all sales nationwide, from Barnes & Noble to mom-and-pop stores. My college major was in Middle Eastern history, so basically I learned everything about marketing on my feet.
As my career progressed, I developed a growing love of small presses, and so yes, it became a conscious effort on my part to remain involved in the small press community.
Bloom: What made you decide to open up your own book marketing and publicity firm in 2003?
CHS: Ah! I was laid off from Penguin Putnam and looking for jobs. But quite quickly, the phone began ringing. First, a small press publisher needed publicity help for a really literary novel.
I had done publicity, marketing, sales, and bookselling so I felt ready to assist in a variety of capacities, which is what I did whenever someone called: I determined where they needed me on their team (and still do.)
I really enjoyed my freelance work, and about the third time the phone rang, it hit me: I have started a business.
Bloom: What advice would you offer to authors who are considering hiring an independent publicist? What have you learned about how/whether a book "breaks through" to get press attention and sales?
CHS: I advise any author who wants to hire a publicist to treat this as a business. Develop a set of expectations and a budget prior to speaking with publicists, and make certain they fit your needs and plans. I believe publicity is all about fits, so an author should interview people, review their websites, speak with them to make certain working with them on a day-to-day basis is possible, get references. Ask questions.
Sometimes breaking out an author is actually a ten-year process, a slow build from book to book. Sometimes it comes in a lightening-flash of bookseller and media love. I have seen it happen both ways. What I have learned is that for the books I represent, there is no set formula. There are definitely things that we know will be helpful — starred trade reviews, other reviews in publications that really fit the book's audience, a striking cover, handselling — but I believe in remaining creative because you can develop all those things for an author and still not break out.
Bloom: I confess that for a long time, when you and I were corresponding about Bloom, and book biz, and other subjects, I assumed you lived in New York City. Talk about that common misperception — that all literary work and life happens in New York — and the ways in which it's wrong, misguided, possibly even damaging to literary life?
CHS: You are not alone! When people find out I live in Tennessee, there is usually an awkward pause. People forget too easily the importance of the South to American literature, and even more so forget the importance of Tennessee itself to American letters. We have Parnassus Bookstore, and Burke's, and Union Avenue Books. Vanderbilt and UT both have MFA programs. Also, we host The Southern Festival of Books and the Sewanee Writers' Conference. And so many writers are here! Pamela Schoenwaldt, Joy Harjo, Marilyn Kallet, Michael Knight, Amy Greene. Alex Haley was originally from Nashville, Charles Wright was from Pickwick Dam, James Agee was born in Knoxville as was Cormac McCarthy. McCarthy set his first four novels in East Tennessee. William Gay lived in Tennessee as well. Poets Laureate Allen Tate and James Dickey went to Vanderbilt, and Robert Penn Warren, also a Poet Laureate, taught there.
The fact is that for the majority of my career in books (all but one year), I have not been in New York. There is a vibrant, different literary world outside New York—and some incredible work being done—and I think the presumption that the best of literature is in New York or that New York is the center of literary life is in fact damaging. MacMurray & Beck published the first novels of Steve Yarbourgh, William Gay, Patricia Henley, and Susan Vreeland, among others, out of small offices in central Denver. Free Spirit Publishing, with whom I was an intern the summer after I graduated from high school, is in Minneapolis. When I interned there, they were publishing books for gifted and talented kids that address real life issues, something they began back in the 1980s to fill a gap. They've continued to innovate. She Writes Press in Berkeley is succeeding with a new publishing model. Don't get me wrong. New York is important, of course. But as book lovers and readers we are more than what one city discovers.
Bloom: What would you say have been some of the most significant changes and trends in bookselling, marketing, and publicity over the last 20 years? 10 years? What do you think might be the Next Big Change?
CHS: I have seen tons of changes: the shrinking of book review pages, the development of paid reviews, the rise of the Internet and Internet media, the development of the citizen (consumer) reviewer, and the creation of online engagement through social media.
I've begun to think the next change will be in delivery — in the methodology itself as well as in how current delivery options are perceived. I am so intrigued by the book vending machines abroad. I imagine soon we will be delivering books in great, fun new ways here, too. I think POD ought to be more acceptable than it is in some quarters. It is a smart choice for smaller houses.
Bloom: A Next Big Change for you is that you are about to become a published author of a collection of short stories. A lot of folks who work in publishing — as editors, publicists, booksellers, etc. — have creative projects going on the backburners, in hopes that their time will come. And it has for you, after many years of working steadily and quietly on your fiction. Tell us about those years, that journey, and what this moment means for you.
CHS: I earned my MFA from Colorado State in 1995 and had had a few stories published in 1995 and 1996. Since then, I have continued to write. I even sent a few pieces out, though none were accepted. Like many, work and motherhood were happy distractions for many years (and still are.) The recent acceptance of my short story, "Sons," at Mud Season Review, was a sweet moment. I wrote that story in 1992. I have always loved it, and for someone else to find merit in it was really exciting.
For as long as I have been writing, the book acceptance happened very quickly. As tickled as I was to get the news, I was also stunned. A few days later, when my family met to celebrate, I was more joyous than the night I had read the acceptance email. It is tremendously exciting!
Bloom: What do you think lit the fire under you to begin pursuing publication more seriously recently?
CHS: I had been working on a middle-grade novel and needed a change, so I decided to revisit my stories. I had sent a couple out, including "Sons." When it was accepted, I thought, "Why not go for it?" I sent more stories out and then decided to go for it wholeheartedly and sent the collection, To Lay to Rest Our Ghosts, off to Fomite.
Bloom: What can you tell us about the stories in your forthcoming collection?
CHS: The stories are about family — often about loss and about accepting each other as we are, sometimes about accepting ourselves as we are. Many of the stories are set in Minnesota and involve snow. I grew up in Minnesota and Massachusetts — snow everywhere during the winter. That may sound like a trivial detail, but snow, and weather in general, are important to my writing. The collection begins with the story of a kid, follows people as they age, and ends with the story of an old man. But what links them isn't as much age or aging as it is the themes of family, loss, and hope.
Bloom: How will it feel to put the marketing, sales, and publicity of your book in the hands of someone else? How do you see yourself being involved?
CHS: Fomite is a press with a small team and asks authors to do a lot of the marketing, so I will be working on my own book, with the assistance of my husband, who is also a book publicist. We chose the cover image with the publisher shortly after the book was accepted and just chose the book title and that has been a great process. We made team decisions, which I like. I have no problem letting others in on the marketing. In my experience, teams are best. Even when a team disagrees, we all refine our thinking and get better. Also, I may be too close to these stories and so having the Fomite team share their perspectives is essential.
Bloom: Are you working on new fiction now?
CHS: Yes! I am taking the middle-grade novel through a last rewrite and then revising a couple of children's picture books. I still have a few short stories to reconsider. Also, because some of the stories link, I want to revisit a manuscript that is a novel-in-stories—a few pieces for that book are from the collection and then there are a bunch of others to add.
Bloom: You have so much experience with book release triumphs and disappointments; and you've already said that a book's reception is unpredictable. What will you consider a "success" for the roll-out of To Lay to Rest Our Ghosts?
CHS: Reviews. Good reviews will mean the book is a success to me. Sales would be sweet, but short story collections are always a tough sell. The critical reception matters most in building my brand as a writer.
Sonya Chung is author of the novels Long for This World (Scribner 2010) and The Loved Ones (Relegation Books 2016), which was a selection for Kirkus Best Fiction 2016, Indie Next List, Library Journal Best Indie Fiction, TNB Book Club, Buzzfeed Books Recommends, and Writer's Bone Best 30 Books 2016. She is founding editor of Bloom and teaches fiction writing at Skidmore College. Learn more about Sonya here.
One Sentence at a Time: The Millions Interviews Sion Dayson
Jen Lue October 11, 2019 | 2 books mentioned 7 min read
I think whenever a writer sits down to write, she's approaching the page with the accumulation of all she's read coming to bear.
Jen Lue | 2 books mentioned 7 min read
Giuseppe Tomasi di Lampedusa's Burst of Sicilian Sun
Sonya Chung July 24, 2014 | 2 books mentioned 10 min read
When I read Lampedusa the sun bursts up indeed, thawing all of that deeply seeded "puritanical horror," and reconciling life forces that, as Lampedusa attempts to show us, were never meant to be opposed.
Sonya Chung | 2 books mentioned 10 min read
Post-40 Bloomers: "Late" According to Whom?
Sonya Chung September 26, 2011 | 2 books mentioned 45 3 min read
Post-40 Bloomers: Harriet Doerr's Impossible Perfection and Happiness
Sonya Chung March 29, 2012 | 2 books mentioned 7 8 min read
Awaiting the Next Revival: In Search of Isabel Bolton
Alice Lowe April 6, 2016 | 2 books mentioned 10 min read
Isabel Bolton has fallen into obscurity a second time. How and why does this happen? What accounts for the failure of a work to catch hold, in spite of outstanding reviews?
Alice Lowe | 2 books mentioned 10 min read
Post-40 Bloomers: Shannon Cain's The Necessity of Certain Behaviors
Ed Porter December 10, 2012 | 2 books mentioned 1 8 min read
Joan Chase: Our Childhood Edens and Lost Orchards of Memory
Amy Weldon November 13, 2014 | 2 books mentioned 3 10 min read
Every year I teach Mary Shelley's novel Frankenstein to first-year college students, who can't quite believe it was written by a girl their age. How could someone so young create a work so furiously complex, alive with the energies of need, anger, love, and alienation? But then, who would have known freakishness so well as a bookish girl in a male-dominated world, secretly convinced she'd killed the mother she never knew? "When I looked around I saw and heard of none like me," the Creature tells his creator, Victor Frankenstein. "Was I, then, a monster, a blot upon the earth, from which all men fled and whom all men disowned?" Yet the Creature's own consciousness makes avoiding this pain impossible, and, like any writer, he is drawn to examine it: "[O]f what a strange nature is knowledge!" he tells Victor. "It clings to the mind, when it has once seized on it, like a lichen on the rock." Knowledge brings pain. But knowledge — and its exploration, in stories — is irresistible.
The sorrowing rage of a precocious daughter who felt spurned by her father and responsible for her mother's death certainly drives the novel. But its origins aren't quite that simple. Frankenstein was a book Mary Shelley had to write, for reasons she might never have been able to explain. That inward pressure is part of the alchemy that makes any novel an even bigger and stranger experience for its writer (and its reader) than the writer knows. While we may sometimes connect the real Mary Shelley with her brooding Creature, Frankenstein's enduring allure comes from a much more mysterious place — an imaginative energy born of transgression, memory, fear, and desire, which may spring from real life but isn't ever fully bound by it. That energy communicates itself to us, elevating the idea of the novel itself — to a heightened sensory tour of a recognizable human reality, fundamentally not responsible to any laws but its own.
Joan Chase, whose first novel was published when she was 47, is a writer whose work demonstrates this energy. She's won many awards (including a Guggenheim, a Whiting Writers' Award, the Janet Heidinger Kafka Prize, and PEN America's Ernest Hemingway Foundation Award), and, according to one online source, is "still writing." Yet she's a shy, little-known presence in the modern literary world, with no webpage or Twitter feed. When confronted with so specific a fictional realm as Chase's, readers accustomed to copious author bios and Internet availability will find themselves baffled. Yet her fiction demonstrates just how little "authorial intent" or "biography" can matter. Chase teaches us what it means for a writer to submit herself to the story, letting fiction and fact alchemize according to the needs of the created world on the page and following wherever that world's logic leads, regardless of literal "truth."
Recently reissued by NYRB Classics, Chase's first novel, During the Reign of the Queen of Persia (1983), is narrated by a collective "we," a group of sisters and cousins living in a matriarchal farm household in northern Ohio in the 1950s. "For as long as we could remember," they tell us, "we had been together in the house which established the center of the known world." Celia, Jenny, Anne, and Katie are cousins and sisters, "like our mothers, who were sisters" and who are all living in the house together at one time or another. "Sometimes we watched each other, knew differences," they say. "But most of the time it was as though the four of us were one and we lived in days that gathered into one stream of time, undifferentiated and communal."
The girls' days are marked by farm-kid pleasures (kittens under the porch, playing in the hayloft with a hidden deck of cards) and violence (fighting with each other and their boy cousin, the sinister Rossie) and family crises that descend like weather: cancer, marital discord, courtship and impending marriage, abuse and making-up again. Family members come and go and come again until three generations — parents and their daughters, and the daughters' husbands and children — have settled into the house. At every turn the girls feel knit deeper into the place:
Peaceable, we waited on the porch in the dappling noontime. In the Mason jars stacked up dusty and fly-specked on the side shelves, in the broken-webbed snowshoes hung there, the heap of rusty hinged traps waiting this long time to be oiled and set to catch something in the night, was the visible imprint of the past we were rooted in.
The girls' world is presided over by a fierce, dominating goddess — their grandmother Lil. Nicknamed the eponymous "Queen of Persia," Lil has been working her entire life, starting as a scrawny 11-year-old nanny for a neighbor with tuberculosis. Even an inheritance from a rich uncle, which enables her to buy a farm for herself, doesn't soften her sense of grievance at the world. "She vowed it was peculiar," say the girls, "her father spent his life in the West, searching for oil, when all along it was right out back under the corn crib. Now wasn't that just like a man? Like life." Focused in old age on her own self-protection, Lil widens her angry judgment of the world to include her daughters and granddaughters. Sometimes she condemns them, but sometimes she protects them. When her oldest daughter Grace dies of cancer, Gram squares off against Grace's feckless husband, Neil. Yet at the novel's end — after making a decision that shocks the reader — Gram snorts, "What did we ever have around here but dying and fighting? Work and craziness." For the girls, Gram models womanhood as sheer cussedness and endurance, a "soiled and faded apron and her exhausted face, marked like an old barn siding that had withstood blasts and abuse of all kinds, beyond any expression other than resignation and self-regard."
The man for whom Gram reserves most of her fury is her husband, Jacob, a stern Amish outcast who "was bigger than all the other men we saw who came around the farm…it was a bigness of bone, as though he were solid calcium with only skin stretched over him." Cursing at the cows, backhanding Rossie into the barn wall for smashing eggs, and changing his long underwear only a few times a year, Grandad is a dark force of nature whose inability to interpret or express his own emotions makes him terrifying but, initially to Gram as a young woman, alluring:
Every night his eyes were watching, wanting her and letting her see it in him; but he wouldn't touch her…though when she would pass close beside him she would hear his breathing, harsh and quick. It nearly drove her wild and her mind came to dwell on him nearly every second. Sometimes, when she lifted up the handle of the stove to stir the wood, the glutted, ashy coals crumbled at the slight touch and something inside her seemed to fragment in the same way.
Eventually, marriage — marked by furtive, rape-like sex and Jacob's long absences — bends Gram's desire into a thick club of anger, aggression, and dark humor with which she attacks everyone around her, and herself:
I seen more damned men than you would believe drinking themselves crazy, killing each other over nothing. And their women dying with babies or something else unnecessary. But you can't tell them. I'm through trying. You can't tell a young gal nothing, nor an older one neither. Not anything she don't want to hear.
Watching Gram hang on to her life exactly as it is — remaining married to a man she hates, stashing her money under the floorboards, and shaking up her family with daily small cruelties — makes the reader wonder: in a world that thwarts women, what makes a woman also thwart herself, surrendering to meanness and pushing against a hard life in a way that only makes it harder? What's the source of that particularly Midwestern passive aggression, self-sabotage, and buried rage? And why hate the one who gets away from it all — in this case Aunt Elinor, a successful New York career woman whose efforts to care for her dying sister are mocked even as they are relied upon? "Aunt Elinor looked patient, as one who had seen a wider world," the girls observe, "one she constantly made visible to the rest of us — accepting the fact that a wider world might mean a weaker place in the old one." Why love a place where the ordinary marvels of life — "The wet orchard grass and briers gleamed like washed planking, while above, the branches held green sails to the wind" — are braided with such pain?
When you are immersed in Joan Chase's writing, that love seems wholly inevitable. In her review of During the Reign of the Queen of Persia, Margaret Atwood described the girls' connection to this place:
Will the 'we,' having known a childhood so all-enveloping, so histrionic and so collective, ever be able to resolve itself successfully into four separate 'I's? For despite the horror of some of the events they witness, the children's life at Gram's is fascinating and addictive, and they live with an intensity and gusto that prevent their final vision from being a bleak one.
Indeed, something in the girls cleaves to their "flamboyantly, joylessly unpredictable" grandmother, no matter what:
When we are grown up and have been through everything, we'll be like that. We'll order kittens drowned by the bagful. Then at night we'll dress in our silken best, pile on jewels and whiz off to parties, bring home prizes for the family. We'll bet on horses.
This thread of resilience brightens the otherwise dark weather of this novel, which nevertheless isn't forced or melodramatic — it's only doing what it must, only being what it is. Lacking answers to the questions we might ordinarily ask the author — Is this your family? Are you saying something about women and passive-aggression here? — we fall back on the novel itself and on our own reactions, delving deeper into the territory of self-investigation. Which is to say, into literature.
Like Marilynne Robinson's Housekeeping, which was published three years earlier, During the Reign of the Queen of Persia is poised on the border between strict realism and something like a dream. If the governing element of Robinson's novel is water — the frozen lake, the drowned train — Chase's novel is of the soil. Rooted in landlocked northern Ohio, it is replete with cluttered farmhouses and barns and deer stealing windfall apples from the orchard. Yet its effects are never showy or awkward, never just rural Gothic cliché. Like William Faulkner, whom Chase admires, this is novelistic imagination with no elaborate scaffolding between reader and author — just direct immersion in a stream of subjectivity and life we come to know through that immersion itself. In this, the novel echoes its subjects: terrifying, marvelous, and memorable things happen here, and that's just how it is — here in this dark Midwestern Eden, with its gnarled and faithful apple trees.
Chase found an early center of gravity in a large family homestead in rural Ohio like the one in Queen of Persia. "[I]t was wonderful," she has said, "to have so much family around me." On an "Ohioana Authors" radio program, she said, "When I began to write what became During the Reign of the Queen of Persia, I didn't decide on Ohio as the setting, Ohio was just there, my imaginative heartland. It was the land of my childhood and from my perspective the most lovely and thrilling place in the world."
These biographical statements (among the few Chase has made) don't account for the elements of Queen of Persia that are less than "lovely." But they don't really need to; we all know that what is "true" doesn't always make the best story. A writer must let the emotional qualities and images on the page shape the story according to her own emerging logic. This means that tracing a writer's "biography" in a novel can be difficult. Particularly for the writer herself. And that, too, is perhaps as it should be.
Chase's later works return to themes similar to Queen of Persia, although in more diffuse, experimental ways. Her second novel, The Evening Wolves (1989) explores how a feckless, wandering father warps his daughters' lives. Francis Clemmons is charming and fierce, self-centered and often irresistible. The first-person narration (traded among his three children and second wife) is precise and pithy, rooting us in particular bodies and subjectivities: "My hair is straight, quiet hair," young daughter Ruthann declares, "and my head feels peaceful, at least where it shows." Elsewhere she tells how a boy "started rubbing my bones, their stone hearts luminous in the dark, binding me like stays." Unlike Persia, the novel feels inconclusive, with an open-endedness that is employed to better effect in Chase's short fiction.
Gathered in Bonneville Blue (1991), Chase's stories are strange and shapely, centered around striking images from down-at-heel rural worlds reminiscent of Persia's barns and backroads. Sally in "Crowing" accompanies a cranky old man around his barn: "People say farm animals know when the hog butcher is coming. Somehow. Even the day before, they will be restive, off their feed, as though word of the appointment has reached them." Here, too, women are yearning yet uncertain how to act. In the lyrical "The Harrier," an unhappily married woman dreams of a younger man:
I didn't go with him up into that bed in the forest, not in the end, although as I said, in that winter of cold and driving spikes of ice he seemed to slam against my bedroom window all night like some night bird wanting in. But I chose to lie on, hugging the curve of my husband's unyielding back, dreaming the smell that is feverish and rank, the distillation of roots and vines newly turned over.
Like Elena Ferrante, whose novels of growing up in midcentury Naples have drawn fresh attention this fall — and who writes from behind an inviolable pseudonym — Joan Chase disrupts the links we seek between a writer's life and her art to let her work stand alone in the public eye. Of course, Joan Chase is her real name. But her relative silence, while thwarting readers' curiosity, serves us as Ferrante's pseudonymity does by sending us back to the work, which stands on its own — enigmatic, dark, and gorgeous.
Reading Chase, Ferrante, and Mary Shelley all together reignites my curiosity about women, writing, and boldness. What interior permissions, or exterior disguises, or at-long-last states of peace and determination must a woman attain to in order to speak the story that wants to take shape, whatever that shape may be? I'm wondering, too, about the relationship between personal privacy and the kind of boldness we need to do our work. In the Internet age, Ferrante's pseudonym and Chase's quietness both suggest strategies to address that issue: if you want to avoid complaining family members, or earnest reviewers asking you about "which parts are autobiographical," or random readers' emails, short-circuiting the link between you and the public might help. Maintaining privacy might also quiet interior voices that insist a good daughter would never write this. Seeking recognition is just not what we do in this family. If you get the wrong kind of attention, it's your own fault.
During the Reign of the Queen of Persia does make me wonder about Chase's family's reaction: whether they recognized themselves, whether they objected, whether they half-resented the one whose success they also envied. But ultimately, it's not our business. It is enough that Joan Chase brought into the world a novel so vivid, risky, and beautiful, and that from it we can learn to trust our stories — to finger the jagged grain of those trees in our childhood Edens, those lost orchards of memory — and let them take us where they need to go.
Amy Weldon | 2 books mentioned 3 10 min read
Post-40 Bloomers: Giuseppe Tomasi di Lampedusa, The Last Leopard
Sonya Chung December 30, 2011 | 2 books mentioned 6 7 min read
One indisputable factor that deprived us of more opportunities to luxuriate in Lampedusa's gifts was a diagnosis of lung cancer at the age of 60. The diagnosis came just a few months after he finished the novel, two publisher rejections already in hand, a third which would arrive weeks before he died in July of that year.
Sonya Chung | 2 books mentioned 6 7 min read


text:
package mesosphere.marathon.upgrade

import akka.testkit.TestActorRef
import mesosphere.marathon.core.launchqueue.LaunchQueue
import mesosphere.marathon.core.task.Task
import mesosphere.marathon.core.task.tracker.TaskTracker
import mesosphere.marathon.event.{ HealthStatusChanged, MesosStatusUpdateEvent }
import mesosphere.marathon.health.HealthCheck
import mesosphere.marathon.state.PathId._
import mesosphere.marathon.state.{ AppDefinition, UpgradeStrategy }
import mesosphere.marathon.test.MarathonActorSupport
import mesosphere.marathon.upgrade.TaskReplaceActor.RetryKills
import mesosphere.marathon.{ MarathonTestHelper, TaskUpgradeCanceledException }
import org.apache.mesos.Protos.{ Status, TaskID }
import org.apache.mesos.SchedulerDriver
import org.mockito.Matchers.any
import org.mockito.Mockito
import org.mockito.Mockito._
import org.mockito.invocation.InvocationOnMock
import org.mockito.stubbing.Answer
import org.scalatest.concurrent.Eventually
import org.scalatest.mock.MockitoSugar
import org.scalatest.{ BeforeAndAfterAll, FunSuiteLike, Matchers }

import scala.concurrent.duration._
import scala.concurrent.{ Await, Promise }

class TaskReplaceActorTest
    extends MarathonActorSupport
    with FunSuiteLike
    with Matchers
    with Eventually
    with BeforeAndAfterAll
    with MockitoSugar {

  test("Replace without health checks") {
    val app = AppDefinition(id = "myApp".toPath, instances = 5, upgradeStrategy = UpgradeStrategy(0.0))
    val driver = mock[SchedulerDriver]
    val taskA = MarathonTestHelper.runningTask("taskA_id")
    val taskB = MarathonTestHelper.runningTask("taskB_id")
    val queue = mock[LaunchQueue]
    val tracker = mock[TaskTracker]

    when(tracker.appTasksLaunchedSync(app.id)).thenReturn(Iterable(taskA, taskB))
    when(driver.killTask(any[TaskID])).thenAnswer(new Answer[Status] {
      def answer(invocation: InvocationOnMock): Status = {
        val taskId = invocation.getArguments()(0).asInstanceOf[TaskID]
        val update = MesosStatusUpdateEvent("", Task.Id(taskId), "TASK_KILLED", "", app.id, "", Nil, Nil, app.version.toString)
        system.eventStream.publish(update)
        Status.DRIVER_RUNNING
      }
    })

    val promise = Promise[Unit]()

    val ref = TestActorRef(
      new TaskReplaceActor(
        driver,
        queue,
        tracker,
        system.eventStream,
        app,
        promise))

    watch(ref)

    for (i <- 0 until app.instances)
      ref ! MesosStatusUpdateEvent("", Task.Id(s"task_$i"), "TASK_RUNNING", "", app.id, "", Nil, Nil, app.version.toString)

    Await.result(promise.future, 5.seconds)
    verify(queue).resetDelay(app)
    verify(driver).killTask(taskA.launchedMesosId.get)
    verify(driver).killTask(taskB.launchedMesosId.get)

    expectTerminated(ref)
  }

  test("Replace with health checks") {
    val app = AppDefinition(
      id = "myApp".toPath,
      instances = 5,
      healthChecks = Set(HealthCheck()),
      upgradeStrategy = UpgradeStrategy(0.0))

    val driver = mock[SchedulerDriver]
    val taskA = MarathonTestHelper.runningTask("taskA_id")
    val taskB = MarathonTestHelper.runningTask("taskB_id")
    val queue = mock[LaunchQueue]
    val tracker = mock[TaskTracker]

    when(tracker.appTasksLaunchedSync(app.id)).thenReturn(Iterable(taskA, taskB))
    when(driver.killTask(any[TaskID])).thenAnswer(new Answer[Status] {
      def answer(invocation: InvocationOnMock): Status = {
        val taskId = invocation.getArguments()(0).asInstanceOf[TaskID]
        val update = MesosStatusUpdateEvent("", Task.Id(taskId), "TASK_KILLED", "", app.id, "", Nil, Nil, app.version.toString)
        system.eventStream.publish(update)
        Status.DRIVER_RUNNING
      }
    })

    val promise = Promise[Unit]()

    val ref = TestActorRef(
      new TaskReplaceActor(
        driver,
        queue,
        tracker,
        system.eventStream,
        app,
        promise))

    watch(ref)

    for (i <- 0 until app.instances)
      ref ! HealthStatusChanged(app.id, Task.Id(s"task_$i"), app.version, alive = true)

    Await.result(promise.future, 5.seconds)
    verify(queue).resetDelay(app)
    verify(driver).killTask(taskA.launchedMesosId.get)
    verify(driver).killTask(taskB.launchedMesosId.get)

    expectTerminated(ref)
  }

  test("Replace and scale down from more than new minCapacity") {
    val app = AppDefinition(id = "myApp".toPath, instances = 2, upgradeStrategy = UpgradeStrategy(1.0))
    val driver = mock[SchedulerDriver]
    val taskA = MarathonTestHelper.runningTask("taskA_id")
    val taskB = MarathonTestHelper.runningTask("taskB_id")
    val taskC = MarathonTestHelper.runningTask("taskC_id")
    val queue = mock[LaunchQueue]
    val tracker = mock[TaskTracker]

    when(tracker.appTasksLaunchedSync(app.id)).thenReturn(Iterable(taskA, taskB, taskC))
    when(driver.killTask(any[TaskID])).thenAnswer(new Answer[Status] {
      def answer(invocation: InvocationOnMock): Status = {
        val taskId = invocation.getArguments()(0).asInstanceOf[TaskID]
        val update = MesosStatusUpdateEvent("", Task.Id(taskId), "TASK_KILLED", "", app.id, "", Nil, Nil, app.version.toString)
        system.eventStream.publish(update)
        Status.DRIVER_RUNNING
      }
    })

    val promise = Promise[Unit]()

    val ref = TestActorRef(
      new TaskReplaceActor(
        driver,
        queue,
        tracker,
        system.eventStream,
        app,
        promise))

    watch(ref)

    eventually { verify(driver, times(2)).killTask(_) }
    eventually { app: AppDefinition => verify(queue, times(2)).add(app) }

    ref ! MesosStatusUpdateEvent("", Task.Id("task_1"), "TASK_RUNNING", "", app.id, "", Nil, Nil, app.version.toString)
    ref ! MesosStatusUpdateEvent("", Task.Id("task_2"), "TASK_RUNNING", "", app.id, "", Nil, Nil, app.version.toString)

    Await.result(promise.future, 5.seconds)

    eventually { verify(driver, times(3)).killTask(_) }
    verify(queue).resetDelay(app)

    expectTerminated(ref)
  }

  test("Replace with minimum running tasks") {
    val app = AppDefinition(
      id = "myApp".toPath,
      instances = 3,
      healthChecks = Set(HealthCheck()),
      upgradeStrategy = UpgradeStrategy(0.5)
    )

    val driver = mock[SchedulerDriver]
    val taskA = MarathonTestHelper.runningTask("taskA_id")
    val taskB = MarathonTestHelper.runningTask("taskB_id")
    val taskC = MarathonTestHelper.runningTask("taskC_id")
    val queue = mock[LaunchQueue]
    val tracker = mock[TaskTracker]

    var oldTaskCount = 3

    when(tracker.appTasksLaunchedSync(app.id)).thenReturn(Iterable(taskA, taskB, taskC))
    when(driver.killTask(any[TaskID])).thenAnswer(new Answer[Status] {
      override def answer(invocation: InvocationOnMock): Status = {
        val taskId = invocation.getArguments()(0).asInstanceOf[TaskID]
        val update = MesosStatusUpdateEvent("", Task.Id(taskId), "TASK_KILLED", "", app.id, "", Nil, Nil, app.version.toString)
        system.eventStream.publish(update)

        oldTaskCount -= 1
        Status.DRIVER_RUNNING
      }
    })

    val promise = Promise[Unit]()

    val ref = TestActorRef(
      new TaskReplaceActor(
        driver,
        queue,
        tracker,
        system.eventStream,
        app,
        promise))

    watch(ref)

    // all new tasks are queued directly
    eventually { app: AppDefinition => verify(queue, times(3)).add(app) }

    // ceiling(minimumHealthCapacity * 3) = 2 are left running
    assert(oldTaskCount == 2)

    // first new task becomes healthy and another old task is killed
    ref ! HealthStatusChanged(app.id, Task.Id(s"task_0"), app.version, alive = true)
    eventually { oldTaskCount should be(1) }

    // second new task becomes healthy and the last old task is killed
    ref ! HealthStatusChanged(app.id, Task.Id(s"task_1"), app.version, alive = true)
    eventually { oldTaskCount should be(0) }

    // third new task becomes healthy
    ref ! HealthStatusChanged(app.id, Task.Id(s"task_2"), app.version, alive = true)
    oldTaskCount should be(0)

    Await.result(promise.future, 5.seconds)

    // all old tasks are killed
    verify(driver).killTask(taskA.launchedMesosId.get)
    verify(driver).killTask(taskB.launchedMesosId.get)
    verify(driver).killTask(taskC.launchedMesosId.get)

    expectTerminated(ref)
  }

  test("Replace with rolling upgrade without over-capacity") {
    val app = AppDefinition(
      id = "myApp".toPath,
      instances = 3,
      healthChecks = Set(HealthCheck()),
      upgradeStrategy = UpgradeStrategy(0.5, 0.0)
    )

    val driver = mock[SchedulerDriver]
    val taskA = MarathonTestHelper.runningTask("taskA_id")
    val taskB = MarathonTestHelper.runningTask("taskB_id")
    val taskC = MarathonTestHelper.runningTask("taskC_id")
    val queue = mock[LaunchQueue]
    val tracker = mock[TaskTracker]

    var oldTaskCount = 3

    when(tracker.appTasksLaunchedSync(app.id)).thenReturn(Iterable(taskA, taskB, taskC))
    when(driver.killTask(any[TaskID])).thenAnswer(new Answer[Status] {
      def answer(invocation: InvocationOnMock): Status = {
        val taskId = Task.Id(invocation.getArguments()(0).asInstanceOf[TaskID])
        val update = MesosStatusUpdateEvent("", taskId, "TASK_KILLED", "", app.id, "", Nil, Nil, app.version.toString)
        system.eventStream.publish(update)

        oldTaskCount -= 1
        Status.DRIVER_RUNNING
      }
    })

    val promise = Promise[Unit]()

    val ref = TestActorRef(
      new TaskReplaceActor(
        driver,
        queue,
        tracker,
        system.eventStream,
        app,
        promise))

    watch(ref)

    // only one task is queued directly
    val queueOrder = org.mockito.Mockito.inOrder(queue)
    eventually { queueOrder.verify(queue).add(_: AppDefinition, 1) }

    // ceiling(minimumHealthCapacity * 3) = 2 are left running
    assert(oldTaskCount == 2)

    // first new task becomes healthy and another old task is killed
    ref ! HealthStatusChanged(app.id, Task.Id("task_0"), app.version, alive = true)
    eventually { oldTaskCount should be(1) }
    eventually { queueOrder.verify(queue).add(_: AppDefinition, 1) }

    // second new task becomes healthy and the last old task is killed
    ref ! HealthStatusChanged(app.id, Task.Id("task_1"), app.version, alive = true)
    eventually { oldTaskCount should be(0) }
    eventually { queueOrder.verify(queue).add(_: AppDefinition, 1) }

    // third new task becomes healthy
    ref ! HealthStatusChanged(app.id, Task.Id("task_2"), app.version, alive = true)
    oldTaskCount should be(0)

    Await.result(promise.future, 5.seconds)

    // all old tasks are killed
    verify(queue).resetDelay(app)
    verify(driver).killTask(taskA.launchedMesosId.get)
    verify(driver).killTask(taskB.launchedMesosId.get)
    verify(driver).killTask(taskC.launchedMesosId.get)

    expectTerminated(ref)
  }

  test("Replace with rolling upgrade with minimal over-capacity") {
    val app = AppDefinition(
      id = "myApp".toPath,
      instances = 3,
      healthChecks = Set(HealthCheck()),
      upgradeStrategy = UpgradeStrategy(1.0, 0.0) // 1 task over-capacity is ok
    )

    val driver = mock[SchedulerDriver]
    val taskA = MarathonTestHelper.runningTask("taskA_id")
    val taskB = MarathonTestHelper.runningTask("taskB_id")
    val taskC = MarathonTestHelper.runningTask("taskC_id")
    val queue = mock[LaunchQueue]
    val tracker = mock[TaskTracker]

    var oldTaskCount = 3

    when(tracker.appTasksLaunchedSync(app.id)).thenReturn(Iterable(taskA, taskB, taskC))
    when(driver.killTask(any[TaskID])).thenAnswer(new Answer[Status] {
      def answer(invocation: InvocationOnMock): Status = {
        val taskId = Task.Id(invocation.getArguments()(0).asInstanceOf[TaskID])
        val update = MesosStatusUpdateEvent("", taskId, "TASK_KILLED", "", app.id, "", Nil, Nil, app.version.toString)
        system.eventStream.publish(update)

        oldTaskCount -= 1
        Status.DRIVER_RUNNING
      }
    })

    val promise = Promise[Unit]()

    val ref = TestActorRef(
      new TaskReplaceActor(
        driver,
        queue,
        tracker,
        system.eventStream,
        app,
        promise))

    watch(ref)

    // only one task is queued directly, all old still running
    val queueOrder = org.mockito.Mockito.inOrder(queue)
    eventually { queueOrder.verify(queue).add(_: AppDefinition, 1) }
    assert(oldTaskCount == 3)

    // first new task becomes healthy and another old task is killed
    ref ! HealthStatusChanged(app.id, Task.Id("task_0"), app.version, alive = true)
    eventually { oldTaskCount should be(2) }
    eventually { queueOrder.verify(queue).add(_: AppDefinition, 1) }

    // second new task becomes healthy and another old task is killed
    ref ! HealthStatusChanged(app.id, Task.Id("task_1"), app.version, alive = true)
    eventually { oldTaskCount should be(1) }
    eventually { queueOrder.verify(queue).add(_: AppDefinition, 1) }

    // third new task becomes healthy and last old task is killed
    ref ! HealthStatusChanged(app.id, Task.Id("task_2"), app.version, alive = true)
    eventually { oldTaskCount should be(0) }
    queueOrder.verify(queue, never()).add(_: AppDefinition, 1)

    Await.result(promise.future, 5.seconds)

    // all old tasks are killed
    verify(driver).killTask(taskA.launchedMesosId.get)
    verify(driver).killTask(taskB.launchedMesosId.get)
    verify(driver).killTask(taskC.launchedMesosId.get)

    expectTerminated(ref)
  }

  test("Replace with rolling upgrade with 2/3 over-capacity") {
    val app = AppDefinition(
      id = "myApp".toPath,
      instances = 3,
      healthChecks = Set(HealthCheck()),
      upgradeStrategy = UpgradeStrategy(1.0, 0.7)
    )

    val driver = mock[SchedulerDriver]
    val taskA = MarathonTestHelper.runningTask("taskA_id")
    val taskB = MarathonTestHelper.runningTask("taskB_id")
    val taskC = MarathonTestHelper.runningTask("taskC_id")
    val queue = mock[LaunchQueue]
    val tracker = mock[TaskTracker]

    var oldTaskCount = 3

    when(tracker.appTasksLaunchedSync(app.id)).thenReturn(Iterable(taskA, taskB, taskC))
    when(driver.killTask(any[TaskID])).thenAnswer(new Answer[Status] {
      def answer(invocation: InvocationOnMock): Status = {
        val taskId = Task.Id(invocation.getArguments()(0).asInstanceOf[TaskID])
        val update = MesosStatusUpdateEvent("", taskId, "TASK_KILLED", "", app.id, "", Nil, Nil, app.version.toString)
        system.eventStream.publish(update)

        oldTaskCount -= 1
        Status.DRIVER_RUNNING
      }
    })

    val promise = Promise[Unit]()

    val ref = TestActorRef(
      new TaskReplaceActor(
        driver,
        queue,
        tracker,
        system.eventStream,
        app,
        promise))

    watch(ref)

    // two tasks are queued directly, all old still running
    val queueOrder = org.mockito.Mockito.inOrder(queue)
    eventually { queueOrder.verify(queue).add(_: AppDefinition, 2) }
    assert(oldTaskCount == 3)

    // first new task becomes healthy and another old task is killed
    ref ! HealthStatusChanged(app.id, Task.Id("task_0"), app.version, alive = true)
    eventually { oldTaskCount should be(2) }
    eventually { queueOrder.verify(queue).add(_: AppDefinition, 1) }

    // second new task becomes healthy and another old task is killed
    ref ! HealthStatusChanged(app.id, Task.Id("task_1"), app.version, alive = true)
    eventually { oldTaskCount should be(1) }
    queueOrder.verify(queue, never()).add(_: AppDefinition, 1)

    // third new task becomes healthy and last old task is killed
    ref ! HealthStatusChanged(app.id, Task.Id("task_2"), app.version, alive = true)
    eventually { oldTaskCount should be(0) }
    queueOrder.verify(queue, never()).add(_: AppDefinition, 1)

    Await.result(promise.future, 5.seconds)

    // all old tasks are killed
    verify(driver).killTask(taskA.launchedMesosId.get)
    verify(driver).killTask(taskB.launchedMesosId.get)
    verify(driver).killTask(taskC.launchedMesosId.get)

    expectTerminated(ref)
  }

  test("Downscale with rolling upgrade with 1 over-capacity") {
    val app = AppDefinition(
      id = "myApp".toPath,
      instances = 3,
      healthChecks = Set(HealthCheck()),
      upgradeStrategy = UpgradeStrategy(1.0, 0.3)
    )

    val driver = mock[SchedulerDriver]
    val taskA = MarathonTestHelper.runningTask("taskA_id")
    val taskB = MarathonTestHelper.runningTask("taskB_id")
    val taskC = MarathonTestHelper.runningTask("taskC_id")
    val taskD = MarathonTestHelper.runningTask("taskD_id")
    val queue = mock[LaunchQueue]
    val tracker = mock[TaskTracker]

    var oldTaskCount = 4

    when(tracker.appTasksLaunchedSync(app.id)).thenReturn(Iterable(taskA, taskB, taskC, taskD))
    when(driver.killTask(any[TaskID])).thenAnswer(new Answer[Status] {
      def answer(invocation: InvocationOnMock): Status = {
        val taskId = Task.Id(invocation.getArguments()(0).asInstanceOf[TaskID])
        val update = MesosStatusUpdateEvent("", taskId, "TASK_KILLED", "", app.id, "", Nil, Nil, app.version.toString)
        system.eventStream.publish(update)

        oldTaskCount -= 1
        Status.DRIVER_RUNNING
      }
    })

    val promise = Promise[Unit]()

    val ref = TestActorRef(
      new TaskReplaceActor(
        driver,
        queue,
        tracker,
        system.eventStream,
        app,
        promise))

    watch(ref)

    // one task is queued directly, one task is killed immediately
    val queueOrder = org.mockito.Mockito.inOrder(queue)
    eventually { queueOrder.verify(queue).add(app, 1) }
    assert(oldTaskCount == 3)

    // first new task becomes healthy and another old task is killed
    ref ! HealthStatusChanged(app.id, Task.Id("task_0"), app.version, alive = true)
    eventually { oldTaskCount should be(2) }
    eventually { queueOrder.verify(queue).add(app, 1) }

    // second new task becomes healthy and another old task is killed
    ref ! HealthStatusChanged(app.id, Task.Id("task_1"), app.version, alive = true)
    eventually { oldTaskCount should be(1) }
    eventually { queueOrder.verify(queue).add(app, 1) }

    // third new task becomes healthy and last old task is killed
    ref ! HealthStatusChanged(app.id, Task.Id("task_2"), app.version, alive = true)
    eventually { oldTaskCount should be(0) }
    eventually { queueOrder.verify(queue, never()).add(app, 1) }

    Await.result(promise.future, 5.seconds)

    // all old tasks are killed
    verify(driver).killTask(taskA.launchedMesosId.get)
    verify(driver).killTask(taskB.launchedMesosId.get)
    verify(driver).killTask(taskC.launchedMesosId.get)
    verify(driver).killTask(taskD.launchedMesosId.get)

    expectTerminated(ref)
  }

  test("Cancelled") {
    val app = AppDefinition(id = "myApp".toPath, instances = 2)
    val driver = mock[SchedulerDriver]
    val taskA = MarathonTestHelper.runningTask("taskA_id")
    val taskB = MarathonTestHelper.runningTask("taskB_id")
    val queue = mock[LaunchQueue]
    val tracker = mock[TaskTracker]

    when(tracker.appTasksLaunchedSync(app.id)).thenReturn(Iterable(taskA, taskB))

    val promise = Promise[Unit]()

    val ref = TestActorRef(
      new TaskReplaceActor(
        driver,
        queue,
        tracker,
        system.eventStream,
        app,
        promise))

    watch(ref)

    system.stop(ref)

    intercept[TaskUpgradeCanceledException] {
      Await.result(promise.future, 5.seconds)
    }.getMessage should equal("The task upgrade has been cancelled")

    expectTerminated(ref)
  }

  test("Retry outstanding kills") {
    val app = AppDefinition(id = "myApp".toPath, instances = 5, upgradeStrategy = UpgradeStrategy(0.0))
    val driver = mock[SchedulerDriver]
    val taskA = MarathonTestHelper.runningTask("taskA_id")
    val taskB = MarathonTestHelper.runningTask("taskB_id")
    val queue = mock[LaunchQueue]
    val tracker = mock[TaskTracker]

    when(tracker.appTasksLaunchedSync(app.id)).thenReturn(Iterable(taskA, taskB))
    when(driver.killTask(any[TaskID])).thenAnswer(new Answer[Status] {
      var firstKillForTaskB = true

      def answer(invocation: InvocationOnMock): Status = {
        val taskId = Task.Id(invocation.getArguments()(0).asInstanceOf[TaskID])

        if (taskId == taskB.taskId && firstKillForTaskB) {
          firstKillForTaskB = false
        }
        else {
          val update = MesosStatusUpdateEvent("", taskId, "TASK_KILLED", "", app.id, "", Nil, Nil, app.version.toString)
          system.eventStream.publish(update)
        }
        Status.DRIVER_RUNNING
      }
    })

    val promise = Promise[Unit]()

    val ref = TestActorRef(
      new TaskReplaceActor(
        driver,
        queue,
        tracker,
        system.eventStream,
        app,
        promise))

    watch(ref)

    ref.underlyingActor.periodicalRetryKills.cancel()
    ref ! RetryKills

    for (i <- 0 until app.instances)
      ref ! MesosStatusUpdateEvent("", Task.Id(s"task_$i"), "TASK_RUNNING", "", app.id, "", Nil, Nil, app.version.toString)

    Await.result(promise.future, 5.seconds)
    verify(queue).resetDelay(app)
    verify(driver).killTask(taskA.launchedMesosId.get)
    verify(driver, times(2)).killTask(taskB.launchedMesosId.get)

    expectTerminated(ref)
  }

  test("Wait until the tasks are killed") {
    val app = AppDefinition(id = "myApp".toPath, instances = 5, upgradeStrategy = UpgradeStrategy(0.0))
    val driver = mock[SchedulerDriver]
    val taskA = MarathonTestHelper.runningTask("taskA_id")
    val taskB = MarathonTestHelper.runningTask("taskB_id")
    val queue = mock[LaunchQueue]
    val tracker = mock[TaskTracker]

    when(tracker.appTasksLaunchedSync(app.id)).thenReturn(Iterable(taskA, taskB))

    val promise = Promise[Unit]()

    val ref = TestActorRef(
      new TaskReplaceActor(
        driver,
        queue,
        tracker,
        system.eventStream,
        app,
        promise))

    watch(ref)

    for (i <- 0 until app.instances)
      ref.receive(MesosStatusUpdateEvent("", Task.Id(s"task_$i"), "TASK_RUNNING", "", app.id, "", Nil, Nil, app.version.toString))

    verify(queue, Mockito.timeout(1000)).resetDelay(app)
    verify(driver, Mockito.timeout(1000)).killTask(taskA.launchedMesosId.get)
    verify(driver, Mockito.timeout(1000)).killTask(taskB.launchedMesosId.get)

    promise.isCompleted should be(false)

    for (oldTask <- Iterable(taskA, taskB))
      ref.receive(MesosStatusUpdateEvent("", oldTask.taskId, "TASK_KILLED", "", app.id, "", Nil, Nil, app.version.toString))

    Await.result(promise.future, 0.second)
  }
}

redpajama_set_name: RedPajamaGithub


A plank road connected Glens Falls with Lake George, opening up the stagecoach era in Lake George.
A plank road, or puncheon, is a dirt path or road covered with a series of planks, often built by turnpike companies to cross soft ground. In the late 1840s plank roads were financed by private individuals, corporations or municipalities and then operated as a toll road.
The old Military Road, originally built in 1755 as the path of an old military road, was built over in 1848 as a plank road, leading to the stagecoach era for Lake George and points north to become resort areas. In 1849, the plank road was continued to Warrensburg, and then to Chester and beyond by 1850. The arrival of rail transport led to a decline in the use of the stagecoach route by the 1880s.
The 1848 Toll House is a part of the Old Fort House Museum campus.


You wake up in the morning and look at your phone. It's a total blur. Your child presents you with his homework assignment for review. You can't read it. At a dinner party you go to the ladies' room to touch up your makeup. But the task is impossible because you've forgotten your readers. You stand at the lectern ready to present at your company's annual meeting. In a panic you realize you can't locate your reading glasses. Do any of these scenarios sound familiar?
The KAMRA corneal inlay is a microscopically thin, tiny opaque disk composed of an FDA-approved bio-compatible plastic material with a 1.6 mm central opening. Its brilliant method of effect borrows from a simple principle of physics that teaches us that only focused rays of light can enter an eye through such a reduced aperture. When centered over the pupil of the eye, KAMRA restores reading vision at all distances without any loss of far vision. There have been over 8,000 KAMRA procedures performed in the U.S. since approval. More than 25,000 worldwide and collective studies continue to indicate a stellar track record of effectiveness and safety beyond what we as refractive surgeons could have dreamed. In our practice we have now placed over 250 KAMRA inlays and the life-changing results continue to amaze (see the link to our video testimonials below).
For those individuals who have issues with both distance and near vision, KAMRA can be combined with iDesign custom LASIK—something we have named Pan-LASIK, and because of its mechanism of action, KAMRA never has to be changed or modified. And best of all, on the unlikely chance you feel that you don't like the KAMRA effect—it is completely reversible!


Enjoying your all-inclusive vacation package at Cape Lodge is like taking a vacation within a vacation. Here you don't just get to experience the gorgeous Australian forest scenery of Leeuwin-Naturalistic Ridge, you can satisfy all of your desires for activity and relaxation, as well. This beautifully small, boutique vineyard hotel is located in the heart of Margaret River Wine Country.
At Cape Lodge, you are not just finding a place to lay your hat while looking for something to do: You are surrounded by so many fun things to do that you could have the time of your life without ever leaving the hotel grounds. Cape Lodge offers Garden View, Forest Spa, Main Lodge or Residence rooms (complete with Guest House). Each of these suites features all of the amenities that you would have at home. Keep in touch with business and private matters with the free wi-fi and/or watch an HD TV when you want to wind down.
Cape Lodge's all-inclusive vacation package offers you executive suites as well as private residences that feel more like home. The main residence is more like a condo, boasting amenities like a gazebo that is complete with BBQ, master bedroom, dining room and even a laundry facility of its own.
Of course, no luxury hotel would be complete without a fine restaurant. Cape Lodge's very own chef, Tony Howell, whips up specialized gourmet meals every day using only the freshest in regional foods and produce. Serving premium vintage wine from Margaret River with your meals is how Tony has helped earn Cape Lodge restaurant a Top 10 listing on the "World for Food in Conde Naste Travelers Gold List." You don't want to miss out on the chance to experience what award-winning luxury hotel service like Cape Lodge can do for you.


The soldier remounts and tells the rest of the squad to go through.
The engineers that have come down the elevator, come into the same hallway and join with the group.
They enter the area with the shark tanks and get to work patching one.
The two pods walk closer to the entrance, and they both open up, a dense fog seeping out of the opening.
From one pod, emerge to groups of mechanical units, one more heavily armed with a blade of energy and a rifle, the other appearing more fluid-like.
The second pod emerges to display an unusual scene. What would appear to be a ceremonial machine, adorned with crystal like objects and a datapad, accompanied by twelve equally adorned units, holding staves as they walk in perfect unison along side the ceremonial unit.
They would enter the elevator, and take as many trips down as necessary to get all of them inside of the base.
The pods seal up once more, sitting idly as the pods wait.
The man with the clipboard sends a message to the Moira.
So did you take care of the robotics bay? I'm still not getting any response from there.
The man with the clipboard looks at the new arrivals.
Welcome to Sealab, if you heard the broadcast we need help repairing the station so it doesn't go under, if you're not up to that task then we'll compensate you for any supplies you deliver and you can be on your way.
The scientists oblivious to any newcomers as they frantically attempt to patch eying the giant tank nervously as hairline cracks form throughout its glass. The Rhyndir's repairs would be working against the clock while the cracks continue to form. If the Rhyndir cannot speed up the repairs, the giant tank will rupture inundating the area. A total of ten minutes is left before the cracks break the glass.
Negative. Robotics possibly reprogrammed, they are hostile.
The squads enter the room, one squad taking aim and staying behind at the entrance to cover them. The three teams move in a half circle formation, spreading out but the edge of their formation sticking to the wall.
The engineers continue to patch the tanks hoping it will work out. A slightly smaller than average engineer makes a call up to the ship to bring down as many men as possible to help patch the tanks due to their imminent situation.
The abominations staring out to the workers have little effect on their performance.
With only a few minutes to spare, the engineers arrive with usable mixtures of adhesive that resist water in order to seal the cracks with sufficient strength.
The engineers work as fast as possible to fix the tanks completely before the cracks catch up.
The elaborately dressed unit looks blankly at the human holding the clipboard. It proceeds to tap in some commands on its own tablet device, but whatever it was doing appears futile. The unit would sketch out a drawing of what appears to be an unarmored Zaedrax shrugging. A second drawing is made next to it, illustrating a vessel. Clearly there is a language barrier, and the unit releases a few high-pitched strings of beeps and whirs to his comrades who once again stare blindly at the human.
The combat units sit awkwardly, their intimidating shells seem to have no benefit in diplomacy, and they shift side to side, perhaps impatiently.


[CBC News] Edmonton police are investigating after a group of elderly women, armed with canes, protected their parish priest from an unprovoked assault.
Father Marcin Mironiuk had just finished leading evening mass at Our Lady Queen of Poland Parish in south Edmonton on March 13 when he was approached by a young man he didn't recognize.
Within seconds, the stranger threw Mironiuk to the ground and wrapped his hands tightly around Mironiuk's neck, said Lorraine Turchansky, a spokesperson for the Catholic Archdiocese of Edmonton.
As Mironiuk struggled to breathe, parishioners responded with the only weapons they had at their disposal — their walking canes.
"They were elderly ladies who were very shocked, understandably, but they did manage to intervene," Turchansky said in an interview Tuesday with CBC Radio's Edmonton AM.


text:
beamizrahi@gmail.com
Béatrice Mizrahi is a director of photography who can adapt to any situation. Her capacity to adapt to the most unexpected filming conditions, as well as her energy and enthusiasm, have provided work possibilities not only in France but also in many countries abroad. For Béatrice, creating the lighting and images of a film is always something intense and important, a unique, delightful experience that she honors by giving the best of herself on every project, no matter what it may be.
From her years as a camera assistant with some of France's best cinematographers, such as Henri Alekan or Bruno Delbonnel, to name just two of the many renowned lighting cameramen she has worked with, Béatrice acquired a mastery of the discipline's technical skills, as well as a strong affinity for working within a team.
In over eighteen years as a director of photography, she developed her style in many areas of the audiovisual world: feature films, commercials, TV dramas, documentaries, and music videos. Her passion for her craft is rooted in the belief that it is the pleasure of working together on the set and the harmonious relations of the crew that will create the energy that is the ultimate motivating force behind the making of the film.
Like jazz pianists, whom she loves, Béatrice prefers to play down her technical knowledge in order to be able to seize the present moment and thus improvise with her lighting, using whatever is available to her.
For Béatrice, lighting a film is above all about participating in a story. For her it means transforming the director's intentions into images, while placing at the director's disposal her own vision and esthetic culture.
Press book:

redpajama_set_name: RedPajamaCommonCrawl


text:
Nomkhita Mona appointed chair of Eastern Cape tourism body
Nomkhita Mona and Tracey Putzier have been appointed chair and deputy chair of the Eastern Cape Parks and Tourism Agency (ECPTA).
Nomkhita is the CEO of the Nelson Mandela Bay Business Chamber. She has extensive experience in the tourism sector, having previously served as as CEO of the Eastern Cape Tourism Board from 2000 until 2004.
""It is a privilege to be appointed to chair this board," she said.
"I am very excited that I am able to be part of the team to contribute towards the growth of the Eastern Cape. There are some really big projects that we are going to push forward and I am very excited by the other members of the board who will be working with us towards taking the entity forward."
Nomkhita has an MBA from Rhodes University and a Master's degree in Labour Relations and Human Resources from Nelson Mandela University.
Tracey Putzier served on the ECPTA Board from 2006 until 2012 and has a proven track record of tourism marketing and tourism development in the Eastern Cape.
The rest of the new Board include Africa's first black evolutionary physiologist Dr Nomakwezi Mzilikazi, economist Siyabulela Onceya, Zola Fihlani, Andrew Baxter, Zolani Mkiva and Lindani Bevileas.


text:
Q: Spring boot REST service Kafka topic commitSync failing I have a simple Spring boot service that is called on-demand and consumes specified number of messages from the topic. Number of messages to consume is passed as a parameter. Service is being called every 30 minutes. Each message size is ~1.6 kb. I always get around 1100 or 1200 messages every-time. Have one topic with one partition only and REST service is the only consumer. Here is how the service is called http://example.com/messages?limit=2000
private OutputResponse getNewMessages(String limit) throws Exception {

        System.out.println("***** START *****");

        final long start = System.nanoTime();

        int loopCntr = 0;
        int counter = 0;
        OutputResponse outputResponse = new OutputResponse();
        Output output = new Output();
        List<Output> rspListObject = new ArrayList<>();
        Consumer<Object, Object> consumer = null;
        String result = null;

        try {
            Properties p = new Properties();
            p.put(ConsumerConfig.MAX_POLL_INTERVAL_MS_CONFIG, "180000");
            p.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, limit);

            consumer = consumerFactory.createConsumer("my-group-id", null, null, p);
            consumer.assign(Collections.singleton(new TopicPartition("test-topic", 0)));

            while (loopCntr < 2) {
                loopCntr++;
                ConsumerRecords<Object, Object> consumerRecords = consumer.poll(Duration.ofSeconds(15));

                for (ConsumerRecord<Object, Object> record : consumerRecords)
                {
                    counter++;
                    try
                    {
                        //get json string
                        result = mapper.writeValueAsString(record.value());
                        //to json
                        output = mapper.readValue(result, Output.class);
                        rspListObject.add(output);
                    } catch (Exception e) {
                        logger.error(e);
                        insertToDB(record.value(),record.offset());
                    }
                }
            }

            outputResponse.setObjects(rspListObject);

            final long end = System.nanoTime();
            System.out.println("Took: " + ((end - start) / 1000000) + "ms");
            System.out.println("Took: " + (end - start) / 1000000000 + " seconds");

            // commit the offset of records to broker
            if (counter > 0) {
                consumer.commitSync();
            }
        } finally {
            try {
                System.out.println(" >>>>> closing the  consumer");
                if (consumer != null)
                    consumer.close();
            }catch(Exception e){
                //log message
            }
        }

        return outputResponse;
    }

this is what I have in application.yml
spring:
  kafka:
    consumer:
      enable-auto-commit: false
      auto-offset-reset: latest
      key-deserializer: org.springframework.kafka.support.serializer.ErrorHandlingDeserializer
      value-deserializer: org.springframework.kafka.support.serializer.ErrorHandlingDeserializer
      properties:
        spring.deserializer.key.delegate.class: org.apache.kafka.common.serialization.StringDeserializer
        spring.deserializer.value.delegate.class: org.springframework.kafka.support.serializer.JsonDeserializer
        spring.json.trusted.packages: '*'
        max.poll.interval.ms: 300000
      group-id: my-group-id

ConsumerConfig values:
allow.auto.create.topics = true
auto.commit.interval.ms = 5000
auto.offset.reset = latest
check.crcs = true
client.dns.lookup = default
client.id =
client.rack =
connections.max.idle.ms = 540000
default.api.timeout.ms = 60000
enable.auto.commit = false
exclude.internal.topics = true
fetch.max.bytes = 52428800
fetch.max.wait.ms = 500
fetch.min.bytes = 1
group.id = my-group-id
group.instance.id = null
heartbeat.interval.ms = 3000
interceptor.classes = []
internal.leave.group.on.close = true
isolation.level = read_uncommitted
key.deserializer = class org.springframework.kafka.support.serializer.ErrorHandlingDeserializer
max.partition.fetch.bytes = 1048576
max.poll.interval.ms = 180000
max.poll.records = 500
metadata.max.age.ms = 300000
metric.reporters = []
metrics.num.samples = 2
metrics.recording.level = INFO
metrics.sample.window.ms = 30000
partition.assignment.strategy = [class org.apache.kafka.clients.consumer.RangeAssignor]
receive.buffer.bytes = 65536
reconnect.backoff.max.ms = 1000
reconnect.backoff.ms = 50
request.timeout.ms = 30000
retry.backoff.ms = 100

This is an error I am getting at commitSync. Tried with consuming 5 messages when doing poll(), tried with setting p.put(ConsumerConfig.MAX_POLL_INTERVAL_MS_CONFIG, "180000"); but same error

Commit cannot be completed since the group has already rebalanced and
assigned the partitions to another member. This means that the time
between subsequent calls to poll() was longer than the configured
max.poll.interval.ms, which typically implies that the poll loop is
spending too much time message processing. You can address this either
by increasing max.poll.interval.ms or by reducing the maximum size of
batches returned in poll() with max.poll.records.

A: I believe that this application simulates your use case but it doesn't exhibit the behavior you describe (as I expected). You should never see a rebalance when manually assigning the topic/partition.
I suggest you run both and compare DEBUG logs to figure out what's wrong.
@SpringBootApplication
public class So63713473Application {

    public static void main(String[] args) {
        SpringApplication.run(So63713473Application.class, args);
    }

    @Bean
    public NewTopic topic() {
        return TopicBuilder.name("so63713473").partitions(1).replicas(1).build();
     }

    @Bean
    public ApplicationRunner runner(ConsumerFactory<String, String> factory, KafkaTemplate<String, String> template) {
        String msg = new String(new byte[1600]);
        return args -> {
            while (true) {
                System.out.println("Hit enter to run a consumer");
                System.in.read();
                int count = 0;
                try (Consumer<String, String> consumer = factory.createConsumer("so63713473", "")) {
                    IntStream.range(0, 1200).forEach(i -> template.send("so63713473", msg));
                    consumer.assign(Collections.singletonList(new TopicPartition("so63713473", 0)));
                    while (count < 1200) {
                        ConsumerRecords<String, String> records = consumer.poll(Duration.ofSeconds(5));
                        count += records.count();
                        System.out.println("Count=" + count);
                    }
                    consumer.commitSync();
                    System.out.println("Success");
                }
            }
        };
    }

}

spring.kafka.consumer.auto-offset-reset=earliest
spring.kafka.consumer.fetch-min-size=1920000
spring.kafka.consumer.fetch-max-wait=1000

spring.kafka.producer.properties.linger.ms=50

EDIT
I can reproduce your issue by adding a second (auto-assigned) consumer in the same group.
@KafkaListener(id = "so63713473", topics = "so63713473")
public void listen(String in) {
    System.out.println(in);
}

2020-09-08 16:40:15.828 ERROR 88813 --- [           main] o.s.boot.SpringApplication               : Application run failed

java.lang.IllegalStateException: Failed to execute ApplicationRunner
    at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:789) [spring-boot-2.3.3.RELEASE.jar:2.3.3.RELEASE]
    at org.springframework.boot.SpringApplication.callRunners(SpringApplication.java:776) [spring-boot-2.3.3.RELEASE.jar:2.3.3.RELEASE]
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:322) [spring-boot-2.3.3.RELEASE.jar:2.3.3.RELEASE]
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:1237) [spring-boot-2.3.3.RELEASE.jar:2.3.3.RELEASE]
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:1226) [spring-boot-2.3.3.RELEASE.jar:2.3.3.RELEASE]
    at com.example.demo.So63713473Application.main(So63713473Application.java:25) [classes/:na]
Caused by: org.apache.kafka.clients.consumer.CommitFailedException: Commit cannot be completed since the group has already rebalanced and assigned the partitions to another member. This means that the time between subsequent calls to poll() was longer than the configured max.poll.interval.ms, which typically implies that the poll loop is spending too much time message processing. You can address this either by increasing max.poll.interval.ms or by reducing the maximum size of batches returned in poll() with max.poll.records.

You can't mix manual and auto assignment in the same group.

redpajama_set_name: RedPajamaStackExchange


Our customers use a variety of fiber management products from many different manufacturers, depending on the environment they are working in, so we offer products from OFS (the former Lucent LGX line), Sumitomo, ADC, Multilink, Corning, Clearfield, Bejed, AFL, and others.
We stock rack mount enclosures for 19" or 23" racks, supporting from 48 to 144 fibers. Other rack and wall mount configurations are readily available and in many cases can be delivered in one or two days. We stock adapter plates for SC, SC duplex, LC, LC duplex, ST, and FC, both loaded and unloaded.
Enclosures are available for patch, patch and splice, or splice.
We carry MTP to LC, MT, or SC cassettes for insertion in to fiber enclosures which provide a plug and play solution for higher fiber count network connections. When combined with our pre-terminated MTP cables you can save time and labor in establishing or extending fiber links. This is an ideal solution for quickly and easily establishing or extending high speed connections in data centers, computer rooms, or MTSO's.
Products are available for inside or outside use, for the main distribution frame, intermediate frame, closet, or premise.


One thing I have found when building Dashboards is that I tend to want to see the same thing several different ways.
What would make this a lot easier is if I could copy/paste a Dashboard Widget, then make whatever changes I want to the new Widget. Maybe I change the chart type, maybe I change the Legend, etc.
In addition to copy/paste a Widget within the same Dashboard, it would be really nice to be able to copy/paste Dashboard Widgets between two different Dashboards. Maybe I'm building Dashboards by job role and two different roles need the same Dashboard Widget, or I already have a Widget built and just need to modify it slightly.
Having these two features would make building Dashboards go a lot faster and make them utilized a lot more frequently!
ACU-I-1526 has 6 votes. Might want to pile on.


Hello, my name is Anderson. I've been apart of a regiment and have attended events from Native to NW and NaS for several years. Giving admins the ability to disable chat has been a recommended and strongly supported feature that can and should be implemented in all servers when different cultures, people, and arguments/conflicts and world views collide. This goes for communities with a large amount of people who disrupt large scale events by presenting an argument that is biased and creates tension with the community. Things like these should be kept to ourselves on teamspeaks, discords, skype/steam chats, etc. and presents a bad image of the community by only several small groups. This also creates a tendency for people to 'game throw' as it's called as well, giving away tactics to the other team or trying reverse psychology on them to do the opposite. The game and events at hand should be focused on your perspective. One step the developers have already gone onto with this is with ghosting and giving admins the ability to have free look, to player, or to players' camera in spectator; I ask, why can't this be the same?
The counter argument to this will be not allowing players to give free speech or to communicate with their team or that you can already mute players in chat on your screen. However, no matter what, you have to keep in mind that these admins are most likely people who care and pay for the server and their community. Their dominion over their server should be of the upmost priority rather than public players or even members of a community who aren't admins to give an opinion about different world events and views that should be kept elsewhere. Communication does come into point, but admins should be able to regulate between disabling team chat or global chat or both depending on the situation such as free play or events, and to those who don't, the people will go elsewhere and find people who do. Finally muting players in your own chat doesn't necessarily solve the problem for the community, more so for yourself to avoid chat that may ensue. I couldn't see an entire community using the mute all players in their own chat as some people want reactions.
Since admins cannot force people to do things, nor can the micromanage every person in a 1000+ community, developers need to update the multiplayer to allow the admins to disable chat at the time and the place on the user interface, menu, or admin panel, only allowing admin and internal admin chat to remain on to relay rules or reasons for something. Tactics and voice chat with regiments or groups in game should be discussed and planned on third party applications like Teamspeak, Discord or Skype, and opinionated discussions should be outside of a certain time frame or event.
Not all communities would have to follow this or use this tool, nor does it have to be forced or maintained by communities that require chat to work, but it should be an option of those who want to keep their community focused on the game at hand. Also, going back to how this solves a community problem and not an individual problem, disabling the chat removes admins to need to view logs for offensive or racist statements (whether or not you believe in that), and helps players focus on the game than what the chat is saying. Spamming is another problem that happens, whether its spam from an individual or the entire sever, it fills up the chat box which breaks it so you can no longer view kills or important messages from team communication temporarily. It's an annoyance that no one should be have to deal with and it is easily exploitable without an ability to disable chat.
I don't know if the developers care about those who enjoy and remain with the game, as it seems they are focusing on Mount and Blade II, but getting a tiny team and adding this feature which separately disables the team and all chat would both improve stability and create less conflict that would otherwise drive new players away from the game. A large amount of people I know would support this idea and would publicly state this is the right way to go. Who knows, this might even be considered for Mount and Blade II.
In conclusion, the community in general is very much immature in way it communicates through chats about opinions, and it needs admins to regulate the sort of behavior both at the moment and in their communities with this tool.
the game will not be receiving any more updates but there are mods out there that can mute someone for the entire server. and i am guessing mute all would also work the same way. look or ask around in the modding section to get it for your server.
Finally muting players in your own chat doesn't necessarily solve the problem for the community, more so for yourself to avoid chat that may ensue. I couldn't see an entire community using the mute all players in their own chat as some people want reactions.
there are mods out there that can mute someone for the entire server. and i am guessing mute all would also work the same way.
My point still stands about the mute all players, if you've read it all. This doesnt solve a problem for everyone, only for a certain person or group; I have never heard of a mod that disables chats for specific players.
The devs are working on their new game. NW hasn't seen an update since fall/summer of 2013. They aren't going to change their mind now just to change how mutes works. Thinking they will come back is foolhardy. the devs working on bannerlord are different from the ones that made this. if you want them to implement it, post on their forums.
If you think this should be in BCoF, then make a post in the donator board or in the BCoF part of the forums.
i have came across some scripts in the past that muted someone for the entire server, not just for you.(even some other mods have this) and, i assume, the mute all with that mod would mute everyone.(talk to the scripter about it) i forgot to mention, outside of muting players, afaik it is impossible for anyone to see the code or change it when it comes to all or team chat.(includes NW devs afaik) meaning it is impossible to tinker with them . admin chat has it's own code that can be tweaked.
if you think it would be best for you and your group/lb/whatever, then look into getting a script that mutes chat for everyone.(i dont recall if it blocks internal admin chat, but i am sure one can get around that if the scripter wants to) if you play with the same people all the time, then you can make rules among yourselves regarding chat and how to admin it.
Would there be a possibility this would become a mod, but only server-side so not everyone would have to download it?
Otherwise I think it's an excellent idea.
Its a very unnecessary idea in all honesty. Just purge the people that cannot follow your rules, thats why they are there for.
Also there is a admin script mod. Download it if you want extra tools.